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