解密Vue3.0的那些新特性

1,398 阅读13分钟

前言

尤大大早在2019年10月5日就开放了vue3的源码,时隔半年,于今年的4月17日发布了vue3.0的Beta版本,4月21日B站开直播,对vue3.0做分享总结,这一连串的操作,也算是兢兢业业,大写的优秀了。这篇文章重点来解密一下他在视频里所提及的一些新特性,包括性能方面的大幅度提升,Tree-shaking抖动,Fragments,Teleport,Suspense,自定义渲染API等,其中最重磅的莫过于Composition API了,虽然有那么点像React Hooks,但是气质上还是有很多不同的,下面会具体介绍。先附上Beta版源码地址:github.com/vuejs/vue-n…,供大家仔细研读。

Performance

Virtual Dom

相信使用过前端框架React的人,对虚拟Dom应该很熟悉了。它的核心思想就是抽象原生Dom,封装对应操作的API。其中的Diff算法算是核心难点。vue3.0这次优化主要就是在Diff算法上下了很多功夫。它是怎么来实现的呢?主要通过patchflag来标记不同类型的数据,来实现动态数据的动态更新,区分静态数据,这样只会在最开始的时候加载一次,后续编译过程中均直接略过,从而达到提升渲染速度的目的。效果是,upadate的性能提升了1.3~2倍,开启ssr的情况下可以提升2~3倍,所以在性能方面的提升还是很可观的。我们来看下一个代码示例:

代码

html

<div id="app">
  <span>{{ vue3 }}</span>
  <span>vue</span>
</div>

vue2的编译结果

function anonymous() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_ssrNode("<span>" + _ssrEscape(_s(vue3)) +
      "</span> <span>vue</span>")])
  }
}

每次vue3值变化的时候,所有的节点都会重新创建,显然性能是额外损耗的。

vue3的编译结果

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

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("span", null, "vue", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _createVNode("span", null, _toDisplayString(_ctx.vue3), 1 /* TEXT */),
    _hoisted_2
  ]))
}

我们注意到,vue3的动态节点,被标记了1,这个数字就是patchflag值,这里表示text类型。它的作用,主要是在之后的动态更新过程中,判断当前Dom节点是否有此标记,如果有,就update该节点,如果没有,就忽略不更新。如此,就能把性能损耗降到最低。尤其是在拥有大量静态节点的Dom结构里,性能提升会更显著。

patchflag标记

patchflag的值有很多,我们常用到的有text、props,组合类型和事件监听等。下面是代码里列举的:

// 这里注释仅供参考。
export const enum PatchFlags {
  TEXT = 1,// 表示具有动态textContent的元素
  CLASS = 1 << 1,  // 表示有动态Class的元素
  STYLE = 1 << 2,  // 表示动态样式(静态如style="color: red",也会提升至动态)
  PROPS = 1 << 3,  // 表示具有非类/样式动态道具的元素。
  FULL_PROPS = 1 << 4,  // 表示带有动态键的道具的元素,与上面三种相斥
  HYDRATE_EVENTS = 1 << 5,  // 表示带有事件监听器的元素
  STABLE_FRAGMENT = 1 << 6,   // 表示其子顺序不变的片段(没懂)。 
  KEYED_FRAGMENT = 1 << 7, // 表示带有键控或部分键控子元素的片段。
  UNKEYED_FRAGMENT = 1 << 8, // 表示带有无key绑定的片段
  NEED_PATCH = 1 << 9,   // 表示只需要非属性补丁的元素,例如ref或hooks
  DYNAMIC_SLOTS = 1 << 10,  // 表示具有动态插槽的元素
  // 特殊 FLAGS -------------------------------------------------------------
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作diff,只需检查 patchFlag === FLAG.
  BAIL = -2 // 一个特殊的标志,指代差异算法(没懂)
}

patchflag的用途:

  1. 用来标记当前节点需要更新,静态节点没有这个参数,更新的时候直接略过
  2. 组合更新,比如:1-只更新显示文本,8-只更新属性,9-更新显示文本和属性

在每次Dom更新的时候,只更新带有patchflag值的节点,没有的直接略过。而且,无论Dom节点层级有多深,都同样适用。这样更新只用关注变化的内容,而不用再去关心那些静态数据。这么做节省了更多的内存,提升更新渲染的速度。是不是很实用。

其中值得提的一点,叫组合patchflag值。这种类型使用的是位运算的原理。举个例子说明一下:

图中,我们可以看到文本和属性的组合值,在按位与之后,就得到了patchflag值为9。其他同理,不做赘述。

静态提升

代码

html

<div id="app">
  <span>移动</span>
  <span>端</span>
  <span>流量</span>
  <span>入口</span>
  <span>{{ vue3 }}</span>
  <span>5G</span>
</div>

编译结果

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

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("span", null, "移动", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("span", null, "端", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("span", null, "流量", -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createVNode("span", null, "入口", -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createVNode("span", null, "5G", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _hoisted_2,
    _hoisted_3,
    _hoisted_4,
    _hoisted_5,
    _createVNode("span", null, _toDisplayString(_ctx.vue3), 1 /* TEXT */),
    _hoisted_6
  ]))
}

我们看到所有的静态节点都被拿到了渲染函数之外,这样做的好处就是,静态节点只用被创建一次,在以后的渲染的时候拿来复用就行。可以极大的优化大型项目的内存占用,不用每次渲染都要重新创建。

事件监听缓存cacheHandlers

我们知道在vue2中,针对节点绑定的事件,每次触发都要重新生成全新的function去更新,而React目前也没有去缓存事件,具体为什么不去做,不得而知。在vue3中,就做了这么一件事情。当cacheHandlers开启的时候,编译会自动生成一个内联函数,将其变成一个静态节点,相当于React中的useCallback。并且本身就支持手写内联函数,这点就比较方便。我们来看下代码示例:

代码

html

<div id="app">
  <button @click="confirmHandler">确认</button>
  <span>{{ vue3 }}</span>
</div>

开启cacheHandlers

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

const _hoisted_1 = { id: "app" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.confirmHandler($event)))
    }, "确认"),
    _createVNode("span", null, _toDisplayString(_ctx.vue3), 1 /* TEXT */)
  ]))
}

关闭cacheHandlers

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

const _hoisted_1 = { id: "app" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _createVNode("button", { onClick: _ctx.confirmHandler }, "确认", 8 /* PROPS */, ["onClick"]),
    _createVNode("span", null, _toDisplayString(_ctx.vue3), 1 /* TEXT */)
  ]))
}

可以很清楚的看到其中的差别,开启cacheHandlers事件缓存之后,生成内联函数_cache,这样每次就不用重复渲染了,在事件更新频繁或者绑定事件过多的情况下,性能优化非常显著。

SSR

这个原理其实很简单,就是把所有的静态内容编译成一个字符串push到一个buffer里。

代码

html

<div id="app">
  <button @click="confirmHandler">确认</button>
  <span>{{ vue3 }}</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
  <span>vue</span>
</div>

编译结果

import { ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent) {
  _push(`<div id="app"><button>确认</button><span>${_ssrInterpolate(_ctx.vue3)}</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span><span>vue</span></div>`)
}

是不是很神奇,我们看到所有的静态节点span全部被编译成了一个字符串,甚至里面的动态节点也被尽可能的字符化。这样做至少可以节省一半以上内存,总体渲染速度加快一倍以上。SSR服务端渲染在性能上,比单纯的抽成Virtual Dom去渲染,快的不是一个量级,两者完全不在一个层面上。

这里要注意一点的是,如果静态节点数量很多,超过了一个阈值,vue3会单独创建一个div,将静态节点innerHTML到该div里去。

Tree-shaking

这里有很多解释,我更偏向于理解成,把没用的树叶”抖掉“这个比喻。

换句话说,就是按需引入我们需要的模块。对于用不到的模块就不会打包。

代码

html

<div id="app">
  <input type="checkbox" v-model="vue3" />
</div>

编译结果

import { vModelCheckbox as _vModelCheckbox, createVNode as _createVNode, withDirectives as _withDirectives, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _withDirectives(_createVNode("input", {
      type: "checkbox",
      "onUpdate:modelValue": $event => (_ctx.vue3 = $event)
    }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
      [_vModelCheckbox, _ctx.vue3]
    ])
  ]))
}

这里在编译阶段,仅仅引入了vModelCheckbox一个Dom相关模块,其他模块都是通用的必须要引入的。这样打出的包会体积更小,速度更快,性能更好。这一点在vue2里是没有做区分的。

Composition API

终于到了vue3.0版本里最重磅的内容。与React Hooks类似,只是实现方式,整体气质不同。

拥有以下特性

  • 兼容现有的Options API,无缝衔接
  • 更加灵活,组合更方便,复用性更强
  • 可以跟其他框架搭配使用

我们知道,vue3采用RFC来讨论所有的API的实现,并且把所有讨论的细节都呈现给使用者,我觉得这点是非常棒的事情。可以更友好的让开发者理解和学习API的使用,附上文档学习地址:composition-api.vuejs.org/#summary

那么,为什么vue3必须要有Composition API这样的东西呢?下面我们看下几个场景,你就能理解Composition API的强大了。

Mixin

图中我们可以看到在解决API复用上,Mixin是可以达到的。但是它最要命的就是命名空间冲突的问题,这个缺陷可以导致代码的不可维护,这点对于开发者是不能接受的,甚至毁灭性的。

Scoped Slots

Scoped Slots虽然可以解决Mixin的问题,但是它的问题就是配置太多,逻辑也太多,这样就导致它的复用性和灵活性极差,还不如Mixin。

最后,我们来看看Composition API是怎么实现的。

Composition Functions

是不是很棒,使用了我们熟悉的函数式编程,结构非常清晰,代码量也减少了很多,最重要的是灵活性和复用性很友好。也不用担心会命名冲突的问题。

组件中的使用

图中不同颜色代表不同逻辑的业务代码,相同颜色表示同一业务需要的代码。我们在写一个比较复杂的组件的时候,有没有这种体会。组件里面包含很多个小的功能组件,这些功能可能在其他页面也有用到,但是数据强依赖这个组件的接口。所以只能分开,单独写到这个组件里,导致组件代码又臭又长,还没法复用。遇到这种情况,我们也只能望洋兴叹。在vue3.0出来之后,我们就可以完美的解决这种问题。我们可以任意在同一组件里,写不同的业务逻辑,不仅层次分明,逻辑清楚,而且神奇的是,你可以信手拈来,拿去复用在其他组件里。你可能也不再需要为了一个页面,去封装十几个小组件了。费神费力,还容易出错。

使用Composition API写出的代码,后期筛查问题、维护和update上,体验也是极度舒适的。

当然,我们不用担心之前的Options API不能用,vue3做了兼容,可以一起使用的。建议有了它就不要再考虑使用其他方案了。

具体使用方法可以查看文档:composition-api.vuejs.org/#summary

Fragments

碎片,这个我理解是不再限制组件中必须提供根节点。你可以这么写

<div>vue3</div>
text

也可以这么写

vue3

其实看编译的代码,你会发现,vue3只是帮你生成了一个根节点来包裹。这么做的好处就是你省事了,腾出手来做你喜欢做的,vue3就像你的爱人一样,默默地帮你做了许多。

Teleport

这个特性,我理解相当于”占位“。看下代码

<!-- 占位,专门用来显示定制内容 -->
<div id="modal-container"></div>

<!-- 父页面 -->
<div id="app"></div>

<template>
  <div>
    ...
    
    <!-- 调用Teleport -->
    <Teleport to="#modal-container">
        <div v-show="isPopUpOpen">
          <p>确定删除?</p>
          <button @click="removeUser">确定</button>
          <button @click="isPopUpOpen = false">取消</button>
        </div>
    </Teleport>
    
  </div>
</template>

这里,id是modal-container的模块会在根页面里占位,在调用的时候插入模块。这么做可以避免由于z-index层级问题导致的显示异常。

我认为这个特性不仅仅这么简单,其他应用场景有待挖掘。

Suspense

翻译为,”悬念“。我理解就是异步加载依赖。

用法

  • 可在嵌套层级中等待嵌套的异步依赖项
  • 支持 async setup()
  • 支持异步组件

代码示例

// vue2实现
<template>
    <div>
        <div v-if="!loading">
            ...
        </div>
        <div v-if="loading">Loading...</div>
    </div>
</template>

// vue3实现
<Suspense>
  <template>
    <Suspended-component />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

这里实现一个过渡效果,使用Suspense实现,更简单直接一些。

这个特性的应用场景,也可能是图片或者模块的顺序加载。对于业务逻辑比较复杂,需要异步加载静态节点的时候,可以考虑使用。

Custom Renderer API

自定义渲染API这块,意味着以后我们可以通过 vue ,实现用 dom 编程的方式来进行 webgl 编程。

其他的一些变化

// 周期函数变化

被替换

beforeCreate -> setup()
created -> setup()

重命名

beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured

新增的

新增的以下2个方便调试 debug 的回调钩子:

onRenderTracked
onRenderTriggered

// 对应的自定义指令变化

// vue2
const MyDirective = {
  bind(el, binding, vnode, prevVnode) {},
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {}
}

// vue3
const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {}
}

更好的TypeScript支持

vue3.0版本里,采用了TypeScript重写,意味着之后会全面拥抱TypeScript。类型提示、检测都更加的友好强大。废弃了Class,如果你还是喜欢Class的用法,可以安装单独库vue-class-component支持。

结语

以上对vue3主要的新特性做了简单的总结,也加入了一些个人的理解,目前还不能放手使用,只能算尝鲜阶段,所以如果想更深入了解,要仔细研读下vue3的源码。尤大大在直播问答环节,表示可能需要年终才会发布正式版本,所以还有一段时间来完善框架,让vue3更饱满的呈现给众人。后续还需要做的工作主要包括,文档编写,完善开发者工具,以及自动化迁移工具等。有大量的工作要做,是为了给vue2升级vue3做准备。尤大大在直播中也提及,未来会有一个过渡版本发布,这也是最后一个vue2版本vue2.7,计划在不损坏兼容性的前提下,加入一些好的vue3的功能,把它们提前合并入2.7版本,提供给开发者使用体验。而在这之后18个月,表示只会进行安全性的描述,不在继续维护。

而关于vue2升级到vue3,作者也友情提示大家,对于业务逻辑偏重的项目,请一定酌情考虑升级,自觉规避风险,言下之意就是说在全面拥抱vue3之前,可能你需要做一些取舍。

最后,我猜想,Composition API是有极大可能提前加入到2.7版本,提前让大家体验,那就让我们敬请期待吧。