Vue 3新特性概览

截至目前,Vue.js 的新版本是 3.2。据 Vue.js 的作者表示,新的 Vue 3 编写的应用程序性能和运行效果非常好,相较于 Vue 2.x 版本,Vue 3 主要有以下几个方面大的改动以及提升:

  • 更快。

  • 更小。

  • 更易于维护。

本节主要对这些新的改动来做一下简单的概述,可能涉及 Vue 3 的新语法,各位读者如果看不懂,我们还会在后面的章节深入讲解的。

更快、更小、更易于维护

更快

更快主要体现在 Vue 3 在性能方面的提升,以及在源码层面的改动,主要包括以下方面:

  • 重构虚拟 DOM

  • 事件缓存。

  • 基于 Proxy 的响应式对象。

(1) 重构虚拟 DOM

Vue 3 重写了虚拟 DOM 的实现方法,使得初始渲染/更新可以提速达 100%,对于 Vue 2.x 版本的虚拟 DOM 来说,Vue 会遍历 <template> 模板中的所有内容,并根据这些标签生成对应的虚拟 DOM(虚拟 DOM 一般指采用 key/value 对象来保存标签元素的属性和内容),当有内容改变时,遍历虚拟 DOM 来找到变化前后不同的内容,我们称这个过程叫作 diff(different),并找到针对这些变化的内容所对应的 DOM 节点,并改变其内部属性。例如下面这段代码:

<template>
    <div class="content">
        <p>number1</p>
        <p>number2</p>
        <p>number3</p>
        <p>{{count}}</p>
    </div>
</template>

当触发响应式时,遍历所有的 <div> 标签和 <p> 标签,找到 {{count}} 变量对应的 <p> 标签的 DOM 节点,并改变其内容。对于那些纯静态 <p> 标签的节点进行 diff 其实是比较浪费资源的,当节点的数量很少时,表现并不明显,但是一旦节点的数量过大,在性能上就会慢很多。对此,Vue 3 在此基础上进行了优化,主要有:

  • 标记静态内容,并区分动态内容(静态提升)。

  • 更新时只 diff 动态的部分。

针对上面的代码,Vue 3 中首先会区分出 {{count}} 这部分动态的节点,在进行 diff 时,只针对这些节点进行,从而减少资源浪费,提升性能。

(2) 事件缓存

我们知道在 Vue 2.x 中,在绑定 DOM 事件时,例如 @click,这些事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化,然后每次触发都要重新生成全新的函数。在 Vue 3 中,提供了事件缓存对象 cacheHandlers,当 cacheHandlers 开启的时候,@click 绑定的事件会被标记成静态节点,被放入 cacheHandlers 中,这样在视图更新时也不会追踪,当事件再次触发时,就无须重新生成函数,直接调用缓存的事件回调方法即可,在事件处理方面提升了 Vue 的性能。

未开启 cacheHandlers 编译后的代码如下:

<div @click="hi">Hello World</div>
// 编译后
export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", { onClick: _ctx.hi }, "Hello World1", 8 /* PROPS */, ["onClick"]))
}

开启 cacheHandlers 编译后的代码如下:

<div @click="hi">Hello World</div>
// 编译后
export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.hi && _ctx.hi(...args)))}, "Hello World1"))
}

可以看到主要区别在于 onClick 那一行,直接从缓存中读取了回调函数。

(3) 基于 Proxy 的响应式对象

Vue 2.x 中,使用 Object.defineProperty() 来实现响应式对象,对于一些复杂的对象,需要循环递归地给每个属性增加 getter/setter 监听器,这使得组件的初始化非常耗时,而 Vue 3 中,引入了一种新的创建响应式对象的方法 reactive,其内部就是利用 ES 6Proxy API 来实现的,这样就可以不用针对每个属性来一一进行添加,以减少开销,提升性能。我们会在后续章节具体讲解 Vue 3 的响应式和 Proxy API

更小

更小主要体现在包所占容量的大小,我们知道,前端资源一般都属于静态资源,例如 JavaScript 文件、HTML 文件等,这些资源都托管在服务器上,用户在使用浏览器访问时,会将这些资源下载下来,所以精简文件包大小是提升页面性能的重要因素。Vue 3 在这方面可以让开发者打包构建出来的资源更小,从而提升性能。

Tree Shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code),就像一棵大树,将那些无用的叶子都剪掉。它依赖于 ES 6 模块语法的静态结构特性,例如 importexport,这个术语和概念在打包工具 RollupWebpack 中普及开来。例如下面这段 ES 6 代码:

import {get} from './api.js'

let doSome = ()=>{
    get()
}
doSome()

// api.js
let post = ()=>{
    console.log('post')
}
export post

let get = ()=>{
    console.log('get')
}
export get

上面的代码中,api.js 代码中的 post 方法相关内容是没有被引入和使用的,有了 Tree Shaking 之后,这部分内容是不会被打包的,这就在一定程度上减少了资源的大小。使用 Tree Shaking 的原理是引入了 ES 6 的模块静态分析,这就可以在编译时正确判断到底加载了什么代码,但是要注意 importexportES 6 原生的,而不是通过 Babel 或者 Webpack 转化的。

Vue 3 中,对代码结构进行了优化,让其更加符合 Tree Shaking 的结构,这样使用相关的 API 时,就不会把所有的都打包进来,只会打包用户用到的 API,例如:

<!-- vue 2.x -->
import Vue from 'vue'
new Vue()
Vue.nextTick(() => {})
const obj = Vue.observable({})

<!-- vue 3.x -->
import { nextTick, observable, createApp } from 'vue'
nextTick(() => {})
const obj = observable({})
createApp({})

同理,例如 <keep-alive><transition><teleport> 等内置组件,如果没有使用,也不会被打包到资源中。

更易于维护

  1. Flow 迁移到 TypeScript

    TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成,其通过 TypeScript 编译器或 Babel 转译为 JavaScript 代码,可运行在任何浏览器和操作系统上。TypeScript 引入了很多新的特性,例如类型监测、接口等,这些特性在框架源码的维护上有很大的提升。

    Vue 3 的源码结构层面,从 Flow 改成了 TypeScript 来编写,Flow 是一个静态类型检测器,有了它就可以在 JavaScript 运行前找出常见的变量类型的 bug,类似于 Java 语言中给变量强制指定类型,它的功能主要包括:

    • 自动类型转换。

    • null 引用。

    • 处理 undefined is not a function

    例如:

    // @flow
    function foo(x: number): number {
        return x + 10
    }
    foo('hi') // 参数x须为number类型,否则会报错 错误信息:
    '[flow] string (This type is incompatible with number See also: function call)'

    上面这段代码采用了 Flow 后,如果类型不对就会报错。一般来说,对于 JavaScript 源码框架,引入类型检测是非常重要的,不仅可以减少 bug 的产生,还可以规范一些接口的定义,这些特性和 TypeScript 非常吻合,所以在 Vue 3 中直接采用了 TypeScript 来进行重写,从源码层面来提升项目的可维护性。

  2. 源代码目录结构遵循 Monorepo

Monorepo 是一种管理代码的方式,它的核心观点是所有的项目在一个代码仓库中,但是代码分割到一个个小的模块中,而不是都放在 src 这个目录下。这样的分割,使得每个开发者大部分时间只是工作在少数的几个文件夹内,并且也只会编译自己负责的模块,不会导致一个 IDE 打不开太大的项目之类的事情,这样很多事情就简单了很多。Monorepo 的结构如图1-3所示。

image 2024 02 20 21 03 54 748
Figure 1. 图1-3 Monorepo的结构

目前很多大型的框架(例如 BabelReactAngularEmberMeteorJest 等)都采用了 Monorepo 这种方式来进行源码的管理,当然在自己的业务项目中,也可以使用 Monorepo 来管理代码。我们可以看一下 Vue.js 在采用 Monorepo 前后的源码结构对比,如图1-4所示。

image 2024 02 20 21 18 41 490
Figure 2. 图1-4 Vue 2.x源码目录结构(左)和Vue 3.x源码目录结构(右)

新特性初体验

组合式API

Vue 2.x 中,组件的主要逻辑是通过一些配置项来编写,包括一些内置的生命周期方法或者组件方法,例如下面的代码:

export default {
    name: 'test',
    components: {},
    props: {},
    data () {
        return {}
    },
    created () {},
    mounted () {},
    watch: {},
    methods: {}
}

上面的代码中,这些基于配置的组件写法称为 Options API(配置式 API),Vue 3 的一大核心新特性是引入了 Composition API(组合式 API),这使得组件的大部分内容都可以通过 setup() 方法进行配置。将上述代码改造成组合式 API,代码如下:

import {onMounted,reactive,watch} from 'vue'
export default {
    props: {
        name: String,
    },
    name: 'test',
    components: {},
    setup(props,ctx) {
        console.log(props.name)
        console.log('created')
        const data = reactive({
            a: 1
        })
        watch(
            () => data.a,
            (val, oldVal) => {
                console.log(val)
            }
        )
        onMounted(()=>{
        })
        const myMethod = (obj) =>{
        }
        retrun {
            data,
            myMethod
        }
    }
}

上面的代码采用了 ES 6 的语法,并且使用了 Vue 3Composition API 中的 setup 方法,可能读者有些看不懂,没关系,我们会在后续章节中具体讲解。

内置组件 Teleport、Suspense 和 Fragments 片段

<teleport><suspense> 都是 Vue 3 里面新增的内置组件,这里把内置组件称作可以直接写在 <template> 里面,而不需要格外引入的组件,例如 <keep-alive> 就是一个内置组件。而 Fragments 是一种新的特性,让开发者可以不用在 <template> 中强制包裹一个根元素,关于 <teleport><suspense> 的内容会在第 3 章深入讲解。

Vue 3 中,片段(Fragments)功能的引入允许组件返回多个根元素,而不需要包裹在一个单一的根元素中。这解决了 Vue 2 中一个常见的限制,即每个组件必须有一个单一的根元素。

Vue 2 中,如果你想在一个组件中返回多个根元素,必须包裹在一个单一的根元素中。例如:

<template>
  <div>
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
</template>

Vue 3 中,可以直接返回多个根元素,无需额外的包裹元素。比如,你可以这样写:

<template>
  <h1>Title</h1>
  <p>Paragraph</p>
</template>

这是因为 Vue 3 内部已经支持了片段,你可以在一个模板中直接包含多个根元素,而不需要用一个额外的元素包裹它们。

服务端渲染

在服务端渲染方面,Vue 3 优化了返回 HTML 字符串的逻辑。在 Vue 2.x 中,所有的节点(包括一些静态节点)在服务端返回时都会转换为虚拟 DOM,再转换成 HTML 字符串返回给浏览器;Vue 3 则将静态节点剥离成字符串,这部分内容不会转换成虚拟 DOM,而是直接拼接返回,在效率上进行了提升。

Vue 2.x 的服务端渲染代码如下:

<div>
    <div>abc</div>
    <div>abc</div>
    <div>abc</div>
    <div>{{msg}}</div>
</div>
// 编译后
function anonymous() {
    var _vm = this;
    var _h = _vm.$createElement;
    var _c = _vm._self._c || _h;
    return _c('div', [_vm._ssrNode(
    "<div>abc</div> <div>abc</div> <div>abc</div> <div>" + _vm._ssrEscape(
    _vm._s(_vm.msg)) + "</div>")])
}

Vue 3 的服务端渲染代码如下:

<div>
    <div>abc</div>
    <div>abc</div>
    <div>abc</div>
    <div>{{msg}}</div>
</div>
// 编译后
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
    const _cssVars = { style: { color: _ctx.color }}
    _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
    }><div>abc</div><div>abc</div><div>abc</div><div>${
    _ssrInterpolate(_ctx.msg)
    }</div></div>`)
}

Vite

伴随着 Vue 3Vue 团队也推出了自己的开发构建工具 Vite,可以在一定程度上取代 Vue Cliwebpack-dev-server 的功能。基于此,Vite 主要有以下特性:

  • 快速的冷启动。

  • 即时的模块热更新。

  • 真正的按需编译。

Vite 在开发环境下基于浏览器原生 ES 6 Modules 开发,在生产环境下基于 Rollup 打包,我们会在后续章节深入讲解 Vite 的相关使用。