Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念,它为后续做服务端渲染以及跨端框架 Weex 提供了基础。 Vue.js 2.x 发展了很久,现在周边的生态设施都已经非常完善了,而且对于 Vue.js 用户而言,它几乎满足了我们日常开发的所有需求。在迭代 2.x 版本的过程中,小右发现了很多需要解决的痛点,比如源码自身的维护性,数据量大后带来的渲染和更新的性能问题,一些想舍弃但为了兼容一直保留的鸡肋 API 等;另外,小右还希望能给开发人员带来更好的编程体验,比如更好的 TypeScript 支持、更好的逻辑复用实践等,所以他希望能从源码、性能和语法 API 三个大的方面优化框架。 那么接下来,我们就一起来看一下 Vue.js 3.0 具体做了哪些优化, 了解Vue.js 3.0的升级给我们开发带来什么收益。
1.源码优化
1-1.更好的代码管理方式:monorepo
2.x
源码托管在 src 目录
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码
3.0
monorepo 把这些模块拆分到不同的目录中,每个模块有各自的API类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
@vue
├── compiler-core
│ ├── LICENSE
│ ├── README.md
│ ├── dist
│ │ ├── compiler-core.cjs.js
│ │ ├── compiler-core.cjs.prod.js
│ │ ├── compiler-core.d.ts
│ │ └── compiler-core.esm-bundler.js
│ ├── index.js
│ └── package.json
├── compiler-dom
│ ...
├── reactivity
│ ...
├── runtime-core
│ ...
├── runtime-dom
│ ...
└── shared
...
1-2.有类型的 JavaScript:TypeScript
2.x
使用Flow做类型检查,Flow 是 Facebook 出品的 JavaScript 静态类型检查工具,它可以以非常小的成本对已有的 JavaScript 代码迁入,非常灵活。但是Flow 对于一些复杂场景类型的检查,支持得并不好。 3.0 使用 TypeScript 重构了整个项目。 TypeScript提供了更好的类型检查,能支持复杂的类型推导。
2.性能优化
2-1.源码体积优化
3.0
- 移除一些冷门的 feature(比如 filter、inline-template 等);
- 引入 tree-shaking 的技术,减少打包体积;
2-2.数据劫持优化
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式来达到数据响应效果的。大体思路参考下图。(详细原理自行学习,哈哈)
2.x Vue.js 1.x 和 Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,具体是这样的:
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})
但这个 API 有一些缺陷:
- 它必须预先知道要拦截的 key 是什么,所以它并不能检测对象属性的添加和删除。尽管 Vue.js 为了解决这个问题提供了 delete 实例方法。
- 对于嵌套层级较深的对象,如果要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的。如果我们定义的响应式数据过于复杂,这就会有相当大的性能损耗。 3.0 为了解决上述 2 个问题,Vue.js 3.0 使用了 Proxy API 做数据劫持,它的内部是这样的:
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
}
})
- 使用了 Proxy API 做数据劫持,它劫持的是整个对象,对于对象的属性的增加和删除都能检测到。
- Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能,我会在后面分析响应式章节详细介绍它的具体实现原理。
2-3.编译优化
2.x 通过数据劫持和依赖收集,Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,虽然 Vue 能保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vnode 树。这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。 3.0 通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,
3.语法 API 优化:Composition API
3-1.优化逻辑组织
2.x 在 Vue.js 2.x 版本中,编写组件本质就是在编写一个“包含了描述组件选项的对象”,我们把它称为 Options API,Options API 的设计是按照 methods、computed、data、props 这些不同的选项分类,和一个逻辑点相关的代码可能散落在各个 Option里,非常分散,如果需要修改一个逻辑点,就需要在单个文件中不断切换和寻找。 3.0 Vue.js 3.0 提供了一种新的 API:Composition API,它有一个很好的机制去解决这样的问题,就是将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。
3-2.优化逻辑复用
2.x 我们通常会用 mixins 去复用逻辑。使用单个 mixin 似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。 首先每个 mixin 都可以定义自己的 props、data,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。 3.0 使用更多的 hook 函数,整个数据来源清晰了,也不会出现命名冲突的问题。
3-3.更好的类型支持
因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了。不像 Options API 所有的东西使用 this。
3-4.tree-shaking 友好
tree-shaking有一个两个要求(对tree-shaking不熟的,还是自行去学习,哈哈):
- 一个是必须是import导入。
- 另一个是必须是单个函数或常量导出。 如果导出的是一个对象,那也无法用tree-shaking 2.x 直接导出的是整个vue实例,如果我们只是简单的用某一些功能的话就有点累赘。 3.0 用到的函数可以通过import声明,对“按需加载”有更好的支持。
3-5.说明
Composition API 属于 API 的增强,它并不是 Vue.js 3.0 组件开发的范式,如果你的组件足够简单,你还是可以使用 Options API。
总结
这片文章主要总结了 Vue.js 3.0 升级做了几个方面的优化,以及为什么会需要这些优化。希望学习完后我们也可以像小右一样去审视自己的工作,有哪些痛点,找到可以改进和努力的方向并实施,只有这样才能够不断提升自己的能力,工作上也会有不错的产出。
文章参考来源:黄轶老师--《Vue.js 3.0 核心源码解析》课程。