【F2E】《Vue3 核心源码解析》总结
《Vue3 核心源码解析》总结
Vue3 优化
Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念, Vue.js 2.x 发展了很久,现在周边的生态设施都已经非常完善了,但是还是存在不少痛点:比如源码自身的维护性、数据量大后带来的渲染和更新的性能问题、一些想舍弃但为了兼容一直保留的鸡肋 API 等;框架开发者希望能给普通开发人员带来更好的编程体验,比如更好的 TypeScript 支持、更好的逻辑复用实践等,于是从源码、性能和语法 API 三个大的方面优化框架。
源码
源码优化,也是对 Vue 框架本身开发的优化,主要目的是让代码更易于开发和维护。源码的优化主要体现在使用 monorepo 和 TypeScript 管理和开发源码。
采用 monorepo 代码管理方式
Vue3,整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中:
相对于 Vue.js 2.x 的源码组织方式,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
引入 TypeScript
源码的优化还体现在 Vue.js 3.0 自身采用了 TypeScript 开发。对于复杂的框架项目开发,使用类型语言非常有利于代码的维护,因为它可以在编码期间帮你做类型检查,避免一些因类型问题导致的错误;也可以利于它去定义接口的类型,利于 IDE 对变量类型的推导。
TypeScript 提供了更好的类型检查,能支持复杂的类型推导;由于源码就使用 TypeScript 编写,也省去了单独维护 d.ts 文件的麻烦;
性能
源码体积
静态资源体积优化,JavaScript 包体积越小,意味着网络传输时间越短,JavaScript 引擎解析包的速度也越快。
- 移除一些冷门的 feature(比如 filter、inline-template 等);
- 引入 tree-shaking 的技术,减少打包体积。
数据劫持
Vue.js 1.x 和 Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,此 API 存在一些问题:
- 要想使用此 API,必须预先知道要拦截的 key 是什么,所以并不能检测对象属性的添加和删除。Vue.js 为了解决这个问题提供了 $set 和 $delete 实例方法。
- Object.defineProperty 对于一个嵌套层级较深的对象,如果要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,如果我们定义的响应式数据过于复杂,这就会有相当大的性能负担。
为了解决上述 2 个问题,Vue.js 3.0 使用了 Proxy API 做数据劫持
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
},
})
Proxy API 并不能监听到内部深层次的对象变化,Vue3 在 getter 中去递归响应式,真正访问到的内部对象才会变成响应式
编译过程
Vue3 通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。
Vue3 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法
语法
逻辑组织
Vue1.x 与 Vue2.x Options API 的设计是按照 methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找。
Vue3 Composition API,它有一个很好的机制去解决这样的问题,就是将某个逻辑关注点相关的代码全都放在一个函数里
逻辑复用
定义自定义 hook 函数,在组件中使用
Composition API 除了在逻辑复用方面有优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。
核心实现
虚拟 DOM 渲染
组件是一个抽象的概念,它是对一棵 DOM 树的抽象,组件的渲染取决于组件的模板
组件的模板决定了组件生成的 DOM 标签,而在 Vue.js 内部,一个组件想要真正的渲染生成 DOM,还需要经历“创建 vnode - 渲染 vnode - 生成 DOM” 这几个步骤
- 应用程序初始化:创建 app 对象和重写 app.mount 方法
const createApp = (...args) => {
// 创建 app 对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写 mount 方法
app.mount = containerOrSelector => {
// ...
}
return app
}
-
创建 vnode:组件 vnode 其实是对抽象事物的描述,将 DOM 使用 JavaScript 对象来描述,可以描述不同类型的节点,比如普通元素节点、组件节点等
-
渲染 vnode:渲染组件生成 subTree、把 subTree 挂载到 container 中。通过 reder 函数创建组件树 vnode,把这个 vnode 再经过内部一层标准化,得到子树 vnode
-
挂载元素函数:创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上
diff 流程
在已知旧子节点的 DOM 结构、vnode 和新子节点的 vnode 情况下,以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作。
-
同步头部节点
同步头部节点就是从头部开始,依次对比新节点和旧节点,如果它们相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。 -
同步尾部节点
同步尾部节点就是从尾部开始,依次对比新节点和旧节点,如果相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。
接下来只有 3 种情况要处理:
- 新子节点有剩余要添加的新节点;
- 旧子节点有剩余要删除的多余节点;
- 未知子序列。
- 添加新节点
如果索引 i 大于尾部索引 e1 且 i 小于 e2,那么从索引 i 开始到索引 e2 之间,直接挂载新子树这部分的节点。
添加完 e 节点后,旧子节点的 DOM 和新子节点对应的 vnode 映射一致,也就完成了更新。
- 删除多余节点
-
从头部同步节点:
-
接着从尾部同步节点:
-
删除子节点中的多余节点:
-
旧子节点的 DOM 和新子节点对应的 vnode 映射一致,也就完成了更新。