写点什么

2023 前端二面 vue 面试题

作者:bb_xiaxia1998
  • 2023-02-23
    浙江
  • 本文字数:17620 字

    阅读完需:约 58 分钟

如何监听 pushState 和 replaceState 的变化呢?

利用自定义事件new Event()创建这两个事件,并全局监听:


<body>  <button onclick="goPage2()">去page2</button>  <div>Page1</div>  <script>    let count = 0;    function goPage2 () {      history.pushState({ count: count++ }, `bb${count}`,'page1.html')      console.log(history)    }    // 这个不能监听到 pushState    // window.addEventListener('popstate', function (event) {    //   console.log(event)    // })    function createHistoryEvent (type) {      var fn = history[type]      return function () {        // 这里的 arguments 就是调用 pushState 时的三个参数集合        var res = fn.apply(this, arguments)        let e = new Event(type)        e.arguments = arguments        window.dispatchEvent(e)        return res      }    }    history.pushState = createHistoryEvent('pushState')    history.replaceState = createHistoryEvent('replaceState')    window.addEventListener('pushState', function (event) {      // { type: 'pushState', arguments: [...], target: Window, ... }      console.log(event)    })    window.addEventListener('replaceState', function (event) {      console.log(event)    })  </script></body>
复制代码

Vue 路由的钩子函数

首页可以控制导航跳转,beforeEachafterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。


  • beforeEach主要有 3 个参数tofromnext

  • toroute即将进入的目标路由对象。

  • fromroute当前导航正要离开的路由。

  • nextfunction一定要调用该方法resolve这个钩子。执行效果依赖 next方法的调用参数。可以控制网页的跳转

函数式组件优势和原理

函数组件的特点


  1. 函数式组件需要在声明组件是指定 functional:true

  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替

  3. 没有生命周期钩子函数,不能使用计算属性,watch

  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件

  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement

  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)


优点


  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件

  2. 函数式组件结构比较简单,代码结构更清晰


使用场景:


  • 一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件

  • “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件


例子


Vue.component('functional',{ // 构造函数产生虚拟节点的    functional:true, // 函数式组件 // data={attrs:{}}    render(h){        return h('div','test')    }})const vm = new Vue({    el: '#app'})
复制代码


源码相关


// functional componentif (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件  return createFunctionalComponent(Ctor, propsData, data, context, children)}
// extract listeners, since these needs to be treated as// child component listeners instead of DOM listenersconst listeners = data.on // 处理事件// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOn // 处理原生事件
// install component management hooks onto the placeholder nodeinstallComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)
复制代码

Vue 修饰符有哪些

vue 中修饰符分为以下五种

  • 表单修饰符

  • 事件修饰符

  • 鼠标按键修饰符

  • 键值修饰符

  • v-bind修饰符


1. 表单修饰符


在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model


关于表单的修饰符有如下:


  • lazy


在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步


<input type="text" v-model.lazy="value"><p>{{value}}</p>
复制代码


  • trim


自动过滤用户输入的首空格字符,而中间的空格不会过滤


<input type="text" v-model.trim="value">
复制代码


  • number


自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值


<input v-model.number="age" type="number">
复制代码


2. 事件修饰符


事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符


  • .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法


<div @click="shout(2)">  <button @click.stop="shout(1)">ok</button></div>//只输出1
复制代码


  • .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法


<form v-on:submit.prevent="onSubmit"></form>
复制代码


  • .capture 使用事件捕获模式,使事件触发从包含这个元素的顶层开始往下触发


<div @click.capture="shout(1)">    obj1<div @click.capture="shout(2)">    obj2<div @click="shout(3)">    obj3<div @click="shout(4)">    obj4</div></div></div></div>// 输出结构: 1 2 4 3 
复制代码


  • .self 只当在 event.target 是当前元素自身时触发处理函数


<div v-on:click.self="doThat">...</div>
复制代码


使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击


  • .once 绑定了事件以后只能触发一次,第二次就不会触发


<button @click.once="shout(1)">ok</button>
复制代码


  • .passive 告诉浏览器你不想阻止事件的默认行为


在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符


<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --><!-- 而不会等待 `onScroll` 完成  --><!-- 这其中包含 `event.preventDefault()` 的情况 --><div v-on:scroll.passive="onScroll">...</div>
复制代码


  • 不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。

  • passive 会告诉浏览器你不想阻止事件的默认行为


  • native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件


<my-component v-on:click.native="doSomething"></my-component>
<!-- 使用.native修饰符来操作普通HTML标签是会令事件失效的 -->
复制代码


3. 鼠标按钮修饰符


鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:


  • .left 左键点击

  • .right 右键点击

  • .middle 中键点击


<button @click.left="shout(1)">ok</button><button @click.right="shout(1)">ok</button><button @click.middle="shout(1)">ok</button>
复制代码


4. 键盘事件的修饰符


键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:


keyCode存在很多,但 vue 为我们提供了别名,分为以下两种:


  • 普通键entertabdeletespaceescupdownleftright...)

  • 系统修饰键ctrlaltmetashift...)


<!-- 只有按键为keyCode的时候才触发 --><input type="text" @keyup.keyCode="shout()">
复制代码


还可以通过以下方式自定义一些全局的键盘码别名


Vue.config.keyCodes.f2 = 113
复制代码


5. v-bind 修饰符


v-bind修饰符主要是为属性进行操作,用来分别有如下:


  • async 能对props进行一个双向绑定


//父组件<comp :myMessage.sync="bar"></comp> //子组件this.$emit('update:myMessage',params);
复制代码


以上这种方法相当于以下的简写


//父亲组件<comp :myMessage="bar" @update:myMessage="func"></comp>func(e){ this.bar = e;}
//子组件jsfunc2(){ this.$emit('update:myMessage',params);}
复制代码


使用async需要注意以下两点:


  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致

  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用

  • prop 设置自定义标签属性,避免暴露数据,防止污染 HTML 结构


<input id="uid" title="title1" value="1" :index.prop="index">
复制代码


  • camel 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox


<svg :viewBox="viewBox"></svg>
复制代码

应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:


  • .stop:阻止事件冒泡

  • .native:绑定原生事件

  • .once:事件只执行一次

  • .self :将事件绑定在自身身上,相当于阻止事件冒泡

  • .prevent:阻止默认事件

  • .caption:用于事件捕获

  • .once:只触发一次

  • .keyCode:监听特定键盘按下

  • .right:右键

Vue 中 diff 算法原理

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用 diff 算法


vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。


简单来说,Diff 算法有以下过程


  • 同级比较,再比较子节点(根据keytag标签名判断)

  • 先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)

  • 比较都有子节点的情况(核心diff)

  • 递归比较子节点

  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅

  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升



vue3 中采用最长递增子序列来实现diff优化


回答范例


思路


  • diff算法是干什么的

  • 它的必要性

  • 它何时执行

  • 具体执行方式

  • 拔高:说一下vue3中的优化


回答范例


  1. Vue中的diff算法称为patching算法,它由Snabbdom 修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换

  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOMpatching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新

  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch 函数,并传入新旧两次虚拟 DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作

  4. patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3patch为例


  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建

  • 如果双方都是文本则更新文本内容

  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性

  • 更新子节点时又分了几种情况

  • 新的子节点是文本,老的子节点是数组则清空,并设置文本;

  • 新的子节点是文本,老的子节点是文本则直接更新文本;

  • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;

  • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节 blabla

  • vue3中引入的更新策略:静态节点标记等


vdom 中 diff 算法的简易实现


以下代码只是帮助大家理解diff算法的原理和流程


  1. vdom转化为真实dom


const createElement = (vnode) => {  let tag = vnode.tag;  let attrs = vnode.attrs || {};  let children = vnode.children || [];  if(!tag) {    return null;  }  //创建元素  let elem = document.createElement(tag);  //属性  let attrName;  for (attrName in attrs) {    if(attrs.hasOwnProperty(attrName)) {      elem.setAttribute(attrName, attrs[attrName]);    }  }  //子元素  children.forEach(childVnode => {    //给elem添加子元素    elem.appendChild(createElement(childVnode));  })
//返回真实的dom元素 return elem;}
复制代码


  1. 用简易diff算法做更新操作


function updateChildren(vnode, newVnode) {  let children = vnode.children || [];  let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深层次对比, 递归过程 updateChildren(childVnode, newChildVNode); } else { //替换 replaceNode(childVnode, newChildVNode); } })}
复制代码

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码


参考 前端进阶面试题详细解答

vue-router 守卫

导航守卫 router.beforeEach 全局前置守卫


  • to: Route: 即将要进入的目标(路由对象)

  • from: Route: 当前导航正要离开的路由

  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)

  • 执行效果依赖 next 方法的调用参数。

  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

  • next(false):取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)


// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => {   next();});router.beforeResolve((to, from, next) => {  next();});router.afterEach((to, from) => {  console.log('afterEach 全局后置钩子');});
复制代码


路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫


const router = new VueRouter({  routes: [    {      path: '/foo',      component: Foo,      beforeEnter: (to, from, next) => {        // ...      }    }  ]})
复制代码


组件内的守卫你可以在路由组件内直接定义以下路由导航守卫


const Foo = {  template: `...`,  beforeRouteEnter (to, from, next) {    // 在渲染该组件的对应路由被 confirm 前调用    // 不!能!获取组件实例 `this`    // 因为当守卫执行前,组件实例还没被创建  },  beforeRouteUpdate (to, from, next) {    // 在当前路由改变,但是该组件被复用时调用    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。    // 可以访问组件实例 `this`  },  beforeRouteLeave (to, from, next) {    // 导航离开该组件的对应路由时调用,我们用它来禁止用户离开    // 可以访问组件实例 `this`    // 比如还未保存草稿,或者在用户离开前,    将setInterval销毁,防止离开之后,定时器还在调用。  }}
复制代码

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示


switch (mode) {    case 'history':    this.history = new HTML5History(this, options.base)    break    case 'hash':    this.history = new HashHistory(this, options.base, this.fallback)      break    case 'abstract':    this.history = new AbstractHistory(this, options.base)      break  default:    if (process.env.NODE_ENV !== 'production') {      assert(false, `invalid mode: ${mode}`)    }}
复制代码


其中,3 种路由模式的说明如下:


  • hash: 使用 URL hash 值来作路由,支持所有浏览器

  • history : 依赖 HTML5 History API 和服务器配置

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

Vuex 页面刷新数据丢失怎么解决

体验


可以从localStorage中获取作为状态初始值:


const store = createStore({  state () {    return {      count: localStorage.getItem('count')    }  }})
复制代码


业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅


store.commit('increment')localStorage.setItem('count', store.state.count)
复制代码


回答范例


  1. vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来

  2. localStorage就很合适,提交mutation的时候同时存入localStoragestore中把值取出作为state的初始值即可。

  3. 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。

  4. 类似的插件有vuex-persistvuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化


原理


可以看一下vuex-persist (opens new window)内部确实是利用subscribe实现的

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能

  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

Vue3 速度快的原因

Vue3.0 性能提升体现在哪些方面


  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;

  • 编译层面做了更多编译优化处理,比如静态标记pachFlagdiff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;

  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快

  • ssr渲染以字符串方式渲染


一、编译阶段


试想一下,一个组件结构如下图


<template>    <div id="content">        <p class="text">静态文本</p>        <p class="text">静态文本</p>        <p class="text">{ message }</p>        <p class="text">静态文本</p>        ...        <p class="text">静态文本</p>    </div></template>
复制代码


可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费


因此,Vue3 在编译阶段,做了进一步优化。主要有如下:


  • diff算法优化

  • 静态提升

  • 事件监听缓存

  • SSR优化


1. diff 算法优化


  • Vue 2x 中的虚拟 dom 是进行全量的对比。

  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化


Vue2.x 的 diff 算法


vue2.xdiff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的



Vue3.0 的 diff 算法


vue3.0diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了



已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)                        //上面这个1就是静态标记 ]))}
复制代码


关于静态类型枚举如下


TEXT = 1 // 动态文本节点CLASS=1<<1,1 // 2//动态classSTYLE=1<<2,// 4 //动态stylePROPS=1<<3,// 8 //动态属性,但不包含类名和样式FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragmentKEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有keyUNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragmentNEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slotHOISTED = -1 // 静态节点// 指示在diff算法中退出优化模式BALL = -2
复制代码


2. hoistStatic 静态提升


  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。

  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用


<p>HelloWorld</p><p>HelloWorld</p>
<p>{ message }</p>
复制代码


开启静态提升前


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}
复制代码


开启静态提升后编译结果


const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _hoisted_1, _hoisted_2, _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}
复制代码


可以看到开启了静态提升后,直接将那两个内容为helloworldp标签声明在外面了,直接就拿来用了。同时 _hoisted_1_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff


3. cacheHandlers 事件监听缓存


  • 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化

  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可


<div> <button @click = 'onClick'>点我</button></div>
复制代码


开启事件侦听器缓存之前:


export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])                       // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 ]))})
复制代码


这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间


开启事件侦听器缓存之后:


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", {   onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))  }, "点我") ]))}
复制代码


上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用


4. SSR 优化


当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染


<div>    <div>        <span>你好</span>    </div>    ...  // 很多个静态属性    <div>        <span>{{ message }}</span>    </div></div>
复制代码


编译后


import { mergeProps as _mergeProps } from "vue"import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { const _cssVars = { style: { color: _ctx.color }} _push(`<div${ _ssrRenderAttrs(_mergeProps(_attrs, _cssVars)) }><div><span>你好</span>...<div><span>你好</span><div><span>${ _ssrInterpolate(_ctx.message) }</span></div></div>`)}
复制代码


二、源码体积


相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking


任何一个函数,如refreactivecomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小


import { computed, defineComponent, ref } from 'vue';export default defineComponent({  setup(props, context) {    const age = ref(18)
let state = reactive({ name: 'test' })
const readOnlyAge = computed(() => age.value++) // 19
return { age, state, readOnlyAge } }});
复制代码


三、响应式系统


vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式


vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历


  • 可以监听动态属性的添加

  • 可以监听到数组的索引和数组length属性

  • 可以监听删除属性

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。


components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
复制代码


原理


export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {     // async component     let asyncFactory     if (isUndef(Ctor.cid)) {         asyncFactory = Ctor         Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend         // 第二次渲染时Ctor不为undefined         if (Ctor === undefined) {             return createAsyncPlaceholder( // 渲染占位符 空虚拟节点                 asyncFactory,                 data,                 context,                 children,                 tag             )         }     } }function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {     if (isDef(factory.resolved)) {         // 3.在次渲染时可以拿到获取的最新组件         return factory.resolved     }    const resolve = once((res: Object | Class<Component>) => {         factory.resolved = ensureCtor(res, baseCtor)         if (!sync) {             forceRender(true) //2. 强制更新视图重新渲染         } else {             owners.length = 0         }     })    const reject = once(reason => {         if (isDef(factory.errorComp)) {             factory.error = true forceRender(true)         }     })    const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后     sync = false     return factory.resolved }
复制代码

如何定义动态路由?如何获取传过来的动态参数?

(1)param 方式


  • 配置路由格式:/router/:id

  • 传递的方式:在 path 后面跟上对应的值

  • 传递后形成的路径:/router/123


1)路由定义


//在APP.vue中<router-link :to="'/user/'+userId" replace>用户</router-link>    
//在index.js{ path: '/user/:userid', component: User,},
复制代码


2)路由跳转


// 方法1:<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:this.$router.push({name:'users',params:{uname:wade}})
// 方法3:this.$router.push('/user/' + wade)
复制代码


3)参数获取通过 $route.params.userid 获取传递的值


(2)query 方式


  • 配置路由格式:/router,也就是普通配置

  • 传递的方式:对象中使用 query 的 key 作为传递方式

  • 传递后形成的路径:/route?id=123


1)路由定义


//方式1:直接在router-link 标签上以对象的形式<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式<button @click='profileClick'>我的</button>
profileClick(){ this.$router.push({ path: "/profile", query: { name: "kobi", age: "28", height: 198 } });}
复制代码


2)跳转方法


// 方法1:<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:this.$router.push('/user?uname=' + jsmes)
复制代码


3)获取参数


通过$route.query 获取传递的值
复制代码

为什么 Vuex 的 mutation 中不能做异步操作?

  • Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。

  • 每个 mutation 执行完成后都会对应到一个新的状态变更,这样 devtools 就可以打个快照存下来,然后就可以实现 time-travel 了。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

v-model 实现原理

我们在 vue 项目中主要使用 v-model 指令在表单 inputtextareaselect 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖(可以看成是value + input方法的语法糖),v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:


  • texttextarea 元素使用 value 属性和 input 事件

  • checkboxradio 使用 checked 属性和 change 事件

  • select 字段将 value 作为 prop 并将 change 作为事件


所以我们可以 v-model 进行如下改写:


<input v-model="sth" /><!-- 等同于 --><input :value="sth" @input="sth = $event.target.value" />
复制代码


当在input元素中使用v-model实现双数据绑定,其实就是在输入的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定


  • 这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input

  • 知道了v-model的原理,我们可以在自定义组件上实现v-model


//Parent<template>  {{num}}  <Child v-model="num"></template>export default {  data(){    return {      num: 0    }  }}
//Child<template> <div @click="add">Add</div></template>export default { props: ['value'], // 属性必须为value methods:{ add(){ // 方法名为input this.$emit('input', this.value + 1) } }}
复制代码


原理


会将组件的 v-model 默认转化成value+input


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); 
// 观察输出的渲染函数:// with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // }
复制代码


// 源码位置 core/vdom/create-component.js line:155
function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } }
复制代码


原生的 v-model,会根据标签的不同生成不同的事件和属性


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) },// on: {"input": function ($event) { // if ($event.target.composing) return;// value = $event.target.value// }// }// })// }
复制代码


编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js


if (el.component) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false } else if (tag === 'select') {     genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') {     genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') {     genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') {     genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false }
复制代码


运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js


inserted (el, binding, vnode, oldVnode) {     if (vnode.tag === 'select') { // #6903     if (oldVnode.elm && !oldVnode.elm._vOptions) {         mergeVNodeHook(vnode, 'postpatch', () => {             directive.componentUpdated(el, binding, vnode)         })     } else {         setSelected(el, binding, vnode.context)     }    el._vOptions = [].map.call(el.options, getValue)     } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {         el._vModifiers = binding.modifiers         if (!binding.modifiers.lazy) {             el.addEventListener('compositionstart', onCompositionStart)             el.addEventListener('compositionend', onCompositionEnd)             // Safari < 10.2 & UIWebView doesn't fire compositionend when             // switching focus before confirming composition choice             // this also fixes the issue where some browsers e.g. iOS Chrome            // fires "change" instead of "input" on autocomplete.             el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */             if (isIE9) {                 el.vmodel = true             }        }    }}
复制代码

Vue 中 key 的作用

vue 中 key 值的作用可以分为两种情况来考虑:


  • 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。

  • 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。


key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速


  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析


  • 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。

  • 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。



思路


  • 权限管理需求分析:页面和按钮权限

  • 权限管理的实现方案:分后端方案和前端方案阐述

  • 说说各自的优缺点


回答范例


  1. 权限管理一般需求是页面权限和按钮权限的管理

  2. 具体实现的时候分后端和前端两种方案:


  • 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可

  • 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息

  • 按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给 v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮


  1. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!


可能的追问


  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?


<el-tabs>   <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>     <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane></el-tabs>
复制代码


  1. 服务端返回的路由信息如何添加到路由器中?


// 前端组件名和组件映射表const map = {  //xx: require('@/views/xx.vue').default // 同步的⽅式  xx: () => import('@/views/xx.vue') // 异步的⽅式}// 服务端返回的asyncRoutesconst asyncRoutes = [  { path: '/xx', component: 'xx',... }]// 遍历asyncRoutes,将component替换为map[component]function mapComponent(asyncRoutes) {  asyncRoutes.forEach(route => {    route.component = map[route.component];    if(route.children) {      route.children.map(child => mapComponent(child))    }    })}mapComponent(asyncRoutes)
复制代码

异步组件是什么?使用场景有哪些?

分析


因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。


体验


大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们


import { defineAsyncComponent } from 'vue'// defineAsyncComponent定义异步组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容const AsyncComp = defineAsyncComponent(() => {  // 加载函数返回Promise  return new Promise((resolve, reject) => {    // ...可以从服务器加载组件    resolve(/* loaded component */)  })})// 借助打包工具实现ES模块动态导入const AsyncComp = defineAsyncComponent(() =>  import('./components/MyComponent.vue'))
复制代码


回答范例


  1. 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。

  2. 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。

  3. 使用异步组件最简单的方式是直接给defineAsyncComponent指定一个loader函数,结合 ES 模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。

  4. 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件

如何在组件中重复使用 Vuex 的 mutation

使用 mapMutations 辅助函数,在组件中这么使用


import { mapMutations } from 'vuex'methods:{    ...mapMutations({        setNumber:'SET_NUMBER',    })}
复制代码


然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)

怎么监听 vuex 数据的变化

分析


  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。

  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的 API:store.subscribe()


回答范例


  1. 我知道几种方法:


  • 可以通过watch选项或者watch方法监听状态

  • 可以使用vuex提供的 API:store.subscribe()


  1. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。

  2. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中


实践


watch方式


const app = createApp({    watch: {      '$store.state.counter'() {        console.log('counter change!');      }    }})
复制代码


subscribe方式:


store.subscribe((mutation, state) => {    if (mutation.type === 'add') {      console.log('counter change in subscribe()!');    }})
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
2023前端二面vue面试题_Vue_bb_xiaxia1998_InfoQ写作社区