02-Vue3的改变

1. 更快

与 Vue2 相比 Vue3 进一步压榨运行时性能

  1. Object.defineProperty vs Proxy
  2. Virtual DOM 重构
  3. 更多编译时优化

1. Proxy

  • 首要的细节 Vue3.0 把数据对象侦测的 API 从 Object.defineProperty 去劫持 getter and setter 换成 proxy,初始性能有实际性的提升
  • 因为 Object.defineProperty 在转化数据对象属性为 getter, setter 其实是一个相当昂贵的操作,因为 JS 引擎喜欢对象的结构越稳定越好,对象的结构不停地改变对它而言可优化性就变低了
  • Proxy 优点:对原始对象做了一个真正的 Proxy,是真正的在对象层面做了 Proxy 不会改变对象结构
Object.definePropertyProxy
对对象属性劫持对整个对象劫持
需要手动绑定响应式对象新增属性的响应性能侦测对象新增属性并实时响应
无法侦测数组对象的变化,需要手动设置代理原型能监听数组
启动就递归,不管对象嵌套多深都把链路上的 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)

1. block tree

一个动态的模板,把它切分成相对内部是静态的块,每一个静态的块内部只需要以一个 Array 去追踪它内部的动态内容。这样就能极大程度上的减少无谓的遍历比对操作

vue2vue3
当 message 值发生改变,需要拿到新旧的 vdom 进行非常耗时的遍历比对操作- block 方式,这个模板就只有一个单独的 block,这个 block 内部唯一的动态节点就是 message,所以更新操作实际上只要检查 message 值改变没有
- 把更新的性能:由模板的整体大小相关,彻底转变成了有多少动态内容相关
- Vue3 更新性能比 Vue2 快了将近 6 倍

4. Vue3.0 Compiler优化细节

Vue 3 Template Exploreropen in new window

<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 去计算一个表达式的值,当这个值变化就触发回调

  1. 更好的 TypeScript 类型推断支持
  2. 更灵活的逻辑复用能力
  3. tree shaking 更友好
    • 在 Vue 3 中,全局和内部 API 都经过了重构,因此全局 API 包括(reative、ref、computed、watch、nextTick...)在实例开发中作为 ES 模块通过 export 单独引入,这使得它们对 tree-shaking 非常友好。没有被使用的 API 的相关代码可以在最终打包时被移除。这样 Vue 本身的尺寸就是动态的了,你使用的功能越少打包出来的 bundler size 也就越小
    • 同时,基于函数 API 所写的代码也有更好的压缩效率,因为所有的函数名和 setup 函数体内部的变量名都可以被压缩,但对象和 class 的属性/方法名却不可以

4. Vue3新特性

Vue3 详细文档open in new window

composition API 包含

  • ref & reactive
  • 新的生命周期函数
  • computed 和 watch
  • Hooks

其他

  • Tleport 新内置组件
  • Suspense 新内置组件

1. script-setup与ref-sugar