跳到主要内容

认识

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

一、前言


Vue.js 3.0 源码学习的目标: 精准理解 Vue.js 3.0 的高层设计思想, 在 Vue.js 3.0 框架设计权衡层面有自己深入的思考。从高层的设计角度探讨框架需要关注的问题,从而更好的理解一些具体的实现为何要做出这样的选择。

1.1 权衡

框架设计里到处体现了权衡的艺术 作为框架的设计者, 一定要对框架的定位和方向有全局的把控, 这样才能做好后续的模块设计和拆分;作为框架的学习者, 学习框架的时候, 应该从全局的角度对框架的设计拥有清晰的认知,否则很容易被细节困住,看不清全貌。

1.2 命令式和声明式

命令式 框架的一大特点是关注过程, 通过一行行代码描述了做事的过程, 这符合我们的逻辑直觉;声明式 更加关注结果, 我们的代码提供的只是一个结果, 具体实现我们并不关心。

因此, Vue.js 3.0 内部的实现一定是命令式的, 而暴露给用户的却更加声明式

1.3 性能与可维护性的权衡

命令式声明式 各有优缺点, 在框架设计的层面, 则体现在性能与可维护性之间的权衡。 声明式代码的性能不优于命令式代码的性能, 。命令式代码可以做到极致的性能优化。比如说:

div.textContent = 'hello world 修改';

通过 命令式代码, 我们明确的知道了哪些发生了变更,只要做必要的修改就行了。但是,对于 声明式 代码而言, 为了实现最优的更新性能, 需要找到前后的差异并只更新变化的地方,但是最终完成这次更新的代码仍然是:

div.textContent = 'hello world 修改';

由上可知, 声明式 代码会比 命令式 代码多出找到差异的性能消耗。既然在性能层面, 命令式 代码是更好的选择, 为什么 Vue.js 3.0 要选择声明式*的设计方案呢?

答: 声明式代码的可维护性比命令式更强, 在命令式代码中, 我们需要维护实现目标的整个过程, 包括要手动完成 DOM 元素的创建、更新、删除等工作。而声明式 代码展示的就是我们想要的结果, 看上去更加的直观。所以,Vue.js 3.0 在采用声明式提升可维护性的同时, 做到了 性能损失最小化

1.4 虚拟 DOM 和真实 DOM 的权衡

声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,因此, 如果我们能够最小化找出差异的性能消耗, 就可以让声明式代码的性能无限接近命令式代码的性能。 而虚拟 DOM 就是为了 最小化 找出差异这一步的性能消耗而出现的。

因此, 虚拟 DOM 的更新技术的性能理论上不可能比原生 JavaScript 操作 DOM 更好

二、初始化


Vue.js 3.0 入口逻辑为:

import App from "./Ap.vue";
import { createApp } from 'vue';

const app = createApp(App);
app.mount('#app');

createApp 主要逻辑如下:

export const createApp = ((...args)=>{
const app = ensureRenderer().createApp(...args);
const { mount } = app;
app.mount = ()=>{
……
}
return app;
})

所以, Vue.js 3.0 通过 createApp 开启初始化工作流。通过 mount 开始挂载流程。createApp() 用于创建一个应用实例, 第一个参数是根组件。第二个参数可选,它是要传递给根组件的 propscreateApp 实际调用 ensureRenderer 先获取 baseCreateRenderer 渲染器函数, 并执行。 baseCreateRenderer 返回一个对象, 有 renderhydratecreateApp 方法。其中, createApp 执行的是 createApp 方法, 返回 App 实例。App 实例中提供了 config 上下文、 use 方法、mixin 方法、 component 方法、directive 方法、mount 方法、unmount 方法、provide 方法等。随后, 将原来的 mount 方法保存起来, 重写 mount 方法, 主要逻辑如下:

  1. 获取 root DOM

  2. root DOM innerHTML 赋值给 app.component.template

  3. 清空 root DOM innerHTML

  4. 调用原来的 mount 方法 开始进入挂载流程

三、挂载


Vue.js 3.0 通过 app.mount(container) 来进行挂载, mount 逻辑如下:

  1. 获取容器元素

  2. 清空容器内容

  3. 根据根组件模版创建根组件 VNode

  4. 调用 render 开始进入渲染器流程: render 主要作用为: 接收虚拟DOM, 转化为真实 DOM, 挂载到 container

  5. 根据传入的根组件 VNode, VNode 不为 null, 进入 patch 流程: 只要 VNode 转换为真实 VNode, 都需要调用 patch

  6. 根组件 VNode 类型为 Component, 会进入 processComponent 流程

  7. processComponent 中, 初始化流程会调用 mountComponent, 进入 mountComponent

  8. mountComponent 中, 调用 createComponentInstance 创建组件实例

  9. mountComponent 中, 调用 setupComponent 进行组件实例安装: Vue2.0 组件实例安装, 调用 this._init 实例属性,方法初始化,数据响应式,生命周期的钩子、属性声明,响应式等等。Vue3.0 调用 setupComponent 进行组件实例安装, 初始化 props, 初始化 slots, setup 数据状态处理。

    1. 调用 initProps 初始化 props

    2. 调用 initSlots 初始化 slots

    3. 调用 setupStatefulComponent 执行 setup 函数, 执行逻辑如下:

      1. 创建 setup 上下文, 上下文中有 attrsslotsemitexpose

      2. 获取 setup 函数, 传入 propssetup 上下文, 并执行

      3. 处理 setup 函数返回值, 如果返回值为函数, 将覆盖 instance.render = 返回值, 如果为对象, 作为数据将暴露给模版使用

      4. 调用 finishComponentSetup 得到组件实例 render 函数, 如果组件实例已经存在 render 函数, 不会编译解析 Template。优先级为 render > template。如果组件实例不存在 render 函数, 调用 compile 函数, 编译解析 Template, 得到 render 函数。

      5. finishComponentSetup 中最后调用 applyOptions 兼容 Vue.js 2.0 选项式 API。其中, data 会通过 reactive 来实现响应式

  10. mountComponent 中, 调用 setupRenderEffect 安装渲染函数副作用: Vue2 中的更新机制为 new Watcher(updateComponent), Vue3 的更新机制为 ReactiveEffect(componentUpdateFn, ()=> queueJob(update))

    1. 定义副作用函数: 执行组件render函数, 返回组件子节点VNode, 子节点 VNode 作为 subTree 调用 patch 继续递归执行。执行 render 函数的期间, 如果有响应式数据, 会访问响应式数据, 会收集当前副作用函数。当响应式数据发生变化时, 会触发当前副作用函数重新执行。

    2. 基于副作用函数创建 effect 实例, 并以调度器的方式, 将当前的更新函数加入到异步任务队列, 等所有数据都更新完成再一次统一执行异步任务队列里面的更新函数,进行VNode的更新。

  11. 最后调用 mountElementVNode 转化为真实 DOM 挂载到容器上

四、更新


五、思考与沉底


六、参考资料