前言
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
轉換函式
接著在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
函式階段
經過編譯階段的處理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
函式,在我們這個場景中簡化後的程式碼如下:
let currentBlock;
function openBlock() {
currentBlock = [];
}
首先會定義一個全域性變數currentBlock
,裡面會存DOM樹中的所有的動態節點。在openBlock
函式中會將其初始化為一個空陣列,所以openBlock
函式需要第一個執行。
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
函式,在我們這個場景中簡化後的程式碼如下:
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有深入理解的人。