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 6
的 Proxy API
来实现的,这样就可以不用针对每个属性来一一进行添加,以减少开销,提升性能。我们会在后续章节具体讲解 Vue 3
的响应式和 Proxy API
。
更小
更小主要体现在包所占容量的大小,我们知道,前端资源一般都属于静态资源,例如 JavaScript
文件、HTML
文件等,这些资源都托管在服务器上,用户在使用浏览器访问时,会将这些资源下载下来,所以精简文件包大小是提升页面性能的重要因素。Vue 3
在这方面可以让开发者打包构建出来的资源更小,从而提升性能。
Tree Shaking
是一个术语,通常用于描述移除 JavaScript
上下文中的未引用代码(dead-code
),就像一棵大树,将那些无用的叶子都剪掉。它依赖于 ES 6
模块语法的静态结构特性,例如 import
和 export
,这个术语和概念在打包工具 Rollup
和 Webpack
中普及开来。例如下面这段 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
的模块静态分析,这就可以在编译时正确判断到底加载了什么代码,但是要注意 import
和 export
是 ES 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>
等内置组件,如果没有使用,也不会被打包到资源中。
更易于维护
-
从
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
来进行重写,从源码层面来提升项目的可维护性。 -
-
源代码目录结构遵循 Monorepo
Monorepo
是一种管理代码的方式,它的核心观点是所有的项目在一个代码仓库中,但是代码分割到一个个小的模块中,而不是都放在 src
这个目录下。这样的分割,使得每个开发者大部分时间只是工作在少数的几个文件夹内,并且也只会编译自己负责的模块,不会导致一个 IDE 打不开太大的项目之类的事情,这样很多事情就简单了很多。Monorepo
的结构如图1-3所示。

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

新特性初体验
组合式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 3
的 Composition API
中的 setup
方法,可能读者有些看不懂,没关系,我们会在后续章节中具体讲解。
内置组件 Teleport、Suspense 和 Fragments 片段
<teleport>
和 <suspense>
都是 Vue 3
里面新增的内置组件,这里把内置组件称作可以直接写在 <template>
里面,而不需要格外引入的组件,例如 <keep-alive>
就是一个内置组件。而 Fragments
是一种新的特性,让开发者可以不用在 <template>
中强制包裹一个根元素,关于 <teleport>
和 <suspense>
的内容会在第 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>`)
}