最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 最新的vue面试题大全含源码级回答(vue2篇)

    正文概述 掘金(Chenjunyi)   2021-04-08   560

    前言

    金三银四快过去了,抓紧这段时间再复习下vue,为了在面试官前突出自己,在回答的时候能带上源码的实现和理解往往更容易成功。这里整理了vue常见的面试题和相应的源码及解读,希望对大家有所帮助。篇幅较长,分为vue2篇和vue3篇。

    vue2

    1.vue响应式原理

    回答这个问题,首先要搞清楚什么叫响应式。通常vue中所说的响应式是指数据响应式,数据变化可以被检测并对这种变化做出响应的机制。而在Vue这种MVVM框架中,最重要的核心就是实现数据层和视图层的连接,通过数据驱动应用,数据变化,视图更新。Vue中的方案是数据劫持+发布订阅模式。

    vue在初始化的时候会进行劫持,包括props,data,methods,computed,watcher,并根据数据类型来做不同处理.

    如果是对象则采用Object.defineProperty()的方式定义数据拦截:

    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          return val
        },
        set(v) {
          val = v
          notify()
        }
      })
    }
    

    如果是数组,则覆盖数组的7个变更方法实现变更通知:

    const arrayProto = Array.prototype
    const arrayMethods = Object.create(arrayProto)
    
    ['push','pop','shift','unshift','splice','sort','reverse']
      .forEach(function (method) {
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        notify()
        return result
      })
    })
    

    这是数据劫持的部分,接下来说下视图更新的机制:

    1. 由于 Vue 执行一个组件的 render 函数是由 Watcher 去代理执行的,Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量,等待响应式属性去收集它。
    2. 在组件执行render函数时访问了响应式属性,响应式属性就会精确的收集到当前全局存在的 Dep.target 作为自身的依赖。
    3. 在响应式属性发生更新时通知 Watcher 去重新调用vm._update(vm._render())进行组件的视图更新,视图更新的时候会通过diff算法对比新老vnode差异,通过patch即时更新DOM。

    2.v-if和v-for哪个优先级高

    答案是v-for解析的优先级高,可以在源码的compiler/codegen/index.js 里的genElement函数找到答案

    function genElement (el: ASTElement, state: CodegenState): string {
      if (el.parent) {
        el.pre = el.pre || el.parent.pre
      }
    
      if (el.staticRoot && !el.staticProcessed) {
        return genStatic(el, state)
      } else if (el.once && !el.onceProcessed) {
        return genOnce(el, state)
      } else if (el.for && !el.forProcessed) {
        return genFor(el, state)
      } else if (el.if && !el.ifProcessed) {
        return genIf(el, state)
      } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
        return genChildren(el, state) || 'void 0'
      } else if (el.tag === 'slot') {
        return genSlot(el, state)
      } else {
        // component or element
        let code
        if (el.component) {
          code = genComponent(el.component, el, state)
        } else {
          let data
          if (!el.plain || (el.pre && state.maybeComponent(el))) {
            data = genData(el, state)
          }
    
          const children = el.inlineTemplate ? null : genChildren(el, state, true)
          code = `_c('${el.tag}'${
            data ? `,${data}` : '' // data
          }${
            children ? `,${children}` : '' // children
          })`
        }
        // module transforms
        for (let i = 0; i < state.transforms.length; i++) {
          code = state.transforms[i](el, code)
        }
        return code
      }
    }
    

    vue中的内置指令都有相应的解析函数,执行顺序是通过简单的if else-if语法来确定的。在genFor的函数里,最后会return一个自运行函数,再次调用genElement。

    虽然v-for和v-if可以放一起,但我们要避免这种写法,在官网中也有明确指出,这会造成性能浪费。

    3.key的作用

    作用:用来判断虚拟DOM的某个节点是否为相同节点,用于优化patch性能,patch就是计算diff的函数。

    先看下patch函数:

    function patch (oldVnode, vnode) {
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        let isInitialPatch = false
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
          // empty mount (likely as component), create new root element
          isInitialPatch = true
          createElm(vnode, insertedVnodeQueue)
        } else {
          const isRealElement = isDef(oldVnode.nodeType)
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
          } else {
          // some code
          }
        }
        return vnode
    }
    

    patch函数接收oldVnode和vnode,也就是要比较的新旧节点对象。

    首先会用isUndef函数判断传入的两个vnode是否为空对象再做相应处理。当两个都为节点对象时,再用sameVnode来判断是否为同一节点,再判断本次操作是新增、修改、还是移除。

    function sameVnode (a, b) {
      return (
        a.key === b.key // key值
        && 
          (
            a.tag === b.tag &&  // 标签名
            a.isComment === b.isComment && // 是否为注释节点
            isDef(a.data) === isDef(b.data) && // 是否都定义了data,data包含一些具体信息,例如onclick , style
            sameInputType(a, b) // 当标签是<input>的时候,type必须相同
          )
      )
    }
    

    sameVnode通过判断key、标签名、是否为注释、data等是否相等,来判断是否需要进行比较。

    值得比较则执行patchVnode,不值得比较则用Vnode替换oldVnode,再渲染真实dom。

    patchVnode会对oldVnode和vnode进行对比,然后进行DOM更新。这个会在diff算法里再进行说明。

    v-for通常都是生成一样的标签,所以key会是patch判断是否相同节点的唯一标识,如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,就会去做pathVnode pdateChildren的更新操作,这造成了大量的dom更新操作,所以设置唯一的key是必要的。

    4.双向绑定原理

    vue中双向绑定是一个指令v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model是语法糖,默认情况下相当于:value和@input。

    通常在表单元素可以直接使用v-model,这是vue解析的时候对这些表单元素进行了处理,根据控件类型自动选取正确的方法来更新元素。

    如果是自定义组件的话要使用它需要在组件内绑定props value并在数据更新数据的时候用$emit('input'),也可以在组件里定义modal属性来自定义绑定的属性名和事件名称。

    model: {
        prop: 'checked',
        event: 'change'
    }
    

    5.nextTick原理

    先看下官方文档的说明:

    nextTick就是将回调函数放到队列里去,保证在异步更新DOM的watcher后面,从而获取到更新后的DOM。

    结合src/core/util/next-tick源码再进行分析。

    首先是定义执行任务队列方法

    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    

    按照推入callbacks队列的顺序执行回调函数。

    然后定义timerFunc函数,根据当前环境支持什么方法来确定调用哪个异步方法

    判断的顺序是: Promise > MutationObserver > setImmediate > setTimeout

    最后是定义nextTick方法:

    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        timerFunc()
      }
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

    其实nextTick就是一个把回调函数推入任务队列的方法。

    了解到这里也差不多了,再深入的话可以说vue中数据变化,触发watcher,watcher进入队列的流程,可以看我的另一篇文章vue中的nextTick完整解析。

    6.data为什么是函数

    如果组件里 data 直接写了一个对象的话,那么在模板中多次声明这个组件,组件中的 data 会指向同一个引用。

    此时对 data 进行修改,会导致其他组件里的 data 也被修改。使用函数每次都重新声明一个对象,这样每个组件的data都有自己的引用,就不会出现相互污染的情况了。

    7.组件通信方式

    1. props和$on$emit

    适合父子组件的通信,通过props传递响应式数据,父组件通过$on监听事件、子组件通过$emit发送事件。

    on和emit是在组件实例初始化的时候通过initEvents初始化事件,在组件实例vm._events赋值一个空的事件对象,通过这个对象实现事件的发布订阅。下面是事件注册的几个关键函数:

    // 组件初始化event对象,收集要监听的事件和对应的回调函数
    function initEvents (vm: Component) {
      vm._events = Object.create(null)
      vm._hasHookEvent = false
      // init parent attached events
      const listeners = vm.$options._parentListeners
      if (listeners) {
        updateComponentListeners(vm, listeners)
      }
    }
    ...
    // 注册组件监听的事件
    function updateComponentListeners (
      vm: Component,
      listeners: Object,
      oldListeners: ?Object
    ) {
      target = vm
      updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
      target = undefined
    }
    
    1. ref$parent$children,还有$root
    • ref: 在普通DOM元素上声明就是DOM元素的引用,组件就是指向组件实例。
    • $parent:访问组件的父组件实例
    • $children:访问所有的子组件集合(数组)
    • $root: 指向root实例
    1. Event Bus

    通常是创建一个空的Vue实例作为事件总线(事件中心),实现任何组件在这个实例上的事件触发与监听。原理就是一个发布订阅的模式,跟$on``$emit一样,在实例化一个组件的事件通过initEvents初始化一个空的event对象,再通过实例化后的这个bus(vue实例)手动的$on$emit添加监听和触发的事件,代码在src/core/instance/events:

    Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this
        // 传入的事件如果是数组,就循环监听每个事件
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn)
          }
        } else {
        // 如果已经有这个事件,就push新的回调函数进去,没有则先赋值空数组再push
          (vm._events[event] || (vm._events[event] = [])).push(fn)
          // instead of a hash lookup
          if (hookRE.test(event)) {
            vm._hasHookEvent = true
          }
        }
        return vm
      }
      ...
      Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        ...
        let cbs = vm._events[event]
        // 循环调用要触发的事件的回调函数数组
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
          const info = `event handler for "${event}"`
          for (let i = 0, l = cbs.length; i < l; i++) {
            invokeWithErrorHandling(cbs[i], vm, args, vm, info)
          }
        }
        return vm
      }
    
    1. attrsattrs、attrs、listeners
    • $attrs: 包含了父作用域没被props声明绑定的数据,组件可以通过v-bind="$attrs"继续传给子组件
    • $listernes: 包含了父作用域中的v-on(不含 .native 修饰器的) 监听事件,可以通过v-on="$listeners"传入内部组件
    1. provide、inject

    父组件通过provide注入一个依赖,其所有的子孙组件可以通过inject来接收。要注意的是官网有这一段话:

    所以Vue不会对provide中的变量进行响应式处理。要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的。实际上在很多高级组件中都可以看到组件会将this通过provide传递给子孙组件,包括element-ui、ant-design-vue等。

    1. vuex 状态管理实现通信

    vuex是专为vue设计的状态管理模式。每个组件实例都有共同的store实例,并且store.state是响应式的,改变state唯一的办法就是通过在这个store实例上commit一个mutation,方便跟踪每一个状态的变化,实现原理在下面的vuex原理里有讲。

    8.computed、watch、method有什么区别

    computed:有缓存,有对应的watcher,watcher有个lazy为true的属性,表示只有在模板里去读取它的值后才会计算,并且这watcher在初始化的时候会赋值dirty为true,watcher只有dirty为true的时候才会重新求值,重新求值后会将dirty置为false,false会直接返回watcher的value,只有下次watcher的响应式依赖有更新的时候,会将watcher的dirty再置为false,这时候才会重新求值,这样就实现了computed的缓存。

    watch:watcher的对象每次更新都会执行函数。watch 更适用于数据变化时的异步操作。如果需要在某个数据变化时做一些事情,使用watch。

    method: 将方法在模板里使用,每次视图有更新都会重新执行函数,性能消耗较大。

    9.生命周期

    官网对生命周期的说明:

    生命周期就是每个Vue实例完成初始化、运行、销毁的一系列动作的钩子。

    基本上可以说8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后。

    • 创建前/后: 在 beforeCreate 阶段,vue 实例的挂载元素 el 还没有。
    • 载入前/后:在 beforeMount 阶段,vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。在 mounted 阶段,vue 实例挂载完成,data.message 成功渲染。
    • 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法。
    • 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在

    结合源码再理解,在源码中生命周期钩子是用callHook函数调用的。看下callHook函数:

    function callHook (vm: Component, hook: string) {
      pushTarget()
      const handlers = vm.$options[hook]
      const info = `${hook} hook`
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
          invokeWithErrorHandling(handlers[i], vm, null, vm, info)
        }
      }
      if (vm._hasHookEvent) {
        vm.$emit('hook:' + hook)
      }
      popTarget()
    }
    

    接收一个vm组件实例的参数和hook,取组件实例的$options传入的hook属性值,有的话会循环调用这个钩子的回调函数。在调用生命钩子的回调函数之前会临时pushTarget一个null值,也就是将Dep.target置为空来禁止在执行生命钩子的时候进行依赖收集。

    vm.$emit('hook:' + hook)则是用来给父组件监听该组件的回调事件。

    接下来看每个生命钩子具体调用的时机。

    1. beforeCreate、created:

    Vue.prototype._init = function (options?: Object) {
        ...
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
        ...
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
     }
    

    在执行beforeCreate之前调用了 initLifecycle、initEvents、initRender函数,所以beforeCreate是在初始化生命周期、事件、渲染函数之后的生命周期。

    在执行created之前调用了initInjections、initState、initProvide,这时候created初始化了data、props、watcher、provide、inject等,所以这时候就可以访问到data、props等属性。

    2. beforeMount、mounted

    在上面的代码片段可以看到created之后会进行DOM的挂载,执行的函数是vm.mount(vm.mount(vm.mount(vm.options.el),接下来分析下$mount方法。

    vm.mount就是Vue.prototype.mount就是Vue.prototype.mount就是Vue.prototype.mount原型方法继承而来的。这个方法在src/platforms/web/entry-runtime-with-compiler.js下声明的,主要进行模板的解析,优先判断是否有render函数这个属性,没有再进行tamplare模板解析,最终都是用render函数进行渲染。

    在解析完render函数后会调用callHook(vm, 'beforeMount'),而后执行vm._render(),再callHook(vm, 'mounted')方法,这时候标记着el 被新创建的 vm.$el 替换,并被挂载到实例上

    3. beforeUpdate、updated

    这两个钩子函数是在数据更新的时候进行回调的函数。在src/core/instance/lifecycle.js找到beforeUpdate调用的代码:

    ...
    new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
    ...
    

    _isMounted为ture的话(DOM已经被挂载)会调用callHook(vm, 'beforeUpdate')方法,然后会对虚拟DOM进行重新渲染。然后在/src/core/observer/scheduler.js下的flushSchedulerQueue()函数中渲染DOM,flushSchedulerQueue会刷新watcher队列并执行,执行完所有watcher的run方法之后(run方法就是watcher进行dom diff并更新DOM的方法),再调用callHook(vm, 'updated'),代码如下:

    /**
     * Flush both queues and run the watchers.
     */
    function flushSchedulerQueue () {
     ...
     
     for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          watcher.before()
        }
        watcher.run()
      }
      ...
      callUpdatedHooks(updatedQueue)
     ...
    }
    
    function callUpdatedHooks (queue) {
      let i = queue.length
      while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'updated')
        }
      }
    }
    

    4. beforeDestroy、destroyed

    这两个钩子是vue实例销毁的钩子,定义在Vue.prototype.$destroy中:

    Vue.prototype.$destroy = function () {
        const vm: Component = this
        if (vm._isBeingDestroyed) {
          return
        }
        callHook(vm, 'beforeDestroy')
        vm._isBeingDestroyed = true
        // remove self from parent
        const parent = vm.$parent
        if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
          remove(parent.$children, vm)
        }
        // teardown watchers
        if (vm._watcher) {
          vm._watcher.teardown()
        }
        let i = vm._watchers.length
        while (i--) {
          vm._watchers[i].teardown()
        }
        // remove reference from data ob
        // frozen object may not have observer.
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        // call the last hook...
        vm._isDestroyed = true
        // invoke destroy hooks on current rendered tree
        vm.__patch__(vm._vnode, null)
        // fire destroyed hook
        callHook(vm, 'destroyed')
        // turn off all instance listeners.
        vm.$off()
        // remove __vue__ reference
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }
    }
    

    在销毁之前执行callHook(vm, 'beforeDestroy'),然后销毁的时候做了几件事:

    • 如果有父元素,将父元素的$children中把该组件实例移除。
    • 移除watchers,并在依赖订阅者中移除自己。
    • 删除数据引用

    5. activated、deactivated

    剩下的还有activated、deactivated、errorCaptured三个钩子函数。

    activated、deactivated这两个钩子函数分别是在keep-alive 组件激活和停用之后的回调。

    errorCaptured捕获到当子孙组件错误时会被调用,在源码中可以经常看到try catch中catch会调用handleError函数,handleError会向组件所有的父级组件抛出异常,

    function handleError (err: Error, vm: any, info: string) {
      pushTarget()
      try {
        if (vm) {
          let cur = vm
          while ((cur = cur.$parent)) {
            const hooks = cur.$options.errorCaptured
            if (hooks) {
              for (let i = 0; i < hooks.length; i++) {
                try {
                  const capture = hooks[i].call(cur, err, vm, info) === false
                  if (capture) return
                } catch (e) {
                  globalHandleError(e, cur, 'errorCaptured hook')
                }
              }
            }
          }
        }
        globalHandleError(err, vm, info)
      } finally {
        popTarget()
      }
    }
    

    分析完源码再一下官网图示,会更清楚:

    最新的vue面试题大全含源码级回答(vue2篇)

    10.keep-aliva原理

    keep-alive是Vue.js的一个内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

    include与exclude两个属性,允许组件有条件地进行缓存,max属性确定最多缓存多少组件实例。

    keep-alive是一个组件,跟其他组件一样有生命周期和render函数,keep-alive包裹的分析keep-alive就是分析一个组件。

    源码再src/core/components/keep-alive,created声明了要缓存的组件对象,和存储的组件keys,keep-alive销毁的时候会用pruneCacheEntry将缓存的所有组件实例销毁,也就是调用组件实例的destroy方法。在挂载完成后监听include和exclude,动态地销毁已经不满足include的组件和满足exclude的组件实例:

    created () {
        this.cache = Object.create(null) // 存储需要缓存的组件
        this.keys = [] // 存储每个需要缓存的组件的key,即对应this.cache对象中的键值
    },
    
    // 销毁keep-alive组件的时候,对缓存中的每个组件执行销毁
    destroyed () {
        for (const key in this.cache) {
          pruneCacheEntry(this.cache, key, this.keys)
        }
    },
    mounted () {
        this.$watch('include', val => {
          pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
          pruneCache(this, name => !matches(val, name))
        })
    },
    

    接下来是render函数:

    render () {
        const slot = this.$slots.default
        const vnode: VNode = getFirstComponentChild(slot)
        // 如果vnode存在就取vnode的选项
        const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
        if (componentOptions) {
          // check pattern
          //获取第一个有效组件的name
          const name: ?string = getComponentName(componentOptions)
          const { include, exclude } = this
          if (
            // not included
            (include && (!name || !matches(include, name))) ||
            // excluded
            (exclude && name && matches(exclude, name))
          ) {
            return vnode// 说明不用缓存,直接返回这个组件进行渲染
          }
    
          // 匹配到了,开始缓存操作
          const { cache, keys } = this // keep-alive组件的缓存组件和缓存组件对应的key
          // 获取第一个有效组件的key
          const key: ?string = vnode.key == null
            // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
          if (cache[key]) {
            // 这个组件的实例用缓存中的组件实例替换
            vnode.componentInstance = cache[key].componentInstance
            // make current key freshest
            // 更新当前key在keys中的位置
            remove(keys, key)
            keys.push(key)
          } else {
            cache[key] = vnode
            keys.push(key)
            // prune oldest entry
            // 如果缓存中的组件个数超过传入的max,销毁缓存中的LRU组件
            // LRU: least recently used 最近最少用,缓存淘汰策略
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(cache, keys[0], keys, this._vnode)
            }
          }
    
          vnode.data.keepAlive = true
        }
        // 若第一个有效的组件存在,但其componentOptions不存在,就返回这个组件进行渲染
        // 或若也不存在有效的第一个组件,但keep-alive组件的默认插槽存在,就返回默认插槽的第一个组件进行渲染
        return vnode || (slot && slot[0])
    }
    

    代码做了详细的注释,这里再分析下render做了什么。

    通过this.$slots.default拿到插槽组件,也就是keep-alive包裹的组件,getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode(vnode节点描述对象,vue通过vnode创建真实的DOM)

    匹配到了就开始缓存,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。并且通过remove(keys, key),将当前的key从keys中删除再重新keys.push(key),这样就改变了当前key在keys中的位置。这个是为了实现max的功能,并且遵循缓存淘汰策略。

    如果没匹配到,说明没缓存过,这时候需要进行缓存,并且判断当前缓存的个数是否超过max指定的个数,如果超过,则销毁keys里的最后一个组件,并从keys中移除,这个就是LRU(Least Recently Used :最近最少使用 )缓存淘汰算法。

    最后返回vnode或者默认插槽的第一个组件进行DOM渲染。

    12.虚拟dom和diff算法

    虚拟DOM是对DOM的描述,用对象属性来描述节点,本质上是JavaScript对象。它有几个意义:

    1. 具备跨平台的优势

    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器、小程序、Node、原生应用、服务端渲染等等。

    1. 提升渲染性能

    频繁变动DOM会造成浏览器的回流或者重回,而通过将大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,可以减少真实DOM的操作次数,从而提高性能。

    1. 代码可维护性更高

    通过虚拟 DOM 的抽象能力,可以用声明式写 UI 的方式,大大提高了我们的工作效率。

    在vue中template最终会转成render函数,而render函数最终是执行的createElement,生成vnode,vnode正是 vue中用来表示虚拟DOM的类,看下vnode:

    class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array<VNode>;
      text: string | void;
      elm: Node | void;
      ns: string | void;
      context: Component | void; // rendered in this component's scope
      key: string | number | void;
      componentOptions: VNodeComponentOptions | void;
      componentInstance: Component | void; // component instance
      parent: VNode | void; // component placeholder node
    
      // strictly internal
      raw: boolean; // contains raw HTML? (server only)
      isStatic: boolean; // hoisted static node
      isRootInsert: boolean; // necessary for enter transition check
      isComment: boolean; // empty comment placeholder?
      isCloned: boolean; // is a cloned node?
      isOnce: boolean; // is a v-once node?
      asyncFactory: Function | void; // async component factory function
      asyncMeta: Object | void;
      isAsyncPlaceholder: boolean;
      ssrContext: Object | void;
      fnContext: Component | void; // real context vm for functional nodes
      fnOptions: ?ComponentOptions; // for SSR caching
      devtoolsMeta: ?Object; // used to store functional render context for devtools
      fnScopeId: ?string; // functional scope id support
    
      constructor (
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) {
        this.tag = tag
        this.data = data
        this.children = children
        this.text = text
        this.elm = elm
        this.ns = undefined
        this.context = context
        this.fnContext = undefined
        this.fnOptions = undefined
        this.fnScopeId = undefined
        this.key = data && data.key
        this.componentOptions = componentOptions
        this.componentInstance = undefined
        this.parent = undefined
        this.raw = false
        this.isStatic = false
        this.isRootInsert = true
        this.isComment = false
        this.isCloned = false
        this.isOnce = false
        this.asyncFactory = asyncFactory
        this.asyncMeta = undefined
        this.isAsyncPlaceholder = false
      }
    
      // DEPRECATED: alias for componentInstance for backwards compat.
      /* istanbul ignore next */
      get child (): Component | void {
        return this.componentInstance
      }
    }
    

    看下其中关键的几个属性:

    • tag: 当前节点的标签名

    • data: 表示节点上的class,attribute,style以及绑定的事件

    • children: 当前节点的子节点,是一个数组

    • text: 当前节点的文本

    • elm: 当前虚拟节点对应的真实dom节点

    • key: 节点的key属性,被当作节点的标志,用以优化

    • componentOptions: 组件的option选项

    • componentInstance: 当前节点对应的组件的实例

    • parent: 当前节点的父节点

    • isStatic: 是否为静态节点

    children和parent是指当前的vnode的子节点和父节点,这样一个个vnode就形成了DOM树。

    diff算法发生在视图更新的时候,也就是数据更新的时候,diff算法会将新旧虚拟DOM作对比,将变化的地方转换为DOM

    当某个数据被修改的时候,依赖对应的watcher会通知更新,执行渲染函数会生成新的vnode,vnode再去与旧的vnode进行对比更新,这就是vue中的虚拟dom diff算法触发的流程。

    看下组件更新的_update方法:

    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        const restoreActiveInstance = setActiveInstance(vm)
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
      }
      ...
    

    vm.$el = vm._patch(),这个就是最终渲染的DOM元素,patch就是vue中diff算法的函数,在key的作用章节有提过。patch将新旧虚拟DOM节点比较后,最终返回真实的DOM节点。

    patch

    看下patch代码(部分):

    function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
        /*vnode不存在则直接调用销毁钩子*/
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        let isInitialPatch = false
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
          // empty mount (likely as component), create new root element
          isInitialPatch = true
          createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
          /*标记旧的VNode是否有nodeType*/
          /*Github:https://github.com/answershuto*/
          const isRealElement = isDef(oldVnode.nodeType)
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node
            /*是同一个节点的时候直接修改现有的节点*/
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
       ...
       return vnode.elm
    

    首先是判断是否有新的vnode,没有代表是要销毁旧的vnode,调用销毁组件的钩子。

    然后判断是否有旧的vnode,没有代表是新增,也就是新建root节点。

    接下来判断旧的vnode是否是真实的元素,而不是组件,如果是组件并且用someVnode判断新旧节点是否是相同的节点(sameVnode在key的作用章节有做解析),是进行patchVnode,这时候进行真正的新老节点的diff。只有相同的节点才会进行diff算法!!!

    patchVnode

    function patchVnode (
        oldVnode,
        vnode,
        insertedVnodeQueue,
        ownerArray,
        index,
        removeOnly
      ) {
        // 两个vnode相同,说明不需要diff,直接返回
        if (oldVnode === vnode) {
          return
        }
    
        // 如果传入了ownerArray和index,可以进行重用vnode,updateChildren里用来替换位置
        if (isDef(vnode.elm) && isDef(ownerArray)) {
          // clone reused vnode
          vnode = ownerArray[index] = cloneVNode(vnode)
        }
    
        const elm = vnode.elm = oldVnode.elm
    
        // 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return
        if (isTrue(oldVnode.isAsyncPlaceholder)) {
          if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
        /*
          如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
          并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
          那么只需要替换elm以及componentInstance即可。
        */
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
    
        let i
        const data = vnode.data
        if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
          i(oldVnode, vnode)
        }
    
        const oldCh = oldVnode.children
        const ch = vnode.children
        if (isDef(data) && isPatchable(vnode)) {
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
        }
        /*如果这个VNode节点没有text文本时*/
        if (isUndef(vnode.text)) {
          if (isDef(oldCh) && isDef(ch)) {
          // 两个vnode都定义了子节点,并且不相同,就对子节点进行diff
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
          } else if (isDef(ch)) {
          // 如果只有新的vnode定义了子节点,则进行添加子节点的操作
            if (process.env.NODE_ENV !== 'production') {
              checkDuplicateKeys(ch)
            }
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
          // 如果只有旧的vnode定义了子节点,则进行删除子节点的操作
            removeVnodes(oldCh, 0, oldCh.length - 1)
          } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '')
          }
        } else if (oldVnode.text !== vnode.text) {
          nodeOps.setTextContent(elm, vnode.text)
        }
        if (isDef(data)) {
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }
    

    通过代码可知,patchVnode分为多种情况,分析下子节点的diff过程 (oldCh 为 oldVnode的子节点,ch 为 Vnode的子节点)

    1. oldCh、ch都定义了调用updateChildren再进行diff
    2. 若 oldCh不存在,ch 存在,首先清空 oldVnode 的文本节点,同时调用 addVnodes 方法将 ch 添加到elm真实 dom 节点当中
    3. 若 oldCh存在,ch不存在,则删除 elm 真实节点下的 oldCh 子节点
    4. 若 oldVnode 有文本节点,而 vnode 没有,那么就清空这个文本节点

    updateChildren是子节点diff的函数,也是最重要的环节。

    updateChildren

    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        // 声明oldCh和newCh的头尾索引和头尾的vnode,
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
        
        const canMove = !removeOnly
    
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(newCh)
        }
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
            // 判断两边的头是不是相同节点
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
            // 判断尾部是不是相同节点
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
            // 判断旧节点头部是不是与新节点的尾部相同,相同则把头部往右移
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
            // 判断旧节点尾部是不是与新节点的头部相同,相同则把头部往左移
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
           /*
              生成一个key与旧VNode的key对应的哈希表
            */
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
        // oldCh或者newCh遍历完,说明剩下的节点不是新增就是删除
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }
    

    首先给startIndex和endIndex来作为遍历的索引,在遍历的时候会先判断头尾节点是否相同,没有找到相同节点后再按照通用方式遍历查找;查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点。

    当oldCh 或者 newCh 遍历完后(遍历完的条件就是 oldCh 或者 newCh 的 startIndex >= endIndex ),说明剩下的节点为新增或者删除,这时候停止oldCh 和 newCh 的 diff。

    13.Vuex原理

    vuex是什么,先看下官方的原话:

    这段话可以得出几个结论:Vuex是为vue.js服务的,而像redux与react是解耦的,然后vuex是状态管理模式,所有的状态以一种可预测的方式发生变化。

    设计思想:

    Vuex的设计思想,借鉴了Flux、Redux,将数据存放到全局的store,再将store挂载到每个vue实例组件中,利用Vue.js的细粒度数据响应机制来进行高效的状态更新。

    原理可以从使用方式开始分析。

    Vue.use(Vuex); // 1. vue的插件机制,安装vuex
    let store = new Vuex.Store({ // 2.实例化store,调用install方法
     	state,
     	getters,
     	modules,
     	mutations,
     	actions,
     	plugins
    });
    new Vue({ // 3.注入store, 挂载vue实例
    	store,
    	render: h=>h(app)
    }).$mount('#app');
    

    Vue.use是vue中的插件机制,内部会调用插件的install方法,vuex的install方法:

    export function install (_Vue) {
      if (Vue) {
        if (process.env.NODE_ENV !== 'production') {
          console.error(
            '[vuex] already installed. Vue.use(Vuex) should be called only once.'
          )
        }
        return
      }
      /*保存Vue,同时用于检测是否重复安装*/
      Vue = _Vue
      /*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
      applyMixin(Vue)
    }
    

    vuex是个全局的状态管理,全局有且只能有一个store实例,所以在install的时候会判断是否已经安装过了,这个就是单例模式,确保一个类只有一个实例。在第一次install的时候会applyMixin,applyMixin是/src/mixin导入的方法:

    function (Vue) {
      const version = Number(Vue.version.split('.')[0])
    
      if (version >= 2) {
        Vue.mixin({ beforeCreate: vuexInit })
      } else {
        // override init and inject vuex init procedure
        // for 1.x backwards compatibility.
        const _init = Vue.prototype._init
        Vue.prototype._init = function (options = {}) {
          options.init = options.init
            ? [vuexInit].concat(options.init)
            : vuexInit
          _init.call(this, options)
        }
      }
    
      /**
       * Vuex init hook, injected into each instances init hooks list.
       */
    
      function vuexInit () {
        const options = this.$options
        // store injection
        if (options.store) {
          this.$store = typeof options.store === 'function'
            ? options.store()
            : options.store
        } else if (options.parent && options.parent.$store) {
          this.$store = options.parent.$store
        }
      }
    }
    

    先是判断下vue的版本,这边分析vue2的逻辑。利用Vue.mixin混入的机制,在组件实例的beforeCreate调用vuexInit方法,首先判断options是否有store,没有代表是root节点,这时候要进行store初始化,没有的话就取父组件的$store赋值,这样就实现了全局共用唯一的store实例。

    store实现的源码在src/store.js,其中最核心的是响应式的实现,通过resetStoreVM(this, state)调用,看下这个方法:

    function resetStoreVM (store, state, hot) {
      const oldVm = store._vm
    
      // bind store public getters
      store.getters = {}
      // reset local getters cache
      store._makeLocalGettersCache = Object.create(null)
      const wrappedGetters = store._wrappedGetters
      const computed = {}
      forEachValue(wrappedGetters, (fn, key) => {
        // use computed to leverage its lazy-caching mechanism
        // direct inline function use will lead to closure preserving oldVm.
        // using partial to return function with only arguments preserved in closure environment.
        computed[key] = partial(fn, store)
        Object.defineProperty(store.getters, key, {
          get: () => store._vm[key],
          enumerable: true // for local getters
        })
      })
    
      // use a Vue instance to store the state tree
      // suppress warnings just in case the user has added
      // some funky global mixins
      const silent = Vue.config.silent
      Vue.config.silent = true
      store._vm = new Vue({
        data: {
          $$state: state
        },
        computed
      })
      Vue.config.silent = silent
    
      // enable strict mode for new vm
      if (store.strict) {
        enableStrictMode(store)
      }
    
      if (oldVm) {
        if (hot) {
          // dispatch changes in all subscribed watchers
          // to force getter re-evaluation for hot reloading.
          store._withCommit(() => {
            oldVm._data.$$state = null
          })
        }
        Vue.nextTick(() => oldVm.$destroy())
      }
    }
    

    resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法对store.getters的每一个getter定义get方法,这样访问this.$store.getter.test就等同于访问store._vm.test。

    state是通过new一个Vue对象来实现数据的“响应式化”,运用Vue的data属性来实现数据与视图的同步更新,computed实现getters的计算属性。最终访问store.state也就是访问store._vm.state。

    结束语

    说的不对欢迎指出来,感谢指教~


    起源地下载网 » 最新的vue面试题大全含源码级回答(vue2篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元