[vue3] vue3对比vue2优化项简要概述
不足与展望
Vue2的不足
- 源码自身的可维护性较差;
- 数据量大后带来渲染和更新的性能问题;
- 存在一些为了兼容但是和鸡肋的API。
对Vue3的期望
- 更好的编程体验;
- 更好的
TypeScript
支持; - 更好的逻辑复用实践。
从源码、性能、语法API三大方面优化框架。
Vue3的优化
源码优化
源码优化的目的是让代码更易于开发和维护。
主要体现在使用monorepo
管理源码仓库,以及使用TypeScript
进行开发。
monorepo
monorepo
将模块拆分到不同的package
中,每个package
拥有各自的API
、类型定义和测试。
这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确。
开发人员也更容易阅读、理解和更改所有模块源码,提高了代码的可维护性。
使用mororepo
还有一个好处是:package
可以独立于Vue.js
使用,例如reactivity
响应式库。
如果用户仅需要使用Vue.js 3.0
的响应式功能,可以单独依赖这个package
而不需要依赖整个Vue.js
,减小了引用包的体积大小。
TypeScript
使用TypeScript
的好处是:
- 可以在编码阶段做类型检查;
- 定义接口类型,有利于
IDE
对变量类型的推导,开发更轻松。
Vue
曾经用过flow
作为类型工具,但后来转TypeScript
了,原因在于:
TypeScript
的社区更活跃;TypeScript
在IDE
中的支持更好;TypeScript
有明确的版本发布策略,使得维护和升级更加可控,Flow
的团队后期烂尾。
一篇相关的文章:Flow vs Typescript: From Flow to Typescript. Why? - DEV Community
性能优化
减小包体积
- 移除一些冷门的
feature
,比如inline-template
; - 引入
tree-shaking
技术。
tree-shaking
tree-shaking
依赖ES2015
的模块语法,通过编译阶段的静态分析,找到没有引入的模块并打上标记。
在项目中没有引入的模块或组件,它们对应的代码就不会被打包,也就间接减小了项目引入Vue.js
包体积的目的。
数据劫持优化
Vue
最独特的特性之一,是其非侵入性的响应式系统。
实现非侵入式的响应式系统的关键在于拦截对数据的读写操作。
响应式数据需要配置getter
和setter
。当渲染函数执行时,读取了数据,触发getter
,在getter
中将object[key]
到依赖的映射记录到Watcher
中。
依赖是一个函数,对于在
template
中使用的响应式数据来说,它的依赖就是渲染函数。
当响应式数据被更新时,即被赋值,会触发setter
,在setter
中根据object
和key
去Watcher
中找到相对应的依赖函数,执行。即触发了视图更新。
总结:
- 在
getter
中使用track
函数收集依赖; - 在
setter
中使用trigger
函数触发更新。
Vue2的做法
在Vue2
中使用Object.defineProperty
这个方法拦截对对象指定属性的读写操作。
Object.defineProperty
为对象的指定属性设置getter
和setter
。
对于一个嵌套层级较深的对象来说,为了遍历对象的每个属性,需要递归调用,为每个属性都设置getter&setter
。
缺点:
-
递归调用会导致性能较差;
-
无法检测
property
的添加或移除,后来新增的属性无法实现响应式;需要使用全局的
Vue.set
方法或者组件内部局部的this.$set
方法,让Vue
有能力将依赖添加到Watcher
中进行管理。 -
使用索引设置数组的某一项的值,无法被监测到;
-
修改数组的
length
属性,不会触发更新; -
无法应对
Map
、Set
这些集合类型的响应式,需要开发者自己想办法解决。
这些缺点都来自于Object.defineProperty
这个API本身的局限性。
Vue3的做法
在Vue3
中,数据拦截使用了ES6
的Proxy API
,这个API
可以直接将目标对象作为整体,拦截对它的各种操作,包括:
- 对某个属性的读/写;
- 新增属性;
- 删除属性;
Proxy
可以Map/Set
在getter
拦截到对get/set/add
等方法的调用。
因此,Vue3
相比于Vue2
:
- 解决了递归配置
getter&setter
的问题; - 解决了新增/删除属性没有响应式的问题;
- 实现了响应式的
Map/Set
。
可以说Vue3
在响应式这一部分的提升来自于ES6
提供了更现代化更高效的API
。
编译优化
在Vue2
中,从new Vue()
到生成DOM
的过程大致如下:
上述的响应式过程发生在init
阶段,而template
编译为render function
的阶段可以在打包过程中完成。
而在运行时中,耗时较多的是patch
阶段。
Vue3
在编译阶段优化了编译的结果,实现了对patch
过程的优化。
具体来说是通过flag
标记、静态提升的手段来实现的。
在Vue2
中,数据更新并触发重新渲染的粒度是组件级的。
在组件内部仍然需要遍历整颗VNode树,如果组件包含许多静态内容,那么diff
算法会在静态内容上浪费许多时间。
也就是说diff
算法的效率由组件的虚拟DOM树规模决定,而不是由动态节点的规模决定。
Vue3
使用了Block Tree
来优化diff
过程。
Block Tree
Block Tree 是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的(
STABLE_FLAGMENT
)。对于结构固定的区块,仅需要使用一个
Array
来追踪其包含的动态节点。
借助 Block Tree,Vue3
将 vnode
的更新性能调整为与动态内容的数量相关。
其它
Vue3
在编译阶段实现了对Slot
的编译优化、事件侦听函数的缓存优化;Vue3
重写了diff
算法。
语法API优化
Vue3
在语法上的一个大的变动主要是提供了Composition API
,即组合式API。
在Vue2
中,编写组件本质是在编写一个“包含了描述组件选项的对象”,被称为Options API
,即选项式API。
选项式API适合开发小型组件,选项内容一目了然。对于包含多个逻辑关注点的大型组件来说,使用选项式API会导致一个逻辑关注点的代码被分散到不同选项中。
组合式API的好处在于可以将逻辑关注点相关的代码封装到一个函数里。
逻辑复用优化
在Vue2
中,存在mixins
这种逻辑复用的方式。它的缺点在于当一个组件混入多个来源的mixins
后,会出现命名冲突和数据来源不清晰的问题。
在Vue3
中,通过组合式函数解决了这一问题,组合式函数将可复用逻辑封装成函数(按照规范通常以use
作为函数名开头),通常是返回一个对象。这是一种更灵活且更稳定的做法,因为:
- 对于组合式函数来说,可以选择性地向外暴露API;
- 对于使用地组件来说,解构返回的对象时,可以重命名避免命名冲突。
组合式API的其它好处:
- 更好的类型支持:组合式API都是函数,类型更容易推导,不想选项式API所有的内容都要通过
this
联系; - 组合式API对
tree-shaking
比较友好,代码也更容易压缩。