Vue的未来:Vapor模式

原文信息: 查看原文查看原文

The Future of Vue: Vapor Mode

- Timi Omoyeni

在不断变化的Web开发世界中,创新的Vue.js团队为我们带来了Vapor模式。这种模式优化了Vue的核心渲染过程,帮助我们的应用程序像轻烟一样运行,而不需要开发者自己深入复杂的优化。

在本文中,我们将探讨Vapor模式如何优雅地提高应用程序的效率,以及如何开始尝试使用它。但首先,让我们先弄清楚为什么首先开发了Vapor模式。


为什么是Vapor模式?

如果您之前使用过JavaScript框架,您可能熟悉虚拟DOM的概念。它涉及创建和更新DOM的虚拟表示,并将其存储在内存中以与实际DOM同步。由于更新VDOM比更新实际DOM更快,它为框架提供了相对廉价地进行必要更改的自由。

在Vue的情况下,其基于VDOM的渲染系统将我们<template>部分中的代码转换为实际的DOM节点。该系统还可以有效管理节点的更改,这些更改可以使用JavaScript函数、API调用等动态进行。

虽然VDOM提高了速度和性能,但更新DOM仍然需要遍历节点树并比较每个虚拟节点的属性以确保准确性。这个过程还包括为树的每个部分生成新的VNodes,无论是否有任何更改,这可能导致内存的不必要压力。

但在Vue中,引入了另一种方法来解决这个问题,称为“编译器感知的虚拟DOM”。

这是一种混合方法,引入了一些优化概念,帮助解决这个问题,包括:

  1. 静态提升
  2. 补丁标志

让我们更仔细地看看这些,以更清楚地了解Vue的渲染系统,这样我们就可以更好地理解Vapor模式带来了什么。


Vue中的静态提升

静态提升是一种技术,它自动从渲染函数中提取VNode创建,允许在多次重新渲染中重用VNodes。这种优化之所以有效,是因为这些VNodes随着时间的推移保持不变。

例如,给定此代码:

<div>
  <p class="vue">Vue.js很酷</p>
  <p class="solid">Solid.js也很酷</p>
  <p>同意吗?{{agree}}</p>
</div>

使用静态提升技术编译时,我们得到:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", { class: "vue" }, "Vue.js is Cool", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", { class: "solid" }, "Solid.js is also Cool", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _createElementVNode("p", null, "Agree?" + _toDisplayString(_ctx.agree), 1 /* TEXT */)
  ]))
}

在上面的示例中,您将看到两个变量:_hoisted_1_hoisted_2。它们包含将保持不变的静态代码,这些代码被提升或从渲染函数中提取出来,以避免重新处理非动态代码。

我们声明并重新渲染最后一个p标签中的元素,因为该元素包含一个动态变量,该变量可能随时更改。

值得注意的是,当有足够的连续静态元素时,它们会被合并为一个单独的静态Vnode(使用createStaticVNode),并传递给渲染函数。

让我们看一个例子:

<div>
  <p class="vue">Vue.js很酷</p>
  <p class="solid">Solid.js也很酷</p>
  <p class="vue">Vue.js很酷</p>
  <p class="solid">Solid.js也很酷</p>
  <p class="solid">React很酷</p>
  <p>{{agree}}</p>
</div>

编译后,我们得到:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p class=\"vue\">Vue.js is Cool</p><p class=\"solid\">Solid.js is also Cool</p><p class=\"vue\">Vue.js is Cool</p><p class=\"solid\">Solid.js is also Cool</p><p class=\"solid\">React is cool</p>", 5)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("p", null, _toDisplayString(_ctx.agree), 1 /* TEXT */)
  ]))
}

现在我们只有一个包含模板所有静态代码的hoisted常量。


Vue中的补丁标志

补丁标志允许Vue智能地更新DOM。它们用于识别具有动态绑定的元素所需的更新类型,例如classidvalue等。与可能重新渲染或检查所有内容的全面更新方法不同,它根据这些标志有选择地仅更新已更改的内容,而不会重新渲染整个组件或检查每个元素。

这不仅通过仅关注已更改的元素来加快更新过程,还避免了不必要的操作,例如调和未更改的元素的顺序。

这是通过在更新时将VNode传递给渲染函数来实现的。createElementVNode函数在其最后一个参数中接受一个数字。这个数字表示一个补丁标志,它指示在调用渲染函数时需要更新的动态绑定的类型。

让我们看看这在实际操作中是什么样子的,看看这段代码:

<div :class="{ active }"></div>

<input :id="id" :value="value" :placeholder="placeholder">

<div>{{ dynamic }}</div>

这里,我们有一个具有动态类activediv,一个具有动态idvalueplaceholderinput元素,以及另一个具有dynamic文本的div

当这段代码被编译时,我们得到:

import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("div", {
      class: _normalizeClass({ active: _ctx.active })
    }, null, 2 /* CLASS */),
    _createElementVNode("input", {
      id: _ctx.id,
      value: _ctx.value,
      placeholder: _ctx.placeholder
    }, null, 8 /* PROPS */, ["id", "value", "placeholder"]),
    _createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}

在这里,每个createElementVNode函数都接受一个数字作为其最后一个参数,该数字表示属性。第一个数字是2,表示类,8表示属性,64表示稳定的片段。您可以在GitHub上找到每个标志的完整列表。

通过这种方法,Vue可以比React和Svelte表现得更好,如下所示的图表。


Vapor模式的案例

尽管Vue的方法已经得到了改进,但仍然存在一些性能问题。这些问题包括不必要的内存使用、树差异以及VDOM的陷阱。

Vapor模式是为了解决这些问题而创建的。

Vapor模式是一种替代的编译策略,旨在通过将您的代码编译成更高效的JavaScript输出来提高您的Vue.js应用程序的性能,这种输出使用更少的内存,需要更少的运行时支持代码,并避免了上面说明的编译器感知VDOM方法的陷阱。

Vapor模式的一些好处包括:

  • 它是可选的,不会影响您现有的代码库。这意味着您可以立即开始使用Vapor模式来优化您的Vue 3应用程序的性能,而无需对您的代码进行任何更改。
  • 在您的应用程序中仅使用Vapor组件,您可以完全从捆绑包中删除VDOM运行时,从而减少基线运行时大小。

❕ Vapor模式将仅支持Composition API和<script setup>


Vue的Vapor模式如何工作

根据Vue(和Vite.js)的创建者Evan You的说法,Vapor模式的灵感来自Solid.js,这是一个用于创建用户界面的声明性JavaScript库,它采用了不同的编译和呈现节点的方法。

与使用虚拟DOM不同,它将模板编译为真实的DOM节点,并使用细粒度的反应进行更新。像Solid一样,Vue在其反应性系统中使用代理和基于读取的自动跟踪。

给定我们在上一个示例中的相同代码,使用Vapor模式,它会编译并给我们:

import { renderEffect as _renderEffect, setClass as _setClass, setDynamicProp as _setDynamicProp, setText as _setText, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
const t1 = _template("<input>")

export function render(_ctx) {
  const n0 = t0()
  const n1 = t1()
  const n2 = t0()
  _renderEffect(() => _setClass(n0, { active }))
  _renderEffect(() => _setDynamicProp(n1, "id", id))
  _renderEffect(() => _setDynamicProp(n1, "value", value))
  _renderEffect(() => _setDynamicProp(n1, "placeholder", placeholder))
  _renderEffect(() => _setText(n2, dynamic))
  return [n0, n1, n2]
}

在编译后的代码版本中,您将看到从vue/vapor包中导入的renderEffectsetClasssetDynamicPropsetTexttemplate

让我们看看每个函数的作用。

当然,以下是每个链接格式化为HTML以在新标签页中打开的文本:

  1. renderEffect:此函数负责监听类、属性和文本的更改,以确保在更新时对这些节点进行正确的更改。
  2. setClass:顾名思义,这个函数给节点元素分配一个类。它接受两个参数:一个element(或node)和一个它分配给元素的class
  3. setDynamicProp:这个函数用于设置元素上的动态属性。它需要三个参数:elementkeyvalue。这些用于确定每次调用此函数时分配或更新的适当值。
  4. setText:这个函数接受一个node和可能的值。它将给定的值设置为节点的textContent,同时还验证内容是否已修改。
  5. template:这个函数接受一个有效的HTML字符串并从中创建一个元素。在检查函数时,我们可以看到它使用基本的DOM操作方法。具体来说,document.createElement用于创建元素。然后使用innerHTML将元素的内容附加,该方法接受HTML字符串。

通过这些函数的组合,Vue可以将您的组件和应用程序编译成更快、更高效的代码,最终提高应用程序的性能和捆绑包大小。

为了让开发者熟悉Vapor模式,Vue团队发布了一个游乐场模板探索器

游乐场允许您比较启用Vapor模式和未启用Vapor模式时代码的编译版本。

在游乐场中,您可以检查代码的CSS、JS和SSR输出。它还允许您切换Vapor模式功能,以轻松比较输出的差异。

模板探索器类似于游乐场,但它只提供代码的JavaScript输出,还有一些选项,如SSR、模块等。


使用Vapor模式

根据Vapor Mode 仓库,以下是使用Vapor模式构建组件的示例:

<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUnmount,
  onUnmounted,
  ref,
} from 'vue/vapor'

const bar = ref('update')
const id = ref('id')
const p = ref<any>({
  bar,
  id: 'not id',
  test: 100,
})

function update() {
  bar.value = 'updated'
  p.value.foo = 'updated foo'
  p.value.newAttr = 'new attr'
  id.value = 'updated id'
}

function update2() {
  delete p.value.test
}

onBeforeMount(() => console.log('root: before mount'))
onMounted(() => console.log('root: mounted'))
onBeforeUnmount(() => console.log('root: before unmount'))
onUnmounted(() => console.log('root: unmounted'))
</script>

<template>
  <div>
    root comp
    <button @click="update">update</button>
    <button @click="update2">update2</button>
    <input :value="p.test" :placeholder="p.bar" :id="p.id" />
  </div>
</template>

与Vue开发者习惯的方式相比,注意我们如何从vue/vapor包中导入refonBeforeMountonMounted和其他函数。

这些函数都是Composition API的一部分,唯一的区别是它们现在从不依赖于VDOM的vapor包中导入。

这允许我们在应用程序中同时使用Vapor模式组件和非Vapor模式组件,而无需额外配置。


支持的功能

作为提高性能和减少基线运行时大小的努力的一部分,Vapor Mode将仅支持组合API,并且只能与<script setup>一起使用。

随着Vue团队工作的继续,我们将看到Vapor Mode支持的功能的更多示例,但有一点是明确的:Vapor Mode组件中支持的功能将与非Vapor模式组件的工作方式相同。

分享于 2024-05-09

访问量 197

预览图片