玩转vue面试题系列3 结合源码分析面试题

377 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

上文: 玩转Vue面试题系列结合源码分析(2)

  1. Vue.mixin的使用场景和原理
  2. Vue组件data为什么必须是个函数?
  3. nextTick在哪里使用?使用原理?
  4. computedwatch的区别
  5. Vue.set方法是如何实现的
  6. Vue为什么需要虚拟dom
  7. Vuediff算法原理
  8. 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟dom进行diff检测差异
  9. 请说明Vue中key的作用和其原理,谈谈你对它的理解

7. Vue.mixin的使用场景和原理

mixin我们用的比较多的就是混入生命周期。

通过Vue.mixin来实现逻辑的复用:

Vue.mixin({
    beforeCreate(){
        // 每个vue组件实例都有该属性了
        this.$store = new Store()
        // 还可以扩展公共逻辑 比如每个组件销毁时都需要做的某些事情...
    }
})

虽然mixin可以实现逻辑的复用,数据的混入复用等等。但是问题在于数据来源不明确。我们在组件中使用某个数据或者某个方法,我们有时候是不知道这个方法或者数据是来自哪里的?父组件传值?还是说provide?...会造成混乱。有时候难以定位错误。

Vue.mixin({
    data(){
        return {
            name:"张三"
        }
    }
})
Vue.component("my-btn",{
    template:`<div>{{name}}</div>`
})

而且对于多个数据,如果组件自己内部定义了和mixin混入的同名属性,可能会导致命名冲突问题。(虽然在组件实例化的过程,mixin的选项和组件自身的选项合并时(mergeOptions方法中)是以用户选项为主)

image-20220421205535716

image-20220421205648412

mixin的核心就是合并属性,内部采用了策略模式进行合并。使用方式就是全局和局部的mixin。

当然针对不同的属性有不同的合并策略。此外:出现命名冲突也是不会报错和提示的

// mixin方法 Vue.mixin
export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 谁调用 this就是谁 最终会将 mixin选项和Vue.options合并在一起
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

image-20220421211536548

8. Vue组件data为什么必须是个函数?

在声明一个组件的时候,其实你data属性不是一个函数,也能正常运行,只是控制台会提醒你,让你把data属性给成函数。

image-20220421213434183

我们知道,我们使用Vue.component定义一个组件的时候,内部实际调用的是Vue.extend方法(我在mini-vue中写过的)。

传送门:mini-vue

**原因:**针对根实例来说(new Vue),一般我们一个项目都是一个根,所以data可以是对象。但是对于组件来说,组件是通过同一个构造函数多次创建实例,如果是同一个对象,所以实例共享一份data,实例的data之间会相互影响。每个组件的数据源应该都是独立的。那就每次都调用data,返回一个独立的数据。

image-20220421222506169

image-20220421222547836

在组件实例化的时候:我们会根据data是否是函数,来进行data函数的执行的。(_init -> initState -> initData)

image-20220421222744642

9. nextTick在哪里使用?使用原理?

nextTick内部采用了异步任务进行了包装(多个nextTick调用,会被合并成一次,内部会合并回调),最后在异步任务中批处理。

主要应用场景就是异步更新(默认调度的时候,就会添加一个nextTick任务),用户为了获取最终的渲染结果,需要在内部任务执行完成以后去执行用户逻辑。这时候用户需要把对应的逻辑放到nextTick中。

image-20220422102017271

10. computedwatch的区别

computed和watch的相同点:

  • 底层都是创建了watcher(computed定义的属性可以在模板中使用,但是watch不能在视图中使用)

  • computed默认不会立即执行,只有取值的时候才会执行。内部会维护一个dirty属性,来控制依赖的值是否发生改变。(默认情况下,计算属性需要同步返回结果,有个包可以把computed变成异步的)

    image-20220422115230809

  • watch默认用户会提供一个回调函数,数据变化了就调用这个回调。我们可以监控某个数据的变化,数据变化了就执行某些操作

image-20220422113320892

image-20220422114228105

11. Vue.set方法是如何实现的

Vue只会在定义在data中的数据进行劫持。对于Vue.set方法,我们可以认为这是vue的补丁方法(在创建好实例以后,通过实例进行属性的添加,vue是不会劫持的,不会触发更新视图的操作)

而且,我们数组也无法监控索引和长度,所以我们就想到一个方法,手动触发更新。

如何实现的?

我们给每一个对象都增添一个dep属性(一个属性对应一个dep),在给对象新增属性,或者修改数组索引对应的元素时,手动触发更新。

const vm =  new Vue({
    data(){
        return {
            firend:{
                name:"张三"
            }
        }
    }
})
vm.firend.age = 22
vm.firend.__ob__.dep.notify() // 手动通知更新

set方法的实现原理就是如此。

image-20220422155459024

export function set (target: Array<any> | Object, key: any, val: any): any {
  // 给数组的指定索引位置元素更改
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 巧妙的把修改值 转为删除值后在新增值
    // vm.$set(vm.movies, 1, "ada") -> vm.splice(1, 1, "aaa")
    target.splice(key, 1, val)
    return val
  }
  // 不是新增属性 修改已有的属性直接修改即可,因为是响应式了
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 取出对象上的observer对象
  const ob = (target: any).__ob__
  // 如果修改的是vue实例 不支持这样做 性能也比较差
  // vm.$set(vm.$data, "abc", "acbc")  vm.$set(vm, "abc", "acbc")
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 不是响应式对象就直接添加
  if (!ob) {
    target[key] = val
    return val
  }
  // 定义响应式
  defineReactive(ob.value, key, val)
  // 更新watcher
  ob.dep.notify()
  return val
}

12. Vue为什么需要虚拟dom

虚拟dom的好处是什么?

  • 我们写的代码可能要针对不同的平台来使用(weex,web,小程序),虚拟dom的最大好处就是跨平台,不需要考虑跨平台问题。

  • 不用关心兼容性问题,可以在上层对应的渲染方法传进来,再通过虚拟dom进行渲染即可。

  • 针对更新的时候,用到了diff算法,有了虚拟dom之后,我们就可以通过diff算法来找到最后的差异进行修改真实dom。

13. Vuediff算法原理

diff算法的特点就是平级比较,内部采用了双指针方式进行了优化,优化了常见操作。

采用了递归比较的方式。

针对一个节点的diff算法

  • 先拿出更节点进行比较,如果是同一个节点,则比较熟悉,如果不是同一个节点则直接换成最新的即可。
  • 同一个节点比较熟悉后,复用老节点

比较子节点

  • 一方有儿子,一方没儿子。(无非就是移出节点,新增节点)
  • 两方都有儿子时,一层层比较
    • 优化比较的方式:
      1. 先比较两方的头结点,不相同
      2. 在比较两方的尾节点,不相同
      3. 开始进行交叉比较,一方的头和另一方的尾进行比较(有两次比较)
      4. 在上面比较都失败的情况下,就是乱序比较了
    • 对于乱序比较,就是维护了一个老虚拟节点子节点的映射表,用新的节点去映射表中去查找此元素是否存在,存在就进行移动,不存在就插入新的节点,最后删除多余的老节点

**缺点:**比较时可能出现多出无谓的移动节点情况。

image-20220422162934031

vue在进行diff比较的时候,发现AB节点是需要删除的节点,CD节点命中,可以复用,所以会把CD节点移动到头指针的最前方,然后把FG节点插入,E节点也删除。很明显,CD节点是不需要移动的,我们只是需要把ABE节点删除,然后把FG节点插入到CD节点之间即可。因为CD节点的相对顺序没有发生改变。

image-20220422163153878

image-20220422191442012

image-20220422191407849

image-20220422191637952

image-20220422191642145

image-20220422191645840

image-20220422191721024

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

我们根据响应式,的确是可以知道哪里出现了更新。如果让一个变量,一个属性就对应一个watcher,粒度太小,watcher这玩意是比较消耗内存的。

  • 如果给每个属性,都去增加watcher,当然也是可以实现响应式更新视图。但是可能在变量age发生改变的时候,触发了name的改变,那么视图又会更新。粒度太小导致也不好控制。
  • 降低watcher的数量,让每个组件有一个watcher,某个属性变化了,我们会把整个组件都更新了,那么这个组件内即使依赖了其他的变量,也会在视图上更新最新值。
  • 如果不用diff算法,我们需要对每个标签,属性等都比较一次,太暴力,反而性能更低。出现虚拟dom就是为了提高性能的。
  • 通过diff算法和响应式原理折中处理了一下。
  • 在vue1.x中,就是给每个属性都增加了一个watcher,导致的情况就是页面一大了就容易卡,刷新很慢等情况。

15. 请说明Vue中key的作用和其原理,谈谈你对它的理解

isSameVNode方法中,会根据key来判断两个元素是否是同一个元素,key不相同说明不是同一个元素(key在动态列表中,不要使用index)。

我们使用key,要尽量保证key的唯一性。这样可以优化diff算法。