前言
jquery时代更新视图是直接对DOM进行操作,缺点是
频繁
操作真实 DOM,性能差。react和vue时代引入了虚拟DOM,更新视图是对新旧虚拟DOM树进行一层层的遍历比较,然后找出需要更新的DOM节点进行更新。这样做的缺点就是如果DOM树很复杂,在进行新旧DOM树比较的时候性能就比较差了。那么有没有一种方法是不需要去遍历新旧DOM树就可以知道哪些DOM需要更新呢?
答案是:在编译时我们就能够知道哪些节点是静态的,哪些是动态的。在更新视图时只需要对这些动态的节点进行靶向更新,就可以省去对比新旧虚拟DOM带来的开销。vue3也是这样做的,甚至都可以抛弃虚拟DOM。但是考虑到渲染函数的灵活性和需要兼容vue2,vue3最终还是保留了虚拟DOM。
这篇文章我们来讲讲vue3是如何找出动态节点,以及响应式变量修改后如何靶向更新。
注:本文使用的vue版本为
3.4.19
靶向更新的流程
先来看看我画的整个靶向更新的流程,如下图:
整个流程主要分为两个大阶段:编译时和运行时。
-
编译时阶段找出动态节点,使用
patchFlag
属性将其标记为动态节点。 -
运行时阶段分为两块:执行render函数阶段和更新视图阶段。
-
执行render函数阶段会找出所有被标记的动态节点,将其塞到
block
节点的
dynamicChildren
属性数组中。 -
更新视图阶段会从block节点的
dynamicChildren
属性数组中拿到所有的动态节点,然后遍历这个数组将里面的动态节点进行靶向更新。
-
一个简单的demo
还是同样的套路,我们通过debug一个demo,来搞清楚vue3是如何找出动态节点以及响应式变量修改后如何靶向更新的,demo代码如下:
<template>
<div>
<h1>title</h1>
<p>{{ msg }}</p>
<button @click="handleChange">change msg</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello");
function handleChange() {
msg.value = "world";
}
</script>
p标签绑定了响应式变量
msg
,点击button按钮时会将
msg
变量的值从hello更新为world。
在之前的文章中我们知道了vue分为编译时和运行时,由于p标签使用了
msg
响应式变量,所以在编译时就会找出p标签。并且将其标记为动态节点,而这里的h1标签由于没有使用响应式变量,所以不会被标记为动态节点。
在运行时阶段点击button按钮修改
msg
变量的值,由于我们在编译阶段已经将p标签标记为了动态节点,所以此时只需要将标记的p标签动态节点中的文本更新为最新的值即可,省去了传统patch函数中的比较新旧虚拟DOM的步骤。
编译阶段
在之前的
面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?
文章中我们讲过了在编译阶段对vue内置的指令、模版语法是在
transform
函数中处理的。在
transform
函数中实际干活的是一堆转换函数,每种转换函数都有不同的作用。比如v-for标签就是由
transformFor
转换函数处理的,而将节点标记为动态节点就是在
transformElement
转换函数中处理的。
首先我们需要启动一个
debug
终端,才可以在node端打断点。这里以vscode举例,首先我们需要打开终端,然后点击终端中的
+
号旁边的下拉箭头,在下拉中点击
Javascript Debug Terminal
就可以启动一个
debug
终端。
然后给
transformElement
函数打个断点,
transformElement
函数在
node_modules/@vue/compiler-core/dist/compiler-core.cjs.js
文件中。
transformElement
转换函数
transformElement
接着在
debug
终端中执行
yarn dev
(这里是以
vite
举例)。在浏览器中访问
http://localhost:5173/
,此时断点就会走到
transformElement
函数中了。我们看到
transformElement
函数中的代码是下面这样的:
const transformElement = (node, context) => {
return function postTransformElement() {
// ...
}
}
从上面可以看到
transformElement
函数中没有做任何事情,直接返回了一个名为
postTransformElement
的回调函数,我们接着给这个回调函数打上断点,将
transformElement
函数的断点给移除了。
每处理一个node节点都会走进一次
postTransformElement
函数这个断点,将断点放了,直到断点走进处理到使用响应式变量的p标签node节点时。在我们这个场景中简化后的
postTransformElement
函数代码如下:
const transformElement = (node, context) => {
return function postTransformElement() {
// 第一部分
let vnodePatchFlag;
let patchFlag = 0;
const child = node.children[0];
const type = child.type;
// 第二部分
const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION;
if (
hasDynamicTextChild &&
getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
patchFlag |= PatchFlags.TEXT;
}
if (patchFlag !== 0) {
vnodePatchFlag = String(patchFlag)
}
// 第三部分
node.codegenNode = createVNodeCall(
vnodePatchFlag
// ...省略
);
};
};
从上面可以看到简化后的
postTransformElement
函数主要分为三部分,其实很简单。
第一部分
第一部分很简单定义了
vnodePatchFlag
和
patchFlag
这两个变量,
patchFlag
变量的作用是标记节点是否为动态节点,
vnodePatchFlag
变量除了标记节点为动态节点之外还保存了一些额外的动态节点信息。
child
变量中存的是当前节点的子节点,
type
变量中存的是当前子节点的节点类型。
第二部分
const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION;
我们接着来看第二部分,其中的
hasDynamicTextChild
变量表示当前子节点是否为动态文本子节点,很明显我们这里的p标签使用了响应式变量
msg
,其文本子节点当然是动态的,所以
hasDynamicTextChild
变量的值为true。
接着我们来看第二部分的这段
if
语句:
if (
hasDynamicTextChild &&
getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
patchFlag |= PatchFlags.TEXT;
}
我们先来看这段if语句的条件,如果
hasDynamicTextChild
为true表示当前子节点是动态文本子节点。
getConstantType
函数是判断动态文本节点涉及到的变量是不是不会改变的常量,为什么判断了
hasDynamicTextChild
还要判断
getConstantType
呢?
答案是如果我们给p标签绑定一个不会改变的常量,因为确实绑定了变量,
hasDynamicTextChild
的值还是为true。但是由于我们绑定的是不会改变的常量,所以p标签中的文本节点永远都不会改变。比如下面这个demo:
<template>
<div>
<p>{{ count }}</p>
</div>
</template>
<script setup lang="ts">
const count = 10;
</script>
我们接着来看if语句里面的内容
patchFlag |= PatchFlags.TEXT
,如果if的判断结果为true,那么就使用“按位或”的运算符。由于此时的
patchFlag
变量的值为0,所以经过“按位或”的运算符计算下来
patchFlag
变量的值变成了
PatchFlags.TEXT
变量的值。我们先来看看
PatchFlags
中有哪些值:
enum PatchFlags {
TEXT = 1, // 二进制值为 1
CLASS = 1 << 1, // 二进制值为 10
STYLE = 1 << 2, // 二进制值为 100
// ...等等等
}
这里涉及到了位运算
<<
,他的意思是向左移多少位。比如
TEXT
表示向左移0位,二进制表示为1。
CLASS
表示为左移一位,二进制表示为10。
STYLE
表示为左移两位,二进制表示为100。
现在你明白了为什么给
patchFlag
赋值要使用“按位或”的运算符了吧,假如当前p标签除了有动态的文本节点,还有动态的class。那么
patchFlag
就会进行两次赋值,分别是:
patchFlag |= PatchFlags.TEXT
和
patchFlag |= PatchFlags.CLASS
。经过两次“按位或”的运算符进行计算后,
patchFlag
的二进制值就是11,二进制值信息中包含动态文本节点和动态class,从右边数的第一位1表示动态文本节点,从右边数的第二位1表示动态class。如下图:
这样设计其实很精妙,后面拿到动态节点进行更新时,只需要将动态节点的
patchFlag
和
PatchFlags
中的枚举进行
&
“按位与”运算就可以知道当前节点是否是动态文本节点、动态class的节点。上面之所以没有涉及到
PatchFlags.CLASS
相关的代码,是因为当前例子中不存在动态class,所以我省略了。
我们接着来看第二部分的第二个if语句,如下:
if (patchFlag !== 0) {
vnodePatchFlag = String(patchFlag)
}
这段代码很简单,如果
patchFlag !== 0
表示当前节点是动态节点。然后将
patchFlag
转换为字符串赋值给
vnodePatchFlag
变量,在dev环境中
vnodePatchFlag
字符串中还包含节点是哪种动态类型的信息。如下图:
第三部分
我们接着将断点走到第三部分,这一块也很简单。将
createVNodeCall
方法的返回值赋值给
codegenNode
属性,
codegenNode
属性中存的就是节点经过
transform
转换函数处理后的信息。
node.codegenNode = createVNodeCall(
vnodePatchFlag
// ...省略
);
我们将断点走到执行完
createVNodeCall
函数后,看看当前的p标签节点是什么样的。如下图:
从上图中可以看到此时的p标签的node节点中有了一个
patchFlag
属性,经过编译处理后p标签已经被标记成了动态节点。
执行
render
函数阶段
render
经过编译阶段的处理p标签已经被标记成了动态节点,并且生成了
render
函数。此时编译阶段的任务已经完了,该到浏览器中执行的运行时阶段了。首先我们要在浏览器中找到编译后的js文件。
其实很简单直接在network上面找到你的那个vue文件就行了,比如我这里的文件是
index.vue
,那我只需要在network上面找叫
index.vue
的文件就行了。但是需要注意一下network上面有两个
index.vue
的js请求,分别是template模块+script模块编译后的js文件,和style模块编译后的js文件。
那怎么区分这两个
index.vue
文件呢?很简单,通过query就可以区分。由style模块编译后的js文件的URL中有type=style的query,如下图所示:
接下来我们来看看编译后的
index.vue
,简化的代码如下:
import {
createElementBlock as _createElementBlock,
createElementVNode as _createElementVNode,
defineComponent as _defineComponent,
openBlock as _openBlock,
toDisplayString as _toDisplayString,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";
const _sfc_main = _defineComponent({
__name: "index",
setup(__props, { expose: __expose }) {
// ...省略
},
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_createElementVNode("h1", null, "title", -1),
_createElementVNode(
"p",
null,
_toDisplayString($setup.msg),
1
/* TEXT */
),
_createElementVNode(
"button",
{ onClick: $setup.handleChange },
"change msg"
),
])
);
}
_sfc_main.render = _sfc_render;
export default _sfc_main;
从上面的代码可以看到经过编译后生成了一个
render
函数,执行这个render函数就会生成虚拟DOM。仔细来看这个
render
函数的返回值结构,这里使用return返回了一个括号。在括号中有两项,分别是
openBlock
函数的返回值和
createElementBlock
函数的返回值。那么这里的return返回的到底是什么呢?
答案是会先执行
openBlock
函数,然后将
createElementBlock
函数执行后的值返回。
现在我们思考一个问题,在编译阶段我们只是将p标签标记成了动态节点,如果还有其他标签也是动态节点那么也会将其标记成动态节点。这些动态节点的标记还是在DOM树中的每个标签中,如果响应式变量的值改变,那么岂不还是需要去遍历DOM树?
答案是在执行render函数生成虚拟DOM的时候会生成一个block节点作为根节点,并且将这些标记的动态节点收集起来塞到block根节点的
dynamicChildren
属性数组中。在
dynamicChildren
属性数组中存的是平铺的DOM树中的所有动态节点,和动态节点在DOM树中的位置无关。
那么根block节点又是怎么收集到所有的动态子节点的呢?
我们先来搞清楚render函数中的那一堆嵌套函数的执行顺序,我们前面已经讲过了首先会执行返回的括号中的第一项
openBlock
函数,然后再执行括号中的第二项
createElementBlock
函数。
createElementBlock
函数是一个层层嵌套的结构,执行顺序是
内层先执行,外层再执行
。所以接下来会先执行里层
createElementVNode
生成h1标签的虚拟DOM,然后执行
createElementVNode
生成p标签的虚拟DOM,最后执行
createElementVNode
生成button标签的虚拟DOM。内层的函数执行完了后再去执行外层的
createElementBlock
生成div标签的虚拟DOM。如下图:
从上图中可以看到render函数中主要就执行了这三个函数:
-
openBlock
函数 -
createElementVNode
函数 -
createElementBlock
函数
openBlock
函数
openBlock
我们先来看最先执行的
openBlock
函数,在我们这个场景中简化后的代码如下:
let currentBlock;
function openBlock() {
currentBlock = [];
}
首先会定义一个全局变量
currentBlock
,里面会存DOM树中的所有的动态节点。在
openBlock
函数中会将其初始化为一个空数组,所以
openBlock
函数需要第一个执行。
createElementVNode
函数
createElementVNode
我们接着来看
createElementVNode
函数,在我们这个场景中简化后的代码如下:
export { createBaseVNode as createElementVNode };
function createBaseVNode() {
const vnode = {
// ...省略
};
if (vnode.patchFlag > 0) {
currentBlock.push(vnode);
}
return vnode;
}
createElementVNode
函数在内部其实叫
createBaseVNode
函数,从上面的代码中可以看到他除了会生成虚拟DOM之外,还会去判断当前节点是否为动态节点。如果是动态节点,那么就将其push到全局的
currentBlock
数组中。比如我们这里的p标签绑定了
msg
变量,当执行
createElementVNode
函数生成p标签的虚拟DOM时就会将p标签的node节点收集起来push到
currentBlock
数组中。
createElementBlock
函数
createElementBlock
我们来看最后执行的
createElementBlock
函数,在我们这个场景中简化后的代码如下:
function createElementBlock() {
return setupBlock(
createBaseVNode()
// ...省略
);
}
createElementBlock
函数会先执行
createBaseVNode
也就是上一步说的
createElementVNode
函数生成最外层div标签对应的虚拟DOM。由于外层div标签没有被标记为动态节点,所以执行
createElementVNode
函数也就只生成div标签的虚拟DOM。
然后将div标签的虚拟DOM作为参数去执行
setupBlock
函数,
setupBlock
函数的代码如下:
function setupBlock(vnode) {
vnode.dynamicChildren = currentBlock;
return vnode;
}
此时子节点生成虚拟DOM的
createElementVNode
函数全部都已经执行完了,这个div标签也就是我们的根节点,
我们前面讲过了执行顺序是
内层先执行,外层再执行
,所以执行到最外层的div标签时,子节点已经全部都执行完成了。此时
currentBlock
数组中已经存了所有的动态子节点,将
currentBlock
数组赋值给根block节点(这里是div节点)的
dynamicChildren
属性。
现在你知道我们前面提的那个问题,根block节点是怎么收集到所有的动态子节点的呢?
后续更新视图执行
patch
函数时只需要拿到根节点的
dynamicChildren
属性,就可以拿到DOM树中的所有动态子节点。
更新视图阶段
当响应式变量改变后,对应的视图就需要更新。对应我们这个场景中就是,点击button按钮后,p标签中的内容从原来的hello,更新为world。
按照传统的
patch
函数此时需要去遍历比较老的虚拟DOM和新的虚拟DOM,然后找出来p标签是需要修改的node节点,然后将其文本节点更新为最新值”world”。
但是我们在上一步生成虚拟DOM阶段已经将DOM树中所有的动态节点收集起来,存在了根block节点的
dynamicChildren
属性中。我们接着来看在新的
patch
函数中是如何读取
dynamicChildren
属性,以及如何将p标签的文本节点更新为最新值”world”。
处理div根节点
在source面板中找到vue源码中的
patch
函数,给
patch
函数打上断点。然后点击button按钮修改
msg
变量的值,导致render函数重新执行,接着会走进了
patch
函数进行视图更新。此时代码已经走到了
patch
函数的断点,在我们这个场景中简化后的
patch
函数代码如下:
const patch = (n1, n2) => {
processElement(n1, n2);
};
从上面可以看到简化后的
patch
函数中实际是调用了
processElement
函数,接着将断点走进
processElement
函数,在我们这个场景中简化后的
processElement
函数代码如下:
const processElement = (n1, n2) => {
patchElement(n1, n2);
};
从上面可以看到在
processElement
函数中依然不是具体实现视图更新的地方,在里面调用了
patchElement
函数。接着将断点走进
patchElement
函数,在我们这个场景中简化后的
patchElement
函数代码如下:
const patchElement = (n1, n2) => {
const el = (n2.el = n1.el);
let { patchFlag, dynamicChildren } = n2;
patchFlag = n1.patchFlag;
if (dynamicChildren) {
patchBlockChildren(n1.dynamicChildren, dynamicChildren);
}
if (patchFlag > 0) {
if (patchFlag & PatchFlags.CLASS) {
// 处理动态class
}
if (patchFlag & PatchFlags.STYLE) {
// 处理动态style
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children);
}
}
}
};
从上面可以看到
patchElement
函数是实际干活的地方了,我们在控制台中来看看接收n1、n2这两个参数是什么样的。
先来看看n1旧虚拟DOM ,如下图:
从上面可以看到此时的n1为根block节点,此时p标签中的文本还是更新前的文本”hello”,
dynamicChildren
属性为收集到的所有动态子节点。
接着来看n2新虚拟DOM,如下图:
从上面可以看到新虚拟DOM中p标签中的文本节点已经是更新后的文本”world”了。
我们接着来看
patchElement
函数中的代码,第一次处理div根节点时
patchElement
函数中只会执行部分代码。后面处理p标签时还会走进
patchElement
函数才会执行剩下的代码,当前执行的代码如下:
const patchElement = (n1, n2) => {
let { patchFlag, dynamicChildren } = n2;
if (dynamicChildren) {
patchBlockChildren(n1.dynamicChildren, dynamicChildren);
}
};
从根block节点(也就是n2新虚拟DOM)中拿到
dynamicChildren
。这个
dynamicChildren
数组我们前面讲过了,里面存的是DOM树中所有的动态节点。然后调用
patchBlockChildren
函数去处理所有的动态节点,我们将断点走进
patchBlockChildren
函数中,在我们这个场景中简化后的
patchBlockChildren
函数代码如下:
const patchBlockChildren = (oldChildren, newChildren) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i];
const newVNode = newChildren[i];
patch(oldVNode, newVNode);
}
};
在
patchBlockChildren
函数中会去遍历所有的动态子节点,在我们这个场景中,
oldVNode
也就是旧的p标签的node节点,
newVNode
是新的p标签的node节点。然后再去调用
patch
函数将这个p标签动态节点更新为最新的文本节点。
如果按照vue2传统的
patch
函数的流程,应该是进行遍历旧的
n1
虚拟DOM和新的
n2
虚拟DOM。然后才能找出p标签是需要更新的节点,接着执行上面的
patch(oldVNode, newVNode)
将p标签更新为最新的文本节点。
而在vue3中由于我们在编译阶段就找出来p标签是动态节点,然后将其收集到根block节点的
dynamicChildren
属性中。在更新阶段执行
patch
函数时,就省去了遍历比较新旧虚拟DOM的过程,直接从
dynamicChildren
属性中就可以将p标签取出来将其更新为最新的文本节点。
处理p标签节点
我们接着来看此时执行
patch(oldVNode, newVNode)
是如何处理p标签的。前面已经讲过了
patch
函数进行层层调用后实际干活的是
patchElement
函数,将断点走进
patchElement
函数。再来回忆一下前面讲的
patchElement
函数代码:
const patchElement = (n1, n2) => {
const el = (n2.el = n1.el);
let { patchFlag, dynamicChildren } = n2;
patchFlag = n1.patchFlag;
if (dynamicChildren) {
patchBlockChildren(n1.dynamicChildren, dynamicChildren);
}
if (patchFlag > 0) {
if (patchFlag & PatchFlags.CLASS) {
// 处理动态class
}
if (patchFlag & PatchFlags.STYLE) {
// 处理动态style
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children);
}
}
}
};
此时的n1就是p标签旧的虚拟DOM节点,n2就是p标签新的虚拟DOM节点。我们在编译时通过给p标签添加
patchFlag
属性将其标记为动态节点,并没有给p标签赋值
dynamicChildren
属性。所以此时不会像处理block根节点一样去执行
patchBlockChildren
函数了,而是会走后面的逻辑。
还记得我们前面讲的是如何给p标签设置
patchFlag
属性吗?
定义了一个
PatchFlags
枚举:
enum PatchFlags {
TEXT = 1, // 二进制值为 1
CLASS = 1 << 1, // 二进制值为 10
STYLE = 1 << 2, // 二进制值为 100
// ...等等等
}
由于一个节点可能同时是:动态文本节点、动态class节点、动态style节点。所以
patchFlag
中需要包含这些信息。
如果是动态文本节点,那就执行“按位或”运算符:
patchFlag |= PatchFlags.TEXT
。执行后
patchFlag
的二进制值为
1
如果也是动态class节点,在前一步的执行结果基础上再次执行“按位或”运算符:
patchFlag |= PatchFlags.CLASS
。执行后
patchFlag
的二进制值为
11
如果也是动态style节点,同样在前一步的执行结果基础上再次执行“按位或”运算符:
patchFlag |= PatchFlags.STYLE
。执行后
patchFlag
的二进制值为
111
我们前面给p标签标记为动态节点时给c。在
patchElement
函数中使用
patchFlag
属性进行”按位与”运算,判断当前节点是否是动态文本节点、动态class节点、动态style节点。
patchFlag
的值是1,转换为两位的二进制后是01。
PatchFlags.CLASS
为
1 << 1
,转换为二进制值为10。01和10进行&(按位与)操作,计算下来的值为00。所以
patchFlag & PatchFlags.CLASS
转换为布尔值后为false,说明当前p标签不是动态class标签。如下图:
同理将
patchFlag
转换为三位的二进制后是001。
PatchFlags.STYLE
为
1 << 2
,转换为二进制值为100。001和100进行&(按位与)操作,计算下来的值为000。所以
patchFlag & PatchFlags.CLASS
转换为布尔值后为false,说明当前p标签不是动态style标签。如下图:
同理将
patchFlag
转换为一位的二进制后还是1。
PatchFlags.TEXT
为1,转换为二进制值还是1。1和1进行&(按位与)操作,计算下来的值为1。所以
patchFlag & PatchFlags.TEXT
转换为布尔值后为true,说明当前p标签是动态文本标签。如下图:
判断到当前节点是动态文本节点,然后使用
n1.children !== n2.children
判断新旧文本是否相等。如果不相等就传入
el
和
n2.children
执行
hostSetElementText
函数,其中的
el
为当前p标签,
n2.children
为新的文本。我们来看看
hostSetElementText
函数的代码,如下:
function setElementText(el, text) {
el.textContent = text;
}
setElementText
函数中的
textContent
属性你可能用的比较少,他的作用和
innerText
差不多。给
textContent
属性赋值就是设置元素的文字内容,在这里就是将p标签的文本设置为最新值”world”。
至此也就实现了当响应式变量
msg
修改后,
靶向更新
p标签中的节点。
总结
现在来看我们最开始讲的整个靶向更新的流程图你应该很容易理解了,如下图:
整个流程主要分为两个大阶段:编译时和运行时。
-
编译时阶段找出动态节点,使用
patchFlag
属性将其标记为动态节点。 -
运行时阶段分为两块:执行render函数阶段和更新视图阶段。
-
执行render函数阶段会找出所有被标记的动态节点,将其塞到
block
节点的
dynamicChildren
属性数组中。 -
更新视图阶段会从block节点的
dynamicChildren
属性数组中拿到所有的动态节点,然后遍历这个数组将里面的动态节点进行靶向更新。
-
如果使用了
v-for
或者
v-if
这种会改变html结构的指令,那么就不只有根节点是block节点了。
v-for
和
v-if
的节点都会生成block节点,此时的这些block节点就组成了一颗block节点树。如果小伙伴们对使用了
v-for
或者
v-if
是如何实现靶向更新感兴趣,可以参考本文的debug方式去探索。又或者在评论区留言,我会在后面的文章中安排上。
在实验阶段的
Vue Vapor
中已经抛弃了虚拟DOM,更多关于
Vue Vapor
的内容可以查看我之前的文章:
没有虚拟DOM版本的vue(Vue Vapor)
。根据vue团队成员
三咲智子
所透露未来将使用
<script vapor>
的方式去区分Vapor组件和目前的组件。
关注(图1)公众号:【前端欧阳】,解锁我更多vue原理文章。
加我(图2)微信回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
未经允许不得转载:大白鲨游戏网 » vue3早已具备抛弃虚拟DOM的能力了