Vue.js是一种被广泛使用的JavaScript框架,用于构建用户界面和单页面应用。Vue3是其最新的主要版本,引入了许多新特性并做了一些改进。
Vue2 和 Vue3 在数据响应性系统的实现上采用了不同的方式:Vue2 使用 Object.defineProperty,而 Vue3 则选择了 Proxy。
在 Vue 2 中,数据响应性系统的核心是通过 Object.defineProperty 来实现的。当我们在Vue组件中定义 data 函数返回的对象时,Vue 会遍历这个对象的每个属性,并使用 Object.defineProperty 将它们转变为 getter/setter。这使得 Vue 可以在访问或修改这些属性时追踪依赖关系和发出更改通知。
然而,Object.defineProperty 有一些限制:
它无法检测到对象属性的添加或删除。这意味着如果在创建Vue组件后动态添加新的根级数据属性,Vue 将无法追踪这个属性的更改。
它无法检测数组的变动,除非使用 Vue.js 指定的方法(如 push,pop 和 Vue.set 等)。
在 Vue3 中,数据响应性系统转向使用 ES6 中的 Proxy 对象。Proxy 对象能够在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。
Proxy 的优势包括:
Proxy 可以拦截对象的任何操作,这包括属性的添加和删除,这样就解决了 Object.defineProperty 的主要限制。
Proxy 可以直接处理数组的变动,而无需使用特殊方法。
但是,Proxy 的主要问题是它不能在低版本的 JavaScript 环境中被 polyfill。这意味着 Vue3 无法在不支持 Proxy 的旧浏览器(如IE11)中运行,除非使用兼容性构建版本,但那将带来额外的性能和包大小成本。
虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vDom 树。
传统 vDom 的性能与模板大小正相关,与动态节点的数量无关。在一些组件整个模板内只有少量动态节点的情况下,这些遍历都是性能的浪费。
根本原因在于,JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足。
静态树提升:在 Vue3 中,编译器可以检测到模板中的静态内容(即不会改变的内容),并将其提升出虚拟DOM渲染函数,这样在重新渲染时就无需再次处理静态内容。这大大提高了渲染性能。
静态属性提升:类似地,Vue3 编译器还可以将静态的根级别节点属性提升出渲染函数,这样在diffing过程中就无需再次处理这些属性。
块级别的渲染:Vue3 的新编译策略将模板中的代码分割为独立的“块”,每个块都有自己的更新函数。当状态改变时,Vue3 可以更精确地知道哪个块需要更新,从而减少不必要的渲染工作。
更好的事件处理:在 Vue3 中,事件监听器绑定在组件的根元素上,而不是在每个元素上。这样可以减少事件监听器的数量,提高性能。
Fragments:Vue3 支持 Fragments,这意味着组件可以有多个根节点,这在虚拟DOM中提供了更多灵活性。
新策略将 vDom 更新性能由与模板整体大小相关提升为与动态内容的数量相关。
官方的测试中,Vue3 的更新效率是 Vue2 同等量级的6倍。
在 Vue2 中,插槽内容是在父组件的上下文中编译的。然而,这种方法有一些限制,比如你不能直接访问子组件的数据和方法,除非子组件将它们作为插槽的 props 提供。
在 Vue3 中,插槽的行为发生了改变。所有的插槽现在默认会被编译为函数。这种新的插槽语法被称为“编译时插槽”,它可以改进性能,因为插槽的内容只在需要的时候才会被求值和渲染。
此外,这种新的插槽语法还解决了 Vue2 中的一些限制。例如,现在我们可以在插槽内容中直接访问子组件的数据和方法,就像它们是在子组件的上下文中被编译的一样。这让我们可以更灵活地定制插槽的内容。
然而,这种新的插槽语法也可能引入一些新的问题,比如作用域混淆。为了解决这个问题,Vue3 推荐在模板中明确地标记插槽的 props,以区分父组件和子组件的作用域。
monomorphic vnode factory 是 Vue 3 在虚拟 DOM 系统中的一种优化策略,它通过生成具有相同形状的虚拟节点来提高性能和内存使用效率。
Vue3 对虚拟节点工厂函数的处理做了优化,通过引入monomorphic vnode factory。简单地说,这种工厂函数生成的虚拟节点始终具有相同的形状(或称为类型)。这使得 JavaScript 引擎可以更有效地优化这些函数,从而提高应用的性能。
此外,这种优化还使得虚拟节点的内存占用更少,因为相同形状的虚拟节点可以共享一些内部数据结构。
在 Vue3 中,编译器生成的标志(flags)用于表示虚拟节点或其子节点的类型。这些标志在编译时生成,它们提供了有关虚拟节点结构的信息,使得渲染引擎能够更有效地处理虚拟节点。
先来介绍一个 Vue3 的模板编译器工具,我们可以在左侧写 Vue3 的模板,会将它实时编译成一个渲染函数,接下来我们要做的就是侦测渲染函数的改变。
来看一个简单的案例,根节点为一个 div,有两个子节点,其中一个子节点的内容为静态的,另一个为动态的。
hello world
{{ message }}
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
// Check the console for the AST
来看渲染函数,Vue2 中我们使用 createElement 来返回子节点,现在我们使用 createElementBlock 来返回一个区块。第一个参数代表当前组件的根节点,第二个参数看它有没有属性,没有就设置为 null。第三个参数为一个数组,内容为当前根节点下的子节点。
第一个子节点 span,_createElementVNode("span", null, "hello world"),第一个参数代表标签类型为 span,第二个参数代表没有属性,第三个参数代表内容只有文本节点 "hello world"。
第二个子节点是一个带有动态绑定的 span,_createElementVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */),这里有一个数字1,这个数字1就是一个 flag,编译时生成的标记。标记不是只有1,可以是1~9,这些标记统一称为 patchFlags。
在这个示例中,div 是一个区块,在这个区块中,只有标记了 patchFlag 的 vNode 才会被真正的追踪。后续 message 的值发生了更新,就会直接找到 div 这个区块,跳转到它动态有 flag 标记的 vNode 上。接下来通过 flag 就可以清楚的知道下面要去比较什么内容,什么内容为动态的。这里 flag 倍设为1,我们就知道当前 span 里面的文本内容可能会发生改变,因为文本内容的 flag 标记就为1。在这个 span 中可能还会有一些其他内容和子节点,这些其他内容这里就不管,这里只需要知道被标记为1的这个文本内容可能会改变,就可以了。
接下来我们再添加一些静态节点,如下:
在 Vue2 的 diff 算法中,会对这个组件树逐层逐级比较每一个子节点,会一个一个的比较这些 span 的内容有没有变化。
在 Vue3 中,只需要找到 div 这个区块,然后逐一比较这个区块下的动态节点即可,这样就会节省很多更新消耗的时间。
之前我们很害怕一个动态内容被嵌套的非常深,从根节点开始逐层比对会很消耗时间的。
我们再更改一下代码结构:
这里可以看到,虽然我们在 span 标签外层嵌套了3层父节点,我们也会直接找到根 div 所在的区块,找到它对应的动态节点。
在 Vue3 中,所有的动态节点都和其所在的区块绑定了,当内容发生变更时,会直接找到它对应的区块,然后找到区块中动态改变的节点,而不会像 Vue2 一样也会将很多静态不会变化的节点也遍历一遍。
这就是 Vue3 虚拟 Dom 优化中最关键的优化,此外还有一些对节点属性的优化。
我们在 span 上定义了一个属性 id,可以看到 patchFlag 并没有变化,这个 id 属性也只在渲染时渲染一次,后续就不会变化了。我们如果把 id 设为动态属性会怎么样呢?
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", { id: _ctx.box }, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, ["id"]),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world")
]))
}
// Check the console for the AST
可以看到,patchFlag 从1变成了9,代表在这个节点上不光文本内容会发生变化,属性也会发生变化,还加了一条注释表示 id 属性会发生变化。
我们在加一个静态的 class 属性 box1:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", {
id: _ctx.box,
class: "box1"
}, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, ["id"]),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world")
]))
}
// Check the console for the AST
可以看到 class 属性只是加到渲染属性中,并没有加入到后面的注释中,所以在 diff 时只会检查这个 span 的文本和 id 有没有变化。
这套体系就会保证我们在更新时只会关注那些真正会变的内容,这样就跳出了虚拟 Dom 更新时的性能瓶颈。
至于这些功能是如何实现的,我们可以点击 Options,选中 hoistStatic 选项,就会出现如下内容:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_2 = ["id"]
const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", {
id: _ctx.box,
class: "box1"
}, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, _hoisted_2),
_hoisted_3,
_hoisted_4,
_hoisted_5,
_hoisted_6,
_hoisted_7
]))
}
// Check the console for the AST
可以看到,Vue 将静态内容都提到渲染函数外面,这些静态内容只会在开始时渲染一次。后续改变时div 这个区块下静态内容直接使用外面定义的,只会检查文本和 id 属性这2个动态内容。
Vue3 引入了一个全新的 API,称为 Composition API,即组合式 API。这个 API 的设计目的是解决在大型应用中管理和复用逻辑的问题。
在 Vue2 中,我们通常通过选项式 API(Options API)来组织代码,即在 Vue 组件中定义各种选项,如 data,methods,computed,watch 等。但是这种方式在处理大型复杂组件时会变得困难,因为相关的代码可能会分布在不同的选项中,使得代码难以理解和维护。此外,逻辑复用也有一些限制,尽管 Vue 提供了 mixins 和 scoped slots,但它们都有一些缺点和限制。
组合式 API 提供了一种新的方式来组织和复用代码。我们可以把组件的逻辑写成一个一个的 composition function,然后在需要的地方调用这些函数。这使得我们可以更方便地把逻辑集中在一起,也更方便地在不同的组件中复用逻辑。一个组合式 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));
// lifecircle
onMounted(() => console.log('onMounted));
// 暴露给模板或渲染函数
return {
count
};
}
};
组合式 API 并没有替代原有的选项式 API,而是作为一个可选的补充。你可以根据需要和喜好选择使用选项式 API 或者组合式 API,或者两者结合。
Vue3 的组合式 API提供了更好的 TypeScript 类型推断支持,主要基于以下几个原因:
在 Vue2 中,如果你想在不同的组件之间复用逻辑,你可能会使用 mixins、高阶组件 和作用域插槽的方式。但是这些技术在 TypeScript 中的类型推断并不理想,因为它们的行为可能会导致命名冲突、类型冲突、类型丢失、数据来源不清晰的问题。
相反,组合式 API允许我们使用普通的 JavaScript 函数来复用逻辑,这些函数有很好的类型推断支持。
Tree shaking 是一种在打包时去除无用代码的优化技术。在 JavaScript 模块中,如果某些导出的函数或变量没有被其他模块使用,那么在打包时,这些未使用的代码可以被移除,从而减少最终产物的体积。
Vue3 的组合式 API 更好地支持 tree shaking,主要是因为它的设计方式更接近于标准的JavaScript 模块。在组合式 API 中,我们可以将代码组织为多个函数,每个函数负责一部分独立的逻辑。然后我们就可以按需导入和使用这些函数,而不是使用一个大的 “Vue” 对象。这意味着,如果我们没有使用某个函数,那么它就可以在打包时被移除。
相比之下,Vue2 的选项式 API 更依赖于 Vue 的全局 API 和实例方法。例如,你可能会在组件的methods 或 computed 属性中使用 this.$set 或 this.$emit。这些方法是 Vue 实例的一部分,不能被单独地导入和使用,因此它们也不能被 tree shaking 优化。
还有一点,基于函数的 API 写的代码有更好的压缩性,因为所有函数名包括函数中定义的变量名都能被压缩,而对象中的属性或方法名并不可以。因此,使用组合式 API 打包出来的代码体积会更小。
(1) ref & reactive
(2) 新的生命周期函数
(3) computed 和 watch
(4) Hooks
(1) Tleport
(2) Suspense
本文不会具体讲解些新特性,会在后面的文章中讲到。
也就是允许我们以全局变量的形式把这些变量应用到 Vue 的模板中。
可以使用 "ref:" 的语法使使用 ref 的方式更方便。
如果我们想让一种原始类型的值具有响应性,那就可以将其作为参数传给 ref,就会返回一个包装对象,可以使用这个对象的 value 属性进行取值。
每次使用 .value 取值还是比较麻烦的,我们可以使用 "ref:" 这个语法糖的形式定义变量,类似于 const 和 let 这种声明符。
ref: count = 1;
function inc() {
count++;
}
接下来就可以直接使用 count 变量了,编译之后就是下面的代码。
const count = ref(1);
function inc() {
count.value++;
}
使用