vue2 源码面试题

200 阅读22分钟

Vue 的初始化过程(new Vue(options))都做了什么?

  • 处理组件配置项

    • 初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上
    • 初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
  • 初始化组件实例的关系属性,比如 parentparent、children、rootroot、refs 等

  • 处理自定义事件

    • 子组件的事件 其实 是 谁调用 谁监听 4
    • 是子组件监听 而不是父组件监听
  • 处理 插槽相关的属性

  • 通过 callhook 调用 beforeCreate 钩子函数

  • 初始化组件的 inject 配置项,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行浅层的响应式处理(只处理了对象第一层数据),并代理每个 key 到 vm 实例上

    • 这里其实并不是注入 而是 子组件一层一层的向上查找 相同的 key
  • 数据响应式,处理 props、methods、data、computed、watch 等选项

  • 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上

  • 通过 callhook 调用 created 钩子函数

  • 如果发现配置项上有 el 选项,则自动调用 mount方法,也就是说有了el选项,就不需要再手动调用mount 方法,也就是说有了 el 选项,就不需要再手动调用 mount 方法,反之,没提供 el 选项则必须调用 $mount

  • 接下来则进入挂载阶段

源码截图

路径 : src/core/instance/init.js

代码行数: 15

beforeCreate 钩子函数 为什么拿不到数据

  1. 在 vue 初始化过程中 beforeCreate 之前没有初始化 数据
  2. 只初始化了 组件的配置项 和 组件的实例关系 属性 parentparent、children、rootroot、refs 等
  3. 处理了 自定义事件 然后就 通过 callhook 调用了 beforeCreate 钩子函数

为什么 created 钩子函数可以拿到数据

  1. 在 beforeCreate 钩子函数之后 执行了 初始化组件的 inject 配置型
  2. 数据的响应式 props methods data computed watch 等
  3. 解析组件的配置 provide 对象 挂载在 vm 上面
  4. 然后 通过 callhook 调用了 created 钩子函数 所以可以拿到

如果配置项 存在 el render template 的优先级

  1. render 的优先级 是最高
  2. el 最低
  3. render > template > el

源码截图

路径 : src/platforms/web/entry-runtime-with-compiler.js

代码行数: 18

Vue 响应式原理是怎么实现的?

  • 响应式的核心是通过 Object.defineProperty 拦截对数据的访问和设置
  • 响应式的数据分为两类:
  • 对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
    • 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
    • 设置数据时由 dep 通知相关的 watcher 去更新
  • 数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
    • 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新 内部是通过 splice
    • 删除数据时,也要由 dep 通知 watcher 去更新 内部是通过 splice

computed 和 methods 有什么区别

computed

  1. 是一个 计算属性 会缓存执行的结果
  2. 性能更好 因为有缓存
  3. 内部 利用了 watcher 来实现 利用 watcher.dirty 来进行判断
  4. 一次渲染中只会 执行一次 computed 后续的访问 不会执行 直接走缓存
  5. 只到 下次的更新 才会再次执行 每一次 执行 compuped 会把 watcher.dirty 设置为 false
  6. 只有下次 update 更新了 才会 重新把 watcher.dirty 设置为 true
  7. update 只有响应式数据更新的时候才会触发

methods

  1. 是一个方法 不会缓存

computed 和 watch 有什么区别

computed

  1. 底层也是利用 watcher 来监听的
  2. 默认懒执行 不可更改
  3. 默认做一些同步操作 因为 如果在 computed 函数前面 加了 async 就会默认返回 一个promise 使用方式错误

watch

  1. 本质底层都是 利用 watcher 来实现的
  2. 可配置 是否懒执行
  3. 一般用来做一些异步操作

Vue 的异步更新机制是如何实现的?

  1. Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之
  2. 当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法
  3. watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。
  4. 然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。
  5. 如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。
  6. 如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。
  7. flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。
  8. flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。

Vue 的 nextTick API 是如何实现的?

Vue.nextTick 或者 vm.$nextTick 的原理其实很简单,就做了两件事:

  • 将传递的回调函数用 try catch 包裹然后放入 callbacks 数组
  • 执行 timerFunc 函数,在浏览器的异步任务队列放入一个刷新 callbacks 数组的函数

Vue.use(plugin) 做了什么?

  1. 负责安装 plugin 插件,其实就是执行插件提供的 install 方法。
  2. 首先判断该插件是否已经安装过
  3. 如果没有,则执行插件提供的 install 方法安装插件,具体做什么有插件自己决定
  4. 如果没有提供 install 方法 或者 install 方法不是 function 则会去判断 传入是不是一个函数 如果是函数就直接执行

Vue.mixin(options) 做了什么?

  1. 负责在 Vue 的全局配置上合并 options 配置。然后在每个组件生成 vnode 时会将全局配置合并到组件自身的配置上来。
  2. 标准化 options 对象上的 props、inject、directive 选项的格式
  3. 处理 options 上的 extends 和 mixins,分别将他们合并到全局配置上
  4. 然后将 options 配置和全局配置进行合并,选项冲突时 options 配置会覆盖全局配置

Vue.component(compName, Comp) 做了什么?

  1. 负责注册全局组件。其实就是将组件配置注册到全局配置的 components 选项上(options.components),然后各个子组件在生成 vnode 时会将全局的 components 选项合并到局部的 components 配置项上。
  2. 如果第二个参数为空,则表示获取 compName 的组件构造函数
  3. 如果 Comp 是组件配置对象,则使用 Vue.extend 方法得到组件构造函数,否则直接进行下一步
  4. 在全局配置上设置组件信息,this.options.components.compName = CompConstructor

Vue.directive('my-directive', {xx}) 做了什么?

  1. 在全局注册 my-directive 指令,然后每个子组件在生成 vnode 时会将全局的 directives 选项合并到局部的 directives 选项中。然后各个子组件在生成 vnode 时会将全局的 directive 选项合并到局部的 directive 配置项上。
  2. 如果第二个参数为空,则获取指定指令的配置对象
  3. 如果不为空,如果第二个参数是一个函数的话,则生成配置对象 { bind: 第二个参数, update: 第二个参数 }
  4. 然后将指令配置对象设置到全局配置上,this.options.directives['my-directive'] = {xx}

Vue.filter('my-filter', function(val) {xx}) 做了什么?

  1. 负责在全局注册过滤器 my-filter,然后每个子组件在生成 vnode 时会将全局的 filters 选项合并到局部的 filters 选项中。
  2. 如果没有提供第二个参数,则获取 my-filter 过滤器的回调函数
  3. 如果提供了第二个参数,则是设置 this.options.filters['my-filter'] = function(val) {xx}。

Vue.extend(options) 做了什么?

  1. Vue.extend 基于 Vue 创建一个子类,参数 options 会作为该子类的默认全局配置,就像 Vue 的默认全局配置一样。所以通过 Vue.extend 扩展一个子类,一大用处就是内置一些公共配置,供子类的子类使用。
  2. 定义子类构造函数,这里和 Vue 一样,也是调用 _init(options)
  3. 合并 Vue 的配置和 options,如果选项冲突,则 options 的选项会覆盖 Vue 的配置项
  4. 给子类定义全局 API,值为 Vue 的全局 API,比如 Sub.extend = Super.extend,这样子类同样可以扩展出其它子类
  5. 返回子类 Sub

Vue.set(target, key, val) 做了什么

  1. 由于 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi'),所以通过 Vue.set 为向响应式对象中添加一个 property,可以确保这个新 property 同样是响应式的,且触发视图更新。
  2. 更新数组指定下标的元素:Vue.set(array, idx, val),内部通过 splice 方法实现响应式更新
  3. 更新对象已有属性:Vue.set(obj, key ,val),直接更新即可 => obj[key] = val
  4. 不能向 Vue 实例或者 $data 动态添加根级别的响应式数据
  5. Vue.set(obj, key, val),如果 obj 不是响应式对象,会执行 obj[key] = val,但是不会做响应式处理
  6. Vue.set(obj, key, val),为响应式对象 obj 增加一个新的 key,则通过 defineReactive 方法设置响应式,并触发依赖更新

Vue.delete(target, key) 做了什么?

  1. 删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。当然同样不能删除根级别的响应式属性。
  2. Vue.delete(array, idx),删除指定下标的元素,内部是通过 splice 方法实现的
  3. 删除响应式对象上的某个属性:Vue.delete(obj, key),内部是执行 delete obj.key,然后执行依赖更新即可

Vue.nextTick(cb) 做了什么?

Vue.nextTick(cb) 方法的作用是延迟回调函数 cb 的执行,一般用于 this.key = newVal 更改数据后,想立即获取更改过后的 DOM 数据:

this.key = 'new val'

Vue.nextTick(function() {
  // DOM 更新了
})

其内部的执行过程是:

  • this.key = 'new val,触发依赖通知更新,将负责更新的 watcher 放入 watcher 队列
  • 将刷新 watcher 队列的函数放到 callbacks 数组中
  • 在浏览器的异步任务队列中放入一个刷新 callbacks 数组的函数
  • Vue.nextTick(cb) 来插队,将 cb 函数放入 callbacks 数组
  • 待将来的某个时刻执行刷新 callbacks 数组的函数
  • 然后执行 callbacks 数组中的众多函数,触发 watcher.run 的执行,更新 DOM
  • 由于 cb 函数是在后面放到 callbacks 数组,所以这就保证了先完成的 DOM 更新,再执行 cb 函数

vm.$set(obj, key, val) 做了什么?

  1. vm.set用于向响应式对象添加一个新的property,并确保这个新的property同样是响应式的,并触发视图更新。由于Vue无法探测对象新增属性或者通过索引为数组新增一个元素,比如:this.obj.newProperty=valthis.arr[3]=val。所以这才有了vm.set 用于向响应式对象添加一个新的 property,并确保这个新的 property 同样是响应式的,并触发视图更新。由于 Vue 无法探测对象新增属性或者通过索引为数组新增一个元素,比如:this.obj.newProperty = 'val'、this.arr[3] = 'val'。所以这才有了 vm.set,它是 Vue.set 的别名。
  2. 为对象添加一个新的响应式数据:调用 defineReactive 方法为对象增加响应式数据,然后执行 dep.notify 进行依赖通知,更新视图
  3. 为数组添加一个新的响应式数据:通过 splice 方法实现

vm.$watch(expOrFn, callback, [options]) 做了什么?

  1. vm.$watch 负责观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。当其发生变化时,回调函数就会被执行,并为回调函数传递两个参数,第一个为更新后的新值,第二个为老值。
  2. 这里需要 注意 一点的是:如果观察的是一个对象,比如:数组,当你用数组方法,比如 push 为数组新增一个元素时,回调函数被触发时传递的新值和老值相同,因为它们指向同一个引用,所以在观察一个对象并且在回调函数中有新老值是否相等的判断时需要注意。
  3. vm.$watch 的第一个参数只接收简单的响应式数据的键路径,对于更复杂的表达式建议使用函数作为第一个参数。

vm.$watch 的内部原理是:

  1. 设置 options.user = true,标志是一个用户 watcher
  2. 实例化一个 Watcher 实例,当检测到数据更新时,通过 watcher 去触发回调函数的执行,并传递新老值作为回调函数的参数
  3. 返回一个 unwatch 函数,用于取消观察

vm.$on(event, callback) 做了什么?

  1. 监听当前实例上的自定义事件,事件可由 vm.emit触发,回调函数会接收所有传入事件触发函数(vm.emit 触发,回调函数会接收所有传入事件触发函数(vm.emit)的额外参数。
  2. vm.$on 的原理很简单,就是处理传递的 event 和 callback 两个参数,将注册的事件和回调函数以键值对的形式存储到 vm._event 对象中,vm._events = { eventName: [cb1, cb2, ...], ... }。

vm.$emit(eventName, [...args]) 做了什么?

  1. 触发当前实例上的指定事件,附加参数都会传递给事件的回调函数。
  2. 其内部原理就是执行 vm._events[eventName] 中所有的回调函数。

备注:从 on和on 和 on和emit 的实现原理也能看出,组件的自定义事件其实是谁触发谁监听,所以在这会儿再回头看 Vue 源码解读(2)—— Vue 初始化过程 中关于 initEvent 的解释就会明白在说什么,因为组件自定义事件的处理内部用的就是 vm.on、vm.on、vm.on、vm.emit。

vm.$off([event, callback]) 做了什么?

  1. 移除自定义事件监听器,即移除 vm._events 对象上相关数据。
  2. 如果没有提供参数,则移除实例的所有事件监听
  3. 如果只提供了 event 参数,则移除实例上该事件的所有监听器
  4. 如果两个参数都提供了,则移除实例上该事件对应的监听器

vm.$once(event, callback) 做了什么?

  1. 监听一个自定义事件,但是该事件只会被触发一次。一旦触发以后监听器就会被移除。

  2. 其内部的实现原理是:

    1. 包装用户传递的回调函数,当包装函数执行的时候,除了会执行用户回调函数之外还会执行 vm.$off(event, 包装函数) 移除该事件
    2. 用 vm.$on(event, 包装函数) 注册事件

vm._update(vnode, hydrating) 做了什么?

  1. 官方文档没有说明该 API,这是一个用于源码内部的实例方法,负责更新页面,是页面渲染的入口,
  2. 其内部根据是否存在 prevVnode 来决定是首次渲染,还是页面更新,
  3. 从而在调用 patch 函数时传递不同的参数。该方法在业务开发中不会用到。

vm.$forceUpdate() 做了什么?

  1. 迫使 Vue 实例重新渲染,它仅仅影响组件实例本身和插入插槽内容的子组件,而不是所有子组件。
  2. 其内部原理到也简单,就是直接调用 vm._watcher.update(),它就是 watcher.update() 方法,执行该方法触发组件更新。

vm.$destroy() 做了什么?

  1. 负责完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令和事件监听器。在执行过程中会调用 beforeDestroy 和 destroy 两个钩子函数。在大多数业务开发场景下用不到该方法,一般都通过 v-if 指令来操作。
  2. 其内部原理是:
1.  调用 beforeDestroy 钩子函数
2.  将自己从老爹肚子里($parent)移除,从而销毁和老爹的关系
3.  通过 watcher.teardown() 来移除依赖监听
4.  通过 vm.__patch__(vnode, null) 方法来销毁节点
5.  调用 destroyed 钩子函数
6.  通过 vm.$off 方法移除所有的事件监听

vm.$nextTick(cb) 做了什么?

vm.$nextTick 是 Vue.nextTick 的别名,其作用是延迟回调函数 cb 的执行,一般用于 this.key = newVal 更改数据后,想立即获取更改过后的 DOM 数据:

其内部的执行过程是:

  1. this.key = 'new val',触发依赖通知更新,将负责更新的 watcher 放入 watcher 队列
  2. 将刷新 watcher 队列的函数放到 callbacks 数组中
  3. 在浏览器的异步任务队列中放入一个刷新 callbacks 数组的函数
  4. vm.$nextTick(cb) 来插队,直接将 cb 函数放入 callbacks 数组
  5. 待将来的某个时刻执行刷新 callbacks 数组的函数
  6. 然后执行 callbacks 数组中的众多函数,触发 watcher.run 的执行,更新 DOM
  7. 由于 cb 函数是在后面放到 callbacks 数组,所以这就保证了先完成的 DOM 更新,再执行 cb 函数

vm._render 做了什么?

官方文档没有提供该方法,它是一个用于源码内部的实例方法,负责生成 vnode。其关键代码就一行,执行 render 函数生成 vnode。不过其中加了大量的异常处理代码。

v-for 中的 key有什么作用

  1. vue 内部会通过 key 来判断 两个节点是否相等
  2. 如果 key 相等 再去 判断 标签名 和 一些属性是否相等
  3. 如果 key 不想等 就直接返回false 走更新操作

src/core/vdom/patch.js

/**
 * 判读两个节点是否相同 
 */
function sameVnode (a, b) {
  return (
    // key 必须相同,需要注意的是 undefined === undefined => true
    a.key === b.key && (
      (
        // 标签相同
        a.tag === b.tag &&
        // 都是注释节点
        a.isComment === b.isComment &&
        // 都有 data 属性
        isDef(a.data) === isDef(b.data) &&
        // input 标签的情况
        sameInputType(a, b)
      ) || (
        // 异步占位符节点
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

renderMixin 做了什么?

  1. 在组件实例上挂载一些 运行时需要的工具

/src/core/instance/render.js

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  // 在组件实例上挂载一些运行时需要用到的工具方法
  installRenderHelpers(Vue.prototype)
  
  // ...
}
  1. installRenderHelpers

/src/core/instance/render-helpers/index.js

/**
 * 在实例上挂载简写的渲染工具函数,这些都是运行时代码
 * 这些工具函数在编译器生成的渲染函数中被使用到了
 * @param {*} target Vue 实例
 */
export function installRenderHelpers(target: any) {
  /**
   * v-once 指令的运行时帮助程序,为 VNode 加上打上静态标记
   * 有点多余,因为含有 v-once 指令的节点都被当作静态节点处理了,所以也不会走这儿
   */
  target._o = markOnce
  // 将值转换为数字
  target._n = toNumber
  /**
   * 将值转换为字符串形式,普通值 => String(val),对象 => JSON.stringify(val)
   */
  target._s = toString
  /**
   * 运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组
   */
  target._l = renderList
  target._t = renderSlot
  /**
   * 判断两个值是否相等
   */
  target._q = looseEqual
  /**
   * 相当于 indexOf 方法
   */
  target._i = looseIndexOf
  /**
   * 运行时负责生成静态树的 VNode 的帮助程序,完成了以下两件事
   *   1、执行 staticRenderFns 数组中指定下标的渲染函数,生成静态树的 VNode 并缓存,下次在渲染时从缓存中直接读取(isInFor 必须为 true)
   *   2、为静态树的 VNode 打静态标记
   */
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  /**
   * 为文本节点创建 VNode
   */
  target._v = createTextVNode
  /**
   * 为空节点创建 VNode
   */
  target._e = createEmptyVNode
}

什么是 Hook Event?

Hook Event 是 Vue 的自定义事件结合生命周期钩子实现的一种从组件外部为组件注入额外生命周期方法的功能。

Hook Event 是如果实现的?

<comp @hook:lifecycleMethod="method" />
  1. 处理组件自定义事件的时候(vm.$on) 如果发现组件有 hook:xx 格式的事件(xx 为 Vue 的生命周期函数),则将 vm._hasHookEvent 置为 true,表示该组件有 Hook Event
  2. 在组件生命周期方法被触发的时候,内部会通过 callHook 方法来执行这些生命周期函数,在生命周期函数执行之后,如果发现 vm._hasHookEvent 为 true,则表示当前组件有 Hook Event,通过 vm.$emit('hook:xx') 触发 Hook Event 的执行

简单说一下 Vue 的编译器都做了什么?

  1. Vue 的编译器做了三件事情:
  2. 将组件的 html 模版解析成 AST 对象
  3. 优化,遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
  4. 从 AST 生成运行时的渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数

详细说一说编译器的解析过程,它是怎么将 html 字符串模版变成 AST 对象的?

备注:整个解析过程的核心是处理开始标签和结束标签

  1. 遍历 HTML 模版字符串,通过正则表达式匹配 "<"
  2. 跳过某些不需要处理的标签,比如:注释标签、条件注释标签、Doctype。
  3. 解析开始标签
1.  得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
2.  进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
3.  通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息
4.  接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
5.  处理结束将 ast 对象存放到 stack 数组
6.  处理完成后会截断 html 字符串,将已经处理掉的字符串截掉
  1. 解析闭合标签
1.  如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。
2.  再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式等,并将处理结果放到元素的 AST 对象上
3.  然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
  1. 最后遍历完整个 html 模版字符串以后,返回 ast 对象

详细说一下静态标记的过程

标记静态节点

  • 通过递归的方式标记所有的元素节点
  • 如果节点本身是静态节点,但是存在非静态的子节点,则将节点修改为非静态节点

标记静态根节点,基于静态节点,进一步标记静态根节点

  • 如果节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点,则标记为静态根节点
  • 如果节点本身不是静态根节点,则递归的遍历所有子节点,在子节点中标记静态根

为什么 子节点不全是 文本节点才能标记成静态根节点

  • 因为 如果子节点都是文本节点 收益太低 不如直接做更新文本处理 *** 源码中注释解析

什么样的节点才可以被标记为静态节点?

  • 文本节点
  • 节点上没有 v-bind、v-for、v-if 等指令
  • 非组件

详细说一下渲染函数的生成过程:

第一类

就是一个 render 函数,负责生成动态节点的 vnode

第二类

  1. 是放在一个叫 staticRenderFns 数组中的静态渲染函数,这些函数负责生成静态节点的 vnode

渲染函数生成的过程

其实就是在遍历 AST 节点,通过递归的方式,处理每个节点,

1.  最后生成形如:_c(tag, attr, children, normalizationType) 的结果。
2.  tag 是标签名,
3.  attr 是属性对象,
4.  children 是子节点组成的数组,
5.  其中每个元素的格式都是 _c(tag, attr, children, normalizationTYpe) 的形式,
6.  normalization 表示节点的规范化类型,是一个数字 012,不重要。

静态节点是怎么处理的

  1. 将生成静态节点 vnode 函数放到 staticRenderFns 数组中
  2. 返回一个 _m(idx) 的可执行函数,意思是执行 staticRenderFns 数组中下标为 idx 的函数,生成静态节点的 vnode

v-once、v-if、v-for、组件 等都是怎么处理的

  1. 单纯的 v-once 节点处理方式和静态节点一致
  2. v-if 节点的处理结果是一个三元表达式
  3. v-for 节点的处理结果是可执行的 _l 函数,该函数负责生成 v-for 节点的 vnode
1.  原理 : 就是一个 for 循环 为可迭代对象中的每个元素 执行一次 render 函数 生成 VNode 最后返回一个 VNode 数组
  1. 组件的处理结果和普通元素一样,得到的是形如 _c(compName) 的可执行代码,生成组件的 vnode

响应式数据更新的整个执行过程:

  1. 响应式拦截到数据的更新
  2. dep 通知 watcher 进行异步更新
  3. watcher 更新时执行组件更新函数 updateComponent
  4. 首先执行 vm._render 生成组件的 vnode,这时就会执行编译器生成的函数

一个组件是如何变成 VNode?

  1. 组件实例初始化,最后执行 $mount 进入挂载阶段
  2. 如果是只包含运行时的 vue.js,只直接进入挂载阶段,因为这时候的组件已经变成了渲染函数,编译过程通过模块打包器 + vue-loader + vue-template-compiler 完成的
  3. 如果没有使用预编译,则必须使用全量的 vue.js
  4. 挂载时如果发现组件配置项上没有 render 选项,则进入编译阶段
  5. 将模版字符串编译成 AST 语法树,其实就是一个普通的 JS 对象
  6. 然后优化 AST,遍历 AST 对象,标记每一个节点是否为静态静态;然后再进一步标记出静态根节点,在组件后续更新时会跳过这些静态节点的更新,以提高性能
  7. 接下来从 AST 生成渲染函数,生成的渲染函数有两部分组成:
  8. 负责生成动态节点 VNode 的 render 函数
  9. 还有一个 staticRenderFns 数组,里面每一个元素都是一个生成静态节点 VNode 的函数,这些函数会作为 render 函数的组成部分,负责生成静态节点的 VNode
  10. 接下来将渲染函数放到组件的配置对象上,进入挂载阶段,即执行 mountComponent 方法
  11. 最终负责渲染组件和更新组件的是一个叫 updateComponent 方法,该方法每次执行前首先需要执行 vm._render 函数,该函数负责执行编译器生成的 render,得到组件的 VNode
  12. 将一个组件生成 VNode 的具体工作是由 render 函数中的 _c、_o、_l、_m 等方法完成的,这些方法都被挂载到 Vue 实例上面,负责在运行时生成组件 VNode
  13. _c,负责生成组件或 HTML 元素的 VNode,_c 是所有 render helper 方法中最复杂,也是最核心的一个方法,其它的 _xx 都是它的组成部分
  14. 接收标签、属性 JSON 字符串、子节点数组、节点规范化类型作为参数
  15. 如果标签是平台保留标签或者一个未知的元素,则直接 new VNode(标签信息) 得到 VNode
  16. 如果标签是一个组件,则执行 createComponent 方法生成 VNode
    1. 函数式组件执行自己的 render 函数生成 VNode
    2. 普通组件则实例化一个 VNode,并且在在 data.hook 对象上设置 4 个方法,在组件的 patch 阶段会被调用,从而进入子组件的实例化、挂载阶段,然后进行编译生成渲染函数,直至完成渲染
    3. 当然生成 VNode 之前会进行一些配置处理比如:
    1.   子组件选项合并,合并全局配置项到组件配置项上
    2.  处理自定义组件的 v-model
    3.  处理组件的 props,提取组件的 props 数据,以组件的 props 配置中的属性为 key,父组件中对应的数据为 value 生成一个 propsData 对象;当组件更新时生成新的 VNode,又会进行这一步,这就是 props 响应式的原理
    4.  处理其它数据,比如监听器
    5.  安装内置的 init、prepatch、insert、destroy 钩子到 data.hooks 对象上,组件 patch 阶段会用到这些钩子方法
  1. _l,运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组
  2. _m,负责生成静态节点的 VNode,即执行 staticRenderFns 数组中指定下标的函数

简单总结 render helper 的作用就是

在 Vue 实例上挂载一些运行时的工具方法,这些方法用在编译器生成的渲染函数中,用于生成组件的 VNode。

Vue 的 patch 算法吗?

Vue 的 patch 算法有三个作用:负责首次渲染和后续更新或者销毁组件

  • 如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模版节点

  • 如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode

    • 首先是全量更新所有的属性 vue3.0 做了大量的优化 不在是全量更新

    • 如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程针对前端操作 DOM 节点的特点进行如下优化:

      • 同层比较(降低时间复杂度)深度优先(递归)
      • 而且前端很少有完全打乱节点顺序的情况,所以做了四种假设,假设新老 VNode 的开头结尾存在相同节点,一旦命中假设,就避免了一次循环,降低了 diff 的时间复杂度,提高执行效率。
      • 如果不幸没有命中假设,则执行遍历,从老的 VNode 中找到新的 VNode 的开始节点
      • 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
      • 如果老的 VNode 先于新的 VNode 遍历结束,则剩余的新的 VNode 执行新增节点操作
      • 如果新的 VNode 先于老的 VNode 遍历结束,则剩余的老的 VNode 执行删除操纵,移除这些老节点
    • 如果新的 VNode 有孩子,老的 VNode 没孩子,则新增这些新孩子节点

    • 如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点

    • 剩下一种就是更新文本节点

  • 如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy,销毁老节点

  • 如果新的 VNode 和 老的 VNode 一样 则直接 return

patch

/**
 * vm.__patch__
 *   1、新节点不存在,老节点存在,调用 destroy,销毁老节点
 *   2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
 *   3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
 */
function patch(oldVnode, vnode, hydrating, removeOnly) {
  // 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
    // <div id="app"><comp></comp></div>
    // 这里的 comp 组件初次渲染时就会走这儿
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // 判断 oldVnode 是否为真实元素
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 是真实元素,则表示初次渲染
      if (isRealElement) {
        // 挂载到真实元素以及处理服务端渲染的情况
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // 走到这儿说明不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // 拿到老节点的真实元素
      const oldElm = oldVnode.elm
      // 获取老节点的父元素,即 body
      const parentElm = nodeOps.parentNode(oldElm)

      // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // 递归更新父占位符节点元素
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // 移除老节点
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

patchVnode

  • 更新节点
  • 全量的属性更新
  • 如果新老节点都有孩子,则递归执行 diff
  • 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
  • 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
  • 更新文本节点 文本内容
/**
 * 更新节点
 *   全量的属性更新
 *   如果新老节点都有孩子,则递归执行 diff
 *   如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
 *   如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
 *   更新文本节点
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 老节点和新节点相同,直接返回
  if (oldVnode === vnode) {
    return
  }
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  const elm = vnode.elm = oldVnode.elm
  // 异步占位符节点
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }
  // 跳过静态节点的更新
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    // 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  // 执行组件的 prepatch 钩子
  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
  // 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
  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)
  }
  if (isUndef(vnode.text)) {
    // 新节点不是文本节点
    if (isDef(oldCh) && isDef(ch)) {
      // 如果新老节点都有孩子,则递归执行 diff 过程
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 老孩子不存在,新孩子存在,则创建这些新孩子节点
      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)) {
      // 老孩子存在,新孩子不存在,则移除这些老孩子节点
      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)
  }
}

updateChildren 做了什么?

  1. 定义了4个游标 新开始索引 新结束索引 老开始索引 老结束索引
  2. 并且声明了4个变量 新开始元素 新结束元素 老开始元素 老结束元素
  3. 因为前端操作 数据 是有规律的 很少乱序操作 所以定了4 种假设 一旦命中了一种假设 就会节省掉一次循环 降低时间负责度 俗称头尾相比
1.  新开始节点 和 老开始节点是同一个节点 直接就对比更新
2.  新开始节点 和 老结束节点是同一个节点 直接就对比更新
3.  新结束节点 和 老开始节点是同一个节点 直接就对比更新
4.  新结束节点 和 老结束节点是同一个节点 直接就对比更新
  1. 如果没有命中假设 就会遍历

src/core/vdom/patch.js

/**
 * diff 过程:
 *   diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
 *             如果不幸没有命中假设,则执行遍历,从老节点中找到新开始节点
 *             找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
 *   如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
 *   如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
 */
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // 老节点的开始索引
  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

  // removeOnly是一个特殊的标志,仅由 <transition-group> 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    // 检查新节点的 key 是否重复
    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)) {
      // 老开始节点和新开始节点是同一个节点,执行 patch
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // patch 结束后老开始和新开始的索引分别加 1
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 老结束和新结束是同一个节点,执行 patch
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // patch 结束后老结束和新结束的索引分别减 1
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 老开始和新结束是同一个节点,执行 patch
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // 处理被 transtion-group 包裹的组件时使用
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      // patch 结束后老开始索引加 1,新结束索引减 1
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 老结束和新开始是同一个节点,执行 patch
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      // patch 结束后,老结束的索引减 1,新开始的索引加 1
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引

      // 找到老节点中每个节点 key 和 索引之间的关系映射 => oldKeyToIdx = { key1: idx1, ... }
      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)) {
          // 如果这两个节点是同一个,则执行 patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // patch 结束后将该老节点置为 undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点,则视为新元素,执行创建
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      // 老节点向后移动一个
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 走到这里,说明老姐节点或者新节点被遍历完了
  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)
  }
}

keep-alive 原理

生命周期

  • created:初始化一个cache、keys,前者用来存缓存组件的虚拟dom集合,后者用来存缓存组件的key集合
  • mounted:实时监听include、exclude这两个的变化,并执行相应操作
  • destroyed:删除掉所有缓存相关的东西

原理

  • keep-alive 是由 render 函数决定渲染结果,在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode。
  • include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单
  • keep-alive 没有 template 使用了 render 在组件渲染的时候会自动执行 render 函数
  • 会通过 cache 存储缓存的组件 key = val 的格式
// src/core/components/keep-alive.js

export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键
  props: {
    include: patternTypes, // 缓存白名单
    exclude: patternTypes, // 缓存黑名单
    max: [String, Number] // 缓存的组件实例数量上限
  },
  created () {
    this.cache = Object.create(null) // 缓存虚拟dom
    this.keys = [] // 缓存的虚拟dom的健集合
  },
  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() {
 /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)

      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      /* 获取组件的key值 */
      const key = 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
     /*  拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      }
        /* 如果没有命中缓存,则将其设置进缓存 */
        else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])

  }
}

Vue 1.x 和 Vue 2.x 区别

vue1.x

  1. 一个响应式数据 对应一个 wacher 在 系统体量较小的时候 会比 vue2.x 性能还要快
  2. 但是当一个页面 足够复杂的时候 就会产生很多 wacher 导致性能下降 不适合 企业系统的构建
  3. 没有引入 虚拟dom 和 diff 算法 是直接更新的 dom
  4. 模版中可以有多个根元素

vue2.x

  1. 一个组件 对应一个 wacher 完美解决了 vue1.x 在大型项目中的性能问题
  2. 虚拟dom,虚拟节点的引入, 做到 定向更新 除了提升了性能外,与html的解耦,为vue支持ssr提供了条件。
  3. 在模板中只允许一个根元素;

Vue3 相比于 Vue2 做了哪些优化

  • Vue3移除一些不常用的 API

    • delete set
  • 响应式的优化 不再需要 delete set 等方法 直接 Proxy 替换 Object.defineProperty

    • 缺点: 兼容性更低
  • 引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

  • diff算法优化

    • 新增了静态标记 如果被标记过的 就不会在重新对比
    • vue2 是全量对比
  • 静态提升

    • 会把静态节点进行局部作用域的提升 再次更新的时候不会循环 会直接取声明的
    • vue2 是 无论是否更新 都会重新创建 dom节点然后渲染
  • cacheHandles 事件监听缓存

    • cacheHandlers 是Vue3中提供的事件缓存对象,当 cacheHandlers 开启,会自动生成一个内联函数,同时生成一个静态节点。当事件再次触发时,只需从缓存中调用即可,无需再次更新
    • vue2.x中,绑定事件每次触发都要重新生成全新的function去更新,
  • SSR优化

    • 当存在大量静态内容时,这些内容会被当作纯字符串推进一个 buffer 里面,即使存在动态的绑定,会通过模版插值潜入进去。这样会比通过虚拟 dmo 来渲染的快上很多。
    • 当静态内容大到一个量级的时候,会用_createStaticVNode 方法在客户端去生成一个 static node,这些静态 node,会被直接 innerHtml,就不需要再创建对象,然后根据对象渲染。
  • TypeScript支持

  • Compostion Api

    • 更好的逻辑代码拆分 代码不分散
    • Vue2 中 代码过于分散
  • 支持多根节点组件

Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

Object.defineProperty

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到 所以 Vue 2 提供了 delete set 等全局 API
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

Proxy

  • 直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
  • Proxy可以直接监听数组的变化(push、shift、splice)
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的
  • Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

Vue3 引进 Tree shaking 是为了什么 ?

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)
  • vue 2 中 不管一个在一个页面使用多少 options 属性 最终打出来的包都是 一样大 因为 vue2 都是把方法挂载在实例上面 没办法知道那些属性被使用
  • vue 3 是基于ES6模板语法(import 与 exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
    • 编译阶段利用ES6 Module 判断哪些模块已经加载
    • 判断那些模块和变量未被使用或者引用,进而删除对应代码

Composition API VS Options API

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition API对 tree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的