最近Vue团队公布了Vue 3.0的开发计划和RFC引起了前端社区的广泛关注。

Vue 3.0将会有很多重大的改进,比如新的响应式系统基于Proxy替代Object.defineProperty新的虚拟DOM实现性能更好更小的打包体积更好的TypeScript支持Composition API等等。

虽然Vue 3.0还在开发中正式版还没发布,但是根据已经公布的RFC和源码我们可以前瞻一下Vue 3.0的原理和底层机制。

注意本文基于当前公布的RFC和开发中的源码正式版发布后可能会有调整仅供参考。

今天想深入剖析Vue 3.0的核心原理包括响应式系统虚拟DOM编译优化等等帮大家提前了解Vue 3.0。

一、Vue 3.0的整体改进

在讲具体原理之前,先简单介绍一下Vue 3.0的整体改进。

Vue 3.0的设计目标是:

  1. 更小:打包体积更小tree-shaking更好按需引入功能。
  2. 更快:虚拟DOM重写编译优化响应式系统优化整体性能提升。
  3. 更易维护:用TypeScript重写代码结构更清晰更易维护。
  4. 更好的原生支持:更好的TypeScript支持更好的JSX支持。
  5. 新的API:Composition API提供更灵活的逻辑组织和,复用方式。

Vue 3.0虽然有很多重大改进,但是会保持对Vue 2.x的兼容大部分API不变迁移成本不会太高。

二、响应式系统的重写

Vue 3.0最大的改进之一就是响应式系统的重写从Object.defineProperty改成Proxy。

Vue 2.x的响应式原理

Vue 2.x用Object.defineProperty把data的属性转成getter/setter在,getter里收集依赖在setter里触发更新。

Object.defineProperty(obj, key, {
  get() {
    // 收集依赖
    dep.depend();
    return value;
  },
  set(newValue) {
    if (newValue === value) return;
    value = newValue;
    // 触发更新
    dep.notify();
  }
});

这种方式有一些局限:

  1. 只能拦截属性的读取和设置:不能拦截属性的新增和删除,所以Vue 2.x需要用Vue.set和Vue.delete来处理新增和删除的属性。
  2. 不能拦截数组索引的修改:不能拦截通过索引修改数组的操作,所以Vue 2.x重写了数组的方法,比如pushpopshift等等来触发更新。
  3. 需要递归遍历对象:需要递归遍历对象的所有属性转成getter/setter如果对象很大嵌套很深会有性能开销。
  4. 不能拦截Map、Set等数据结构:Object.defineProperty只能处理普通对象不能处理MapSet等ES6数据结构。

Vue 3.0的响应式原理

Vue 3.0用ES6的Proxy来实现响应式Proxy能拦截对象的各种操作包括读取设置删除枚举等等功能更强大。

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    // 收集依赖
    track(target, key);
    const result = Reflect.get(target, key, receiver);
    // 递归代理嵌套对象
    if (isObject(result)) {
      return reactive(result);
    }
    return result;
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    if (oldValue !== value) {
      // 触发更新
      trigger(target, key);
    }
    return result;
  },
  deleteProperty(target, key) {
    const hadKey = hasOwn(target, key);
    const result = Reflect.deleteProperty(target, key);
    if (result && hadKey) {
      // 触发更新
      trigger(target, key);
    }
    return result;
  }
});

Proxy的优势:

  1. 能拦截所有操作:能拦截属性的读取设置删除新增等等不需要Vue.set和,Vue.delete了。
  2. 能拦截数组索引的修改:能拦截通过索引修改数组的操作不需要重写数组方法了。
  3. 惰性递归:不需要一开始就递归遍历所有属性,只有访问到某个属性的时候,才递归代理嵌套对象性能更好。
  4. 支持Map、Set等数据结构:Proxy能处理MapSet等ES6数据结构响应式支持更全面。
  5. 性能更好:Proxy的性能比Object.defineProperty好特别是大对象的情况。

响应式API

Vue 3.0提供了新的响应式API:

  1. reactive:创建响应式对象类似Vue 2.x的data。
  2. ref:创建响应式引用值有value属性用来包装基本类型,或者任何值。
  3. computed:创建计算属性。
  4. watch:创建监听器。
  5. readonly:创建只读的响应式对象。

这些API是Composition API的基础能让我们更灵活地组织和,复用逻辑。

依赖收集和触发

Vue 3.0的依赖收集和触发机制和Vue 2.x类似,但是实现更高效。

用WeakMap来存储对象的依赖Map来存储对象每个属性的依赖Set来存储每个属性的effect(副作用函数,比如渲染函数computedwatch等等)。

targetMap (WeakMap)
  └── target (对象)
      └── depsMap (Map)
          └── key (属性名)
              └── dep (Set)
                  └── effect1, effect2, ...

当读取属性的时候,调用track把当前的effect加入对应属性的dep。当设置属性的时候,调用trigger遍历对应属性的dep执行所有的effect触发更新。

这种数据结构比Vue 2.x的更清晰也更高效,而且用WeakMap能自动垃圾回收不会内存泄漏。

三、虚拟DOM的重写

Vue 3.0另一个重大改进是虚拟DOM的重写性能更好。

Vue 2.x的虚拟DOM

Vue 2.x的虚拟DOM是每次重新渲染都重新创建整个VNode树,然后和旧的VNode树做diff比较差异更新DOM。

这种方式,虽然能工作,但是有一些性能问题:

  1. 全量diff:每次都要diff整个VNode树,即使,只有一小部分变化也要遍历整个树性能开销大。
  2. 创建VNode开销:每次重新渲染都要创建新的VNode对象有内存和,GC开销。
  3. 没有利用静态信息:没有利用编译时的静态信息来优化diff过程。

Vue 3.0的虚拟DOM

Vue 3.0的虚拟DOM做了很多优化:

  1. 基于块的优化:把模板分成多个块(block)每个块里有动态的节点diff的时候,只需要遍历块里的动态节点不用遍历整个树性能更好。
  1. 静态提升:把静态的节点提升到渲染函数外面只创建一次重新渲染的时候,复用不用重新创建减少VNode创建开销。
  1. PatchFlag优化:给动态的VNode加上PatchFlag标记这个节点有哪些动态的部分,比如文本classstyleprops等等diff的时候,只需要比较标记的部分不用全量比较性能更好。
  1. 缓存事件处理函数:把事件处理函数缓存起来不用每次重新创建减少子组件不必要的重新渲染。
  1. 更快的diff算法:重写了diff算法用双端比较和,最长递增子序列优化列表diff性能更好。

编译优化

Vue 3.0的编译器做了很多优化在编译模板的时候,就分析模板的静态和动态部分生成优化后的渲染函数。

比如模板:

<div>
  <span>静态文本</span>
  <span>{{ message }}</span>
  <div :class="active ? 'active' : ''">动态class</div>
</div>

编译后会生成类似这样的渲染函数:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "静态文本", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _createVNode("div", {
      class: _ctx.active ? 'active' : ''
    }, "动态class", 2 /* CLASS */)
  ]))
}

可以看到:

  1. 静态的span被提升到外面hoisted1只创建一次。
  2. 动态的span有PatchFlag1表示,只有文本是动态的。
  3. 动态的div有PatchFlag2表示,只有class是动态的。
  4. 整个div是一个blockdiff的时候,只需要遍历里面的动态节点。

这些编译优化能显著提升渲染性能特别是模板里静态内容多的情况。

四、Composition API

Vue 3.0另一个重要的新特性是Composition API提供更灵活的逻辑组织和,复用方式。

Options API的问题

Vue 2.x用Options API组件的逻辑分散在datamethodscomputedwatch等等选项里当组件复杂的时候,一个功能的逻辑会分散在各个选项里难以阅读和维护也难以复用。

比如一个组件有多个功能每个功能都有自己的datamethodscomputedwatch代码会分散在各个选项里要理解一个功能需要在各个选项之间,跳来跳去很麻烦。

而且逻辑复用也不方便Vue 2.x用mixins复用逻辑,但是mixins有命名冲突来源不清晰等等问题。

Composition API的优势

Composition API让我们能把一个功能的逻辑组织在,一起用函数的方式组织代码更清晰也更易复用。

比如:

import { ref, computed, onMounted } from 'vue'

function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  const update = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  return { x, y }
}

export default {
  setup() {
    const { x, y } = useMouse()
    return { x, y }
  }
}

可以看到鼠标位置的逻辑都在useMouse函数里组织在一起很清晰也能很方便地在,其他组件里复用不会有命名冲突的问题。

Composition API是Vue 3.0的一大亮点能让复杂组件的代码更清晰更易维护也能更好地复用逻辑。

五、其他改进

除了上面的改进Vue 3.0还有很多其他改进:

1. 更小的打包体积

Vue 3.0用TypeScript重写代码结构更模块化支持更好的tree-shaking按需引入功能打包体积更小。比如,如果你不用transitionkeep-alive等等功能就不会打包进去体积更小。

2. 更好的TypeScript支持

Vue 3.0用TypeScript重写原生支持TypeScript类型推导更好用TypeScript开发Vue应用更方便类型更安全。

3. 更好的JSX支持

Vue 3.0对JSX的支持更好能更方便地用JSX写渲染函数适合复杂的动态组件。

4. 自定义渲染器

Vue 3.0把核心和平台相关的代码分离能更方便地创建自定义渲染器,比如渲染到CanvasWebGL小程序等等跨平台能力更强。

5. Suspense

Vue 3.0支持Suspense组件能更方便地处理异步组件和,异步数据加载的状态类似React的Suspense。

6. Teleport(Portal)

Vue 3.0支持Teleport组件能把组件的DOM渲染到指定的位置,比如body适合弹窗对话框等等场景。

六、迁移注意事项

Vue 3.0虽然保持大部分兼容,但是也有一些不兼容的变化迁移的时候,要注意:

  1. Vue.set和Vue.delete不再需要:因为Proxy能拦截新增和删除,所以Vue.set和Vue.delete不再需要了直接操作对象就行。
  2. 数组方法不再被重写:Proxy能拦截数组索引的修改,所以数组方法不再被重写直接用原生方法就行。
  3. filters被移除:Vue 3.0移除了filters建议用methods或computed替代。
  4. 事件总线被移除:Vue 3.0移除了$on$off$once等等事件方法不能用Vue做事件总线了建议用第三方库,比如mitt。
  5. inline-template被移除:Vue 3.0移除了inline-template建议用默认插槽替代。
  6. data必须是函数:Vue 3.0组件的data必须是函数不能是对象了和Vue 2.x的规则一致,但是更严格。
  7. Props的default工厂函数不能访问this:Vue 3.0props的default工厂函数不能访问this了要用参数接收props。

迁移的时候,建议先用Vue 3.0的兼容版本,或者迁移工具检查不兼容的地方逐步迁移不要一次性全部改。

七、写在最后

以上就是Vue 3.0的前瞻原理剖析深入理解了Vue 3.0的底层机制。

Vue 3.0是Vue的一个重大版本有很多改进新的响应式系统基于Proxy新的虚拟DOM编译优化Composition API更小的体积更好的TypeScript支持等等性能更好开发体验更好。

虽然Vue 3.0还在开发中正式版还没发布,但是从已经公布的RFC和源码来看Vue 3.0非常值得期待会给我们带来更好的开发体验和性能。

建议大家提前了解Vue 3.0的新特性和原理为迁移做好准备等正式版发布后能快速上手。

当然也不要急于在生产环境用alpha或beta版本等正式版稳定后再迁移比较稳妥。

希望我的剖析能帮大家更好地理解Vue 3.0的原理和底层机制。

如果有什么问题,或者不同的看法欢迎在评论区留言我们一起交流。

最后用一句话结束这篇文章:"Vue 3.0是Vue的未来提前了解早做准备。"

愿Vue越来越好大家都能用好Vue开发出优秀的应用。