02-Vue3的改变
1. 更快
与 Vue2 相比 Vue3 进一步压榨运行时性能
Object.defineProperty
vsProxy
Virtual DOM
重构- 更多编译时优化
1. Proxy
- 首要的细节 Vue3.0 把数据对象侦测的 API 从
Object.defineProperty
去劫持getter and setter
换成proxy
,初始性能有实际性的提升 - 因为
Object.defineProperty
在转化数据对象属性为getter, setter
其实是一个相当昂贵的操作,因为 JS 引擎喜欢对象的结构越稳定越好,对象的结构不停地改变对它而言可优化性就变低了 Proxy
优点:对原始对象做了一个真正的Proxy
,是真正的在对象层面做了Proxy
不会改变对象结构
Object.defineProperty | Proxy |
---|---|
对对象属性劫持 | 对整个对象劫持 |
需要手动绑定响应式对象新增属性的响应性 | 能侦测对象新增属性并实时响应 |
无法侦测数组对象的变化,需要手动设置代理原型 | 能监听数组 |
启动就递归,不管对象嵌套多深都把链路上的 propery 转化为响应式的 | 只在 getter 时才进行对象下一层属性的劫持 |
无兼容性问题 | 兼容性问题 |
2. Virtual DOM重构
整个 Virtual DOM 用 TS 重写了,初始性能和组件的启动性能比之前快了将近一倍
3. 更多编译时优化
- Slot 默认编译为函数,这样就使得父子组件不存在更新的强耦合,然后生成 Vnode 的函数尽量让其参数一致化
- 在编译时给每一个 VNode 带着关于他类型跟 children 类型的信息。都可以帮助 run time 变得更快
2. 传统vdom性能瓶颈
Virtual DOM 可能误以为它是为了操作更快而产生的,其实不是。Virtual DOM 是一个抽象层
- 作用:能让纯 JS 去描述界面,UI 是什么样子
- 核心价值:更加强大灵活性及表达力
- 反过来它付出的代价是:每次数据更新 Virtual DOM 理论上是要重新创建 Virtual DOM 树状的数据结构,然后算法需要从头到尾把旧的 tree 和新的 tree 进行一次彻底的遍历比对
虽然 Vue 通过数据侦测能在组件层面最小化要更新的点,但是组件这个粒度还是相对比较粗的,单个组件内部仍需要遍历该组件的整个 vdom tree
这样一个模板,左边是一个模板,右边是在每次数据更新时,算法要实际进行的操作
- 这个过程,就是从上到下,先要去 diff div 看下新的 VNode 跟旧的 VNode 是不是同一个 div,如果是同一个再去看它的 id 有没有变。 然后在去看它的 children 有没有变,这些子节点顺序有没有发生改变。 顺序改变之后,还要去看这个元素的节点(class、text)发生改变没有
- 实际上可以看到,模板中只有 message 是动态改变。但每次都需要去遍历整个
vdom tree
来进行比对 - 这个算法 react 推出来时,大家都在质疑,这样更新渲染会不会很慢。 但现在的 JS 引擎够快,大多数情况下,可能在 16ms 内完成更新。但是应用足够大的情况下,16ms 不总是够的
1. react方式
最初的 vdom 不是从模板编译而来。react 它的 vdom 是从 jsx 编译过来。jsx 只是 JS 的一个语法延伸,它具备 JS 的一切动态性
- 时间分片,就是承认框架在使用时,会消耗大量的 JS 的 CPU 时间,策略是把这些 CPU 时间切分到一帧一帧。从而不会去影响用户的操作
2. Vue3不抛弃vdom
- 高级场景下手写 render function获得更强的表达力
- 生成代码更简洁
- 兼容Vue2.x(现有生态都依赖于 Virtual DOM)
3. Vue3改进
Vue 特有的东西底层是 Virtual DOM,上层是包含大量有价值的静态信息模板。这点跟 react 完全不一样。同样一个组件一眼就能看出除了这个 message 其他所有的节点都是不会变的。这就是一个包含可以推测的优化信息量的不同
- Vue2.x 方式:首先兼容 render function,然后再 Vdom tree 设置规则把永远不会变化的 node 标注为静态节点 Vnode 存储在内存中。两个新旧 tree 在比对时,静态节点就会直接复用内存中的 Vnode
- Vue3:找到了一种比 Vue2 更加压榨性能的思路
- 在 Vue 中能改变模板节点结构的有结构性指令
v-if, v-for
。结构性指令为边界。把模块切分成一个一个的静态的块(block tree)
- 在 Vue 中能改变模板节点结构的有结构性指令
1. block tree
一个动态的模板,把它切分成相对内部是静态的块,每一个静态的块内部只需要以一个 Array 去追踪它内部的动态内容。这样就能极大程度上的减少无谓的遍历比对操作
vue2 | vue3 |
---|---|
当 message 值发生改变,需要拿到新旧的 vdom 进行非常耗时的遍历比对操作 | - block 方式,这个模板就只有一个单独的 block,这个 block 内部唯一的动态节点就是 message,所以更新操作实际上只要检查 message 值改变没有 - 把更新的性能:由模板的整体大小相关,彻底转变成了有多少动态内容相关 - Vue3 更新性能比 Vue2 快了将近 6 倍 |
4. Vue3.0 Compiler优化细节
<div>
<span>static</span>
<span>static</span>
<span>static</span>
<span>static</span>
<span>{{message}}</span>
<span>static</span>
<span>static</span>
<span>static</span>
</div>
生成的渲染函数:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static")
]))
}
- 之前的 Virtual DOM 算法下,需要把所有的 span 全部都过一遍,而且所有的 span 都需要去看它旧的 props 和新的 props 有没有变。虽然 JS 做这些事情很快,但应用越来越大时,不可避免的会占用更多的更新时间
- 新版本优化下,一个 block 就直接看它里面有没有任何动态的内容。只要把这些动态的节点过一遍就行。节省了很多更新时所消耗的性能时间
- 甚至在一个 block 里不管嵌套的多深,不需要遍历这个 div ,只需要去寻找到这个动态的节点 span
<div>
<span>static</span>
<span>static</span>
<span>static</span>
<span>static</span>
<div>
<span>{{message}}</span>
</div>
<span>static</span>
<span>static</span>
<span>static</span>
</div>
- 所有动态节点都是跟根节点的 block 绑定起来的。在数据更新时,只要走到根节点 div 的 block 就可以跳转到动态节点上
- 另一个优化:有个静态的 id 绑定,patchFlag 也没有变化,对于 run time 来说,这个 id 在与不在都没区别,只会在创建时创建一次,后面的更新就不再管它
<span id="foo">{{message}}</span>
一个动态的绑定
<div>
<span>static</span>
<span>static</span>
<span>static</span>
<span>static</span>
<span :id="foo" class = "count">{{message}}</span>
<span>static</span>
<span>static</span>
<span>static</span>
</div>
生成的渲染函数
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", {
id: _ctx.foo,
class: "count"
}, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, ["id"]),
_createVNode("span", null, "static"),
_createVNode("span", null, "static"),
_createVNode("span", null, "static")
]))
}
- 在动态更新时,永远只会关注那些真正会变的东西。这样既跳出了 Virtual DOM 更新时的性能瓶颈,又依然保留了可以手写 render function 的灵活性
- 既拥有 react 的灵活性,又保留了基于模板的性能保证
3. 更好的逻辑组合方式
- Vue2 的 options API 简洁的代码组织方式所吸引,但当组件复杂度提升,原有的"简洁性"又会成为负担
- Vue3 compostion API 示例
const app = {
setup() {
// data
const count = ref(0);
// computed
const plusOne = computed(() => count.value + 1);
// method
const increname = () => count.value++;
// watch
watch(() => count.value * 2, v => console.log(v));
// lifecycle
onMounted(() => console.log('onmounted'));
// 暴露给模板或渲染函数
return {
count
};
}
}
ref 会创建一个值,这个值会包含数字 0。这个值其实就是一个 wrapper
包装对象,然后通过 count.value
来取值
- 即使 count 包含是一个原始类型的值,也可以把它在函数之间传来传去,传来传去的同时每当用这个
.value
来取值时,还会被追踪依赖。 然后去改这个.value
的值它又会触发更新 - 可以直接创建一个计算属性,计算属性返回的也是值的包装
- watch 去计算一个表达式的值,当这个值变化就触发回调
- 更好的 TypeScript 类型推断支持
- 更灵活的逻辑复用能力
- tree shaking 更友好
- 在 Vue 3 中,全局和内部 API 都经过了重构,因此全局 API 包括(reative、ref、computed、watch、nextTick...)在实例开发中作为 ES 模块通过 export 单独引入,这使得它们对 tree-shaking 非常友好。没有被使用的 API 的相关代码可以在最终打包时被移除。这样 Vue 本身的尺寸就是动态的了,你使用的功能越少打包出来的 bundler size 也就越小
- 同时,基于函数 API 所写的代码也有更好的压缩效率,因为所有的函数名和 setup 函数体内部的变量名都可以被压缩,但对象和 class 的属性/方法名却不可以
4. Vue3新特性
composition API 包含
- ref & reactive
- 新的生命周期函数
- computed 和 watch
- Hooks
其他
- Tleport 新内置组件
- Suspense 新内置组件