跳到主要内容

认识

2024年03月13日
柏拉文
越努力,越幸运

一、认识


二、初始化


  1. 定义 Vue.prototype._initVue.prototype.$dataVue.prototype.$propsVue.prototype.$setVue.prototype.$deleteVue.prototype.$watchVue.prototype.$onVue.prototype.$onceVue.prototype.$offVue.prototype.$emitVue.prototype._updateVue.prototype.$forceUpdateVue.prototype.$destroyVue.prototype.$nextTickVue.prototype._render等方法

  2. 执行 Vue.prototype._init 函数

  3. initInternalComponent() 或者 mergeOptions 处理组件配置项

    • initInternalComponent(): 每个子组件初始化时走这里,这里只做了一些性能优化, 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率

    • mergeOptions: 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中

  4. initLifecycle(vm) 初始化组件实例的关系属性,比如 $parent$children$root$refs

  5. initEvents(vm) 处理自定义事件: 这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关

  6. initRender(vm) 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数

  7. callHook(vm, 'beforeCreate', undefined, false /* setContext */) 调用 beforeCreate 钩子函数: 数据初始化并未完成,像dataprops这些属性无法访问到

  8. initInjections(vm) 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 keyvm 实例

  9. initState(vm) 处理数据响应式,处理 propsmethodsdatacomputedwatch

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

  11. callHook(vm, 'created') 调用 created 钩子函数: 数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

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

  13. 接下来则进入挂载阶段

三、挂载


  1. 调用 $mount , $mount 主要有以下工作:

    1. 处理 render 选项或者 template 选项, 如果两个都没有, 通过 el 获取 DOM 上的 outerHTML 字符串

    2. 通过 compileToFunctions 编译 template , 生成 render 函数 和 staticRenderFns 并挂载到 Vue.$options

  2. 调用 $mount > Vue.prototype.$mount > mountComponent

  3. mountComponent 函数中,主要有以下工作:

    1. callHook(vm, 'beforeMount') 执行 beforeMount 钩子

    2. 定义 updateComponent 函数, updateComponent 函数可以渲染 VNode

    3. 实例化渲染 Watcher, 将updateComponent 作为第二个参数传入, updateComponent 后续会作为 Watchergetter 函数。

  4. Watcherconstructor 构造函数中, 初始调用 this.getter 函数, 进而调用执行 updateComponent 函数进行首次渲染

  5. updateComponent 函数中, 主要有以下工作:

    1. 执行 vm._render 生成组件的 VNode: 在组件渲染的过程中, 用到的数据属性会触发 getter, getter 内部会收集依赖。

    2. 执行 vm.__patch__ 进行首次渲染

  6. 递归遍历 VNode , 创建各个节点,处理节点上的属性和指令, 如果是自定义组件则创建组件实例, 进行组件的初始化、挂载

  7. 最终所有 VNode 变成真实的 DOM 节点并替换掉页面上的模版内容

  8. 完成初始渲染

四、更新


  1. 响应式拦截到数据的更新, setter 拦截到更新操作

  2. 调用 dep.notify() , 循环遍历 subs 中的所有 Watcher, 执行 Watcherupdate 方法。

  3. update 方法中, 会将此时的 Watcher 加入到渲染队列 queue, 通过 nextTick 进行批量更新渲染

  4. nextTick 中,通过 promise.then(flushCallbacks) 将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个 Watcher, 开始执行每一个 Watcherrun 方法

  5. Watcher 中的 run 方法调用 Watcher 中的 get 方法, get 方法调用 Watcher 中的 getter 函数, 此时的 getter 函数就是 updateComponent , 用于初始或者更新渲染

  6. 首先执行 vm._render 生成组件的 vnode,这时就会执行编译器生成的函数

  7. 执行 vm.__patch__ 进行更新渲染

  8. 执行 patchVnode 进行 VNode Diff 操作

  9. 完成更新

五、思考与沉淀


5.1 Vue 的编译过程的设计思想?

Vue.js 在不同的平台下都会有编译的过程, 因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入, Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开

5.2 Vue 在订阅依赖时所做的优化?

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 ab,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

5.3 Vue 是如何优雅的处理不同平台不同的 patch?

5.4 Vue 是如何将 Dep 类 和 Watcher 类建立联系?

参考资料


Vue.js 技术揭秘