前言
最近有粉絲找到我,說被面試官給問懵了。
-
粉絲:面試官上來就問“一個vue文件是如何渲染成瀏覽器上面的真實(shí)DOM?”,當(dāng)時還挺竊喜這題真簡單。就簡單說了一下先是編譯成render函數(shù)、然后根據(jù)render函數(shù)生成虛擬DOM,最后就是根據(jù)虛擬DOM生成真實(shí)DOM。按照正常套路面試官接著會問vue響應(yīng)式原理和diff算法,結(jié)果面試官不講武德問了我“那render函數(shù)又是怎么生成的呢?”。
-
我:之前寫過一篇 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章專門講過這個吖。
-
粉絲:我就是按照你文章回答的面試官,底層其實(shí)是調(diào)用的一個叫
baseCompile
的函數(shù)。在baseCompile
函數(shù)中主要有三部分,執(zhí)行baseParse
函數(shù)將template模版轉(zhuǎn)換成模版AST抽象語法樹
,接著執(zhí)行transform
函數(shù)處理掉vue內(nèi)置的指令和語法糖就可以得到javascript AST抽象語法樹
,最后就是執(zhí)行generate
函數(shù)遞歸遍歷javascript AST抽象語法樹
進(jìn)行字符串拼接就可以生成render函數(shù)。當(dāng)時在想這回算是穩(wěn)了,結(jié)果跟著就翻車了。 -
粉絲:面試官接著又讓我講“
transform
函數(shù)內(nèi)具體是如何處理vue內(nèi)置的v-for、v-model等指令?”,你的文章中沒有具體講過這個吖,我只有說不知道。面試官接著又問:generate
函數(shù)是如何進(jìn)行字符串拼接得到的render函數(shù)呢?,我還是回答的不知道。 -
我:我的鍋,接下來就先安排一篇文章來講講
transform
函數(shù)內(nèi)具體是如何處理vue內(nèi)置的v-for、v-model等指令?。
先來看個流程圖
先來看一下我畫的transform
函數(shù)執(zhí)行流程圖,讓你對整個流程有個大概的印象,后面的內(nèi)容看著就不費(fèi)勁了。如下圖:
從上面的流程圖可以看到transform
函數(shù)的執(zhí)行過程主要分為下面這幾步:
-
在
transform
函數(shù)中調(diào)用createTransformContext
函數(shù)生成上下文對象。在上下文對象中存儲了當(dāng)前正在轉(zhuǎn)換的node節(jié)點(diǎn)的信息,后面的traverseNode
、traverseChildren
、nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)、directiveTransforms
對象中的轉(zhuǎn)換函數(shù)都會依賴這個上下文對象。 -
然后執(zhí)行
traverseNode
函數(shù),traverseNode
函數(shù)是一個典型的洋蔥模型。第一次執(zhí)行traverseNode
函數(shù)的時候會進(jìn)入洋蔥模型的第一層,先將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第一層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,將轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)存到第一層的exitFns
數(shù)組中。經(jīng)過第一次轉(zhuǎn)換后v-for等指令已經(jīng)被初次處理了。 -
然后執(zhí)行
traverseChildren
函數(shù),在traverseChildren
函數(shù)中對當(dāng)前node節(jié)點(diǎn)的子節(jié)點(diǎn)執(zhí)行traverseNode
函數(shù)。此時就會進(jìn)入洋蔥模型的第二層,和上一步一樣會將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第二層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,將轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)存到第二層的exitFns
數(shù)組中。 -
假如第二層的node節(jié)點(diǎn)已經(jīng)沒有了子節(jié)點(diǎn),洋蔥模型就會從“進(jìn)入階段”變成“出去階段”。將第二層的
exitFns
數(shù)組中存的回調(diào)函數(shù)全部執(zhí)行一遍,對node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換,然后出去到第一層的洋蔥模型。經(jīng)過第二次轉(zhuǎn)換后v-for等指令已經(jīng)被完全處理了。 -
同樣將第一層中的
exitFns
數(shù)組中存的回調(diào)函數(shù)全部執(zhí)行一遍,由于此時第二層的node節(jié)點(diǎn)已經(jīng)全部處理完了,所以在exitFns
數(shù)組中存的回調(diào)函數(shù)中就可以根據(jù)子節(jié)點(diǎn)的情況來處理父節(jié)點(diǎn)。 -
執(zhí)行
nodeTransforms
數(shù)組中的transformElement
轉(zhuǎn)換函數(shù),會返回一個回調(diào)函數(shù)。在回調(diào)函數(shù)中會調(diào)用buildProps
函數(shù),在buildProps
函數(shù)中只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會執(zhí)行directiveTransforms
對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前node節(jié)點(diǎn)有v-model指令,才會去執(zhí)行transformModel
轉(zhuǎn)換函數(shù)。v-model等指令也就被處理了。文章來源:http://www.zghlxwxcb.cn/news/detail-855086.html
舉個例子
還是同樣的套路,我們通過debug一個簡單的demo來帶你搞清楚transform
函數(shù)內(nèi)具體是如何處理vue內(nèi)置的v-for、v-model等指令。demo代碼如下:
<template>
<div>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
<p>標(biāo)題是:{{ title }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msgList = ref([
{
id: 1,
value: "",
},
{
id: 2,
value: "",
},
{
id: 3,
value: "",
},
]);
const title = ref("hello word");
</script>
在上面的代碼中,我們給input標(biāo)簽使用了v-for和v-model指令,還渲染了一個p標(biāo)簽。p標(biāo)簽中的內(nèi)容由foo
變量、bar
字符串、baz
變量拼接而來的。
我們在上一篇 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章中已經(jīng)講過了,將template模版編譯成模版AST抽象語法樹的過程中不會處理v-for、v-model等內(nèi)置指令,而是將其當(dāng)做普通的props屬性處理。
比如我們這個demo,編譯成模版AST抽象語法樹后。input標(biāo)簽對應(yīng)的node節(jié)點(diǎn)中就增加了三個props屬性,name分別為for、bind、model,分別對應(yīng)的是v-for、v-bind、v-model。真正處理這些vue內(nèi)置指令是在transform
函數(shù)中。
transform函數(shù)
本文中使用的vue版本為3.4.19,transform
函數(shù)在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。找到transform
函數(shù)的代碼,打上斷點(diǎn)。
從上一篇文章我們知道了transform
函數(shù)是在node端執(zhí)行的,所以我們需要啟動一個debug
終端,才可以在node端打斷點(diǎn)。這里以vscode舉例,首先我們需要打開終端,然后點(diǎn)擊終端中的+
號旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal
就可以啟動一個debug
終端。
接著在debug
終端中執(zhí)行yarn dev
(這里是以vite
舉例)。在瀏覽器中訪問?http://localhost:5173/,此時斷點(diǎn)就會走到transform
函數(shù)中了。我們在debug終端中來看看調(diào)用transform
函數(shù)時傳入的root
變量,如下圖:
從上圖中我們可以看到transform
函數(shù)接收的第一個參數(shù)root
變量是一個模版AST抽象語法樹,為什么說他是模版AST抽象語法樹呢?因?yàn)檫@棵樹的結(jié)構(gòu)和template模塊中的結(jié)構(gòu)一模一樣,root
變量也就是模版AST抽象語法樹是對template模塊進(jìn)行描述。
根節(jié)點(diǎn)的children下面只有一個div子節(jié)點(diǎn),對應(yīng)的就是最外層的div標(biāo)簽。div節(jié)點(diǎn)children下面有兩個子節(jié)點(diǎn),分別對應(yīng)的是input標(biāo)簽和p標(biāo)簽。input標(biāo)簽中有三個props,分別對應(yīng)input標(biāo)簽上面的v-for指令、key屬性、v-model指令。從這里我們可以看出來此時vue內(nèi)置的指令還沒被處理,在執(zhí)行parse函數(shù)生成模版AST抽象語法樹階段只是將其當(dāng)做普通的屬性處理后,再塞到props屬性中。
p標(biāo)簽中的內(nèi)容由兩部分組成:<p>標(biāo)題是:{{ title }}</p>
。此時我們發(fā)現(xiàn)p標(biāo)簽的children也是有兩個,分別是寫死的文本和title
變量。
我們接著來看transform
函數(shù),在我們這個場景中簡化后的代碼如下:
function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);
}
從上面的代碼中可以看到transform
函數(shù)內(nèi)主要有兩部分,從名字我想你應(yīng)該就能猜出他們的作用。傳入模版AST抽象語法樹和options
,調(diào)用createTransformContext
函數(shù)生成context
上下文對象。傳入模版AST抽象語法樹和context
上下文對象,調(diào)用traverseNode
函數(shù)對樹中的node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換。
createTransformContext函數(shù)
在講createTransformContext
函數(shù)之前我們先來了解一下什么是context(上下文)。
什么是上下文
上下文其實(shí)就是在某個范圍內(nèi)的“全局變量”,在這個范圍內(nèi)的任意地方都可以拿到這個“全局變量”。舉兩個例子:
在vue中可以通過provied向整顆組件樹提供數(shù)據(jù),然后在樹的任意節(jié)點(diǎn)可以通過inject拿到提供的數(shù)據(jù)。比如:
根組件App.vue,注入上下文。
const count = ref(0)
provide('count', count)
業(yè)務(wù)組件list.vue,讀取上下文。
const count = inject('count')
在react中,我們可以使用React.createContext
函數(shù)創(chuàng)建一個上下文對象,然后注入到組件樹中。
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('light');
// ...
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
在這顆組件樹的任意層級中都能拿到上下文對象中提供的數(shù)據(jù):
const theme = useContext(ThemeContext);
樹中的節(jié)點(diǎn)一般可以通過children拿到子節(jié)點(diǎn),但是父節(jié)點(diǎn)一般不容易通過子節(jié)點(diǎn)拿到。在轉(zhuǎn)換的過程中我們有的時候需要拿到父節(jié)點(diǎn)進(jìn)行一些操作,比如將當(dāng)前節(jié)點(diǎn)替換為一個新的節(jié)點(diǎn),又或者直接刪掉當(dāng)前節(jié)點(diǎn)。
所以在這里會維護(hù)一個context上下文對象,對象中會維護(hù)一些狀態(tài)和方法。比如當(dāng)前正在轉(zhuǎn)換的節(jié)點(diǎn)是哪個,當(dāng)前轉(zhuǎn)換的節(jié)點(diǎn)的父節(jié)點(diǎn)是哪個,當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中是第幾個子節(jié)點(diǎn),還有replaceNode
、removeNode
等方法。
上下文中的一些屬性和方法
我們將斷點(diǎn)走進(jìn)createTransformContext
函數(shù)中,簡化后的代碼如下:
function createTransformContext(
root,
{
nodeTransforms = [],
directiveTransforms = {},
// ...省略
}
) {
const context = {
// 所有的node節(jié)點(diǎn)都會將nodeTransforms數(shù)組中的所有的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍
nodeTransforms,
// 只執(zhí)行node節(jié)點(diǎn)的指令在directiveTransforms對象中對應(yīng)的轉(zhuǎn)換函數(shù)
directiveTransforms,
// 需要轉(zhuǎn)換的AST抽象語法樹
root,
// 轉(zhuǎn)換過程中組件內(nèi)注冊的組件
components: new Set(),
// 轉(zhuǎn)換過程中組件內(nèi)注冊的指令
directives: new Set(),
// 當(dāng)前正在轉(zhuǎn)換節(jié)點(diǎn)的父節(jié)點(diǎn),默認(rèn)轉(zhuǎn)換的是根節(jié)點(diǎn)。根節(jié)點(diǎn)沒有父節(jié)點(diǎn),所以為null。
parent: null,
// 當(dāng)前正在轉(zhuǎn)換的節(jié)點(diǎn),默認(rèn)為根節(jié)點(diǎn)
currentNode: root,
// 當(dāng)前轉(zhuǎn)換節(jié)點(diǎn)在父節(jié)點(diǎn)中的index位置
childIndex: 0,
replaceNode(node) {
// 將當(dāng)前節(jié)點(diǎn)替換為新節(jié)點(diǎn)
},
removeNode(node) {
// 刪除當(dāng)前節(jié)點(diǎn)
},
// ...省略
};
return context;
}
從上面的代碼中可以看到createTransformContext
中的代碼其實(shí)很簡單,第一個參數(shù)為需要轉(zhuǎn)換的模版AST抽象語法樹,第二個參數(shù)對傳入的options
進(jìn)行解構(gòu),拿到options.nodeTransforms
數(shù)組和options.directiveTransforms
對象。
nodeTransforms
數(shù)組中存了一堆轉(zhuǎn)換函數(shù),在樹的遞歸遍歷過程中會將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍。directiveTransforms
對象中也存了一堆轉(zhuǎn)換函數(shù),和nodeTransforms
數(shù)組的區(qū)別是,只會執(zhí)行node節(jié)點(diǎn)的指令在directiveTransforms
對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如node節(jié)點(diǎn)中只有v-model指令,那就只會執(zhí)行directiveTransforms
對象中的transformModel
轉(zhuǎn)換函數(shù)。這里將拿到的nodeTransforms
數(shù)組和directiveTransforms
對象都存到了context
上下文中。
在context
上下文中存了一些狀態(tài)屬性:
-
root:需要轉(zhuǎn)換的AST抽象語法樹。
-
components:轉(zhuǎn)換過程中組件內(nèi)注冊的組件。
-
directives:轉(zhuǎn)換過程中組件內(nèi)注冊的指令。
-
parent:當(dāng)前正在轉(zhuǎn)換節(jié)點(diǎn)的父節(jié)點(diǎn),默認(rèn)轉(zhuǎn)換的是根節(jié)點(diǎn)。根節(jié)點(diǎn)沒有父節(jié)點(diǎn),所以為null。
-
currentNode:當(dāng)前正在轉(zhuǎn)換的節(jié)點(diǎn),默認(rèn)為根節(jié)點(diǎn)。
-
childIndex:當(dāng)前轉(zhuǎn)換節(jié)點(diǎn)在父節(jié)點(diǎn)中的index位置。
在context
上下文中存了一些方法:
-
replaceNode:將當(dāng)前節(jié)點(diǎn)替換為新節(jié)點(diǎn)。
-
removeNode:刪除當(dāng)前節(jié)點(diǎn)。
traverseNode函數(shù)
接著將斷點(diǎn)走進(jìn)traverseNode
函數(shù)中,在我們這個場景中簡化后的代碼如下:
function traverseNode(node, context) {
context.currentNode = node;
const { nodeTransforms } = context;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context);
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}
if (!context.currentNode) {
return;
} else {
node = context.currentNode;
}
}
traverseChildren(node, context);
context.currentNode = node;
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
從上面的代碼中我們可以看到traverseNode
函數(shù)接收兩個參數(shù),第一個參數(shù)為當(dāng)前需要處理的node節(jié)點(diǎn),第一次調(diào)用時傳的就是樹的根節(jié)點(diǎn)。第二個參數(shù)是上下文對象。
我們再來看traverseNode
函數(shù)的內(nèi)容,內(nèi)容主要分為三部分。分別是:
-
將
nodeTransforms
數(shù)組內(nèi)的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,如果轉(zhuǎn)換函數(shù)的執(zhí)行結(jié)果是一個回調(diào)函數(shù),那么就將回調(diào)函數(shù)push到exitFns
數(shù)組中。 -
調(diào)用
traverseChildren
函數(shù)處理子節(jié)點(diǎn)。 -
將
exitFns
數(shù)組中存的回調(diào)函數(shù)依次從末尾取出來挨個執(zhí)行。
traverseChildren函數(shù)
我們先來看看第二部分的traverseChildren
函數(shù),代碼很簡單,簡化后的代碼如下:
function traverseChildren(parent, context) {
let i = 0;
for (; i < parent.children.length; i++) {
const child = parent.children[i];
context.parent = parent;
context.childIndex = i;
traverseNode(child, context);
}
}
在traverseChildren
函數(shù)中會去遍歷當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),在遍歷過程中會將context.parent
更新為當(dāng)前的節(jié)點(diǎn),并且將context.childIndex
也更新為當(dāng)前子節(jié)點(diǎn)所在的位置。然后再調(diào)用traverseNode
函數(shù)處理當(dāng)前的子節(jié)點(diǎn)。
所以在traverseNode
函數(shù)執(zhí)行的過程中,context.parent
總是指向當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn),context.childIndex
總是指向當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中的index位置。如下圖:
進(jìn)入時執(zhí)行的轉(zhuǎn)換函數(shù)
我們現(xiàn)在回過頭來看第一部分的代碼,代碼如下:
function traverseNode(node, context) {
context.currentNode = node;
const { nodeTransforms } = context;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context);
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}
if (!context.currentNode) {
return;
} else {
node = context.currentNode;
}
}
// ...省略
}
首先會將context.currentNode
更新為當(dāng)前節(jié)點(diǎn),然后從context上下文中拿到由轉(zhuǎn)換函數(shù)組成的nodeTransforms
數(shù)組。
在 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章中我們已經(jīng)講過了nodeTransforms
數(shù)組中主要存了下面這些轉(zhuǎn)換函數(shù),代碼如下:
const nodeTransforms = [
transformOnce,
transformIf,
transformMemo,
transformFor,
transformFilter,
trackVForSlotScopes,
transformExpression
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
]
很明顯我們這里的v-for指令就會被nodeTransforms
數(shù)組中的transformFor
轉(zhuǎn)換函數(shù)處理。
看到這里有的小伙伴就會問了,怎么沒有在nodeTransforms
數(shù)組中看到處理v-model
指令的轉(zhuǎn)換函數(shù)呢?處理v-model
指令的轉(zhuǎn)換函數(shù)是在directiveTransforms
對象中。在directiveTransforms
對象中主要存了下面這些轉(zhuǎn)換函數(shù):
const directiveTransforms = {
bind: transformBind,
cloak: compilerCore.noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow
}
nodeTransforms
和directiveTransforms
的區(qū)別是,在遞歸遍歷轉(zhuǎn)換node節(jié)點(diǎn)時,每次都會將nodeTransforms
數(shù)組中的所有轉(zhuǎn)換函數(shù)都全部執(zhí)行一遍。比如當(dāng)前轉(zhuǎn)換的node節(jié)點(diǎn)中沒有使用v-if指令,但是在轉(zhuǎn)換當(dāng)前node節(jié)點(diǎn)時還是會執(zhí)行nodeTransforms
數(shù)組中的transformIf
轉(zhuǎn)換函數(shù)。
而directiveTransforms
是在遞歸遍歷轉(zhuǎn)換node節(jié)點(diǎn)時,只會執(zhí)行node節(jié)點(diǎn)中存在的指令對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前轉(zhuǎn)換的node節(jié)點(diǎn)中有使用v-model指令,所以就會執(zhí)行directiveTransforms
對象中的transformModel
轉(zhuǎn)換函數(shù)。由于node節(jié)點(diǎn)中沒有使用v-html指令,所以就不會執(zhí)行directiveTransforms
對象中的transformVHtml
轉(zhuǎn)換函數(shù)。
我們前面講過了context上下文中存了很多屬性和方法。包括當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是誰,當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中的index位置,替換當(dāng)前節(jié)點(diǎn)的方法,刪除當(dāng)前節(jié)點(diǎn)的方法。這樣在轉(zhuǎn)換函數(shù)中就可以通過context上下文對當(dāng)前節(jié)點(diǎn)進(jìn)行各種操作了。
將轉(zhuǎn)換函數(shù)的返回值賦值給onExit
變量,如果onExit
不為空,說明轉(zhuǎn)換函數(shù)的返回值是一個回調(diào)函數(shù)或者由回調(diào)函數(shù)組成的數(shù)組。將這些回調(diào)函數(shù)push進(jìn)exitFns
數(shù)組中,在退出時會將這些回調(diào)函數(shù)倒序全部執(zhí)行一遍。
執(zhí)行完回調(diào)函數(shù)后會判斷上下文中的currentNode
是否為空,如果為空那么就return掉整個traverseNode
函數(shù),后面的traverseChildren
等函數(shù)都不會執(zhí)行了。如果context.currentNode
不為空,那么就將本地的node
變量更新成context上下文中的currentNode
。
為什么需要判斷context上下文中的currentNode
呢?原因是經(jīng)過轉(zhuǎn)換函數(shù)的處理后當(dāng)前節(jié)點(diǎn)可能會被刪除了,也有可能會被替換成一個新的節(jié)點(diǎn),所以在每次執(zhí)行完轉(zhuǎn)換函數(shù)后都會更新本地的node變量,保證在下一個的轉(zhuǎn)換函數(shù)執(zhí)行時傳入的是最新的node節(jié)點(diǎn)。
退出時執(zhí)行的轉(zhuǎn)換函數(shù)回調(diào)
我們接著來看traverseNode
函數(shù)中最后一部分,代碼如下:
function traverseNode(node, context) {
// ...省略
context.currentNode = node;
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
由于這段代碼是在執(zhí)行完traverseChildren
函數(shù)再執(zhí)行的,前面已經(jīng)講過了在traverseChildren
函數(shù)中會將當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)全部都處理了,所以當(dāng)代碼執(zhí)行到這里時所有的子節(jié)點(diǎn)都已經(jīng)處理完了。所以在轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)中我們可以根據(jù)當(dāng)前節(jié)點(diǎn)轉(zhuǎn)換后的子節(jié)點(diǎn)情況來決定如何處理當(dāng)前節(jié)點(diǎn)。
在處理子節(jié)點(diǎn)的時候我們會將context.currentNode
更新為子節(jié)點(diǎn),所以在處理完子節(jié)點(diǎn)后需要將context.currentNode
更新為當(dāng)前節(jié)點(diǎn)。這樣在執(zhí)行轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)時,context.currentNode
始終就是指向的是當(dāng)前的node節(jié)點(diǎn)。
請注意這里是倒序取出exitFns
數(shù)組中存的回調(diào)函數(shù),在進(jìn)入時會按照順序去執(zhí)行nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)。在退出時會倒序去執(zhí)行存下來的回調(diào)函數(shù),比如在nodeTransforms
數(shù)組中transformIf
函數(shù)排在transformFor
函數(shù)前面。transformIf
用于處理v-if指令,transformFor
用于處理v-for指令。在進(jìn)入時transformIf
函數(shù)會比transformFor
函數(shù)先執(zhí)行,所以在組件上面同時使用v-if和v-for指令,會是v-if指令先生效。在退出階段時transformIf
函數(shù)會比transformFor
函數(shù)后執(zhí)行,所以在transformIf
回調(diào)函數(shù)中可以根據(jù)transformFor
回調(diào)函數(shù)的執(zhí)行結(jié)果來決定如何處理當(dāng)前的node節(jié)點(diǎn)。
traverseNode
函數(shù)其實(shí)就是典型的洋蔥模型,依次從父組件到子組件挨著調(diào)用nodeTransforms
數(shù)組中所有的轉(zhuǎn)換函數(shù),然后從子組件到父組件倒序執(zhí)行nodeTransforms
數(shù)組中所有的轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)。traverseNode
函數(shù)內(nèi)的設(shè)計很高明,如果你還沒反應(yīng)過來,別著急我接下來會講他高明在哪里。
洋蔥模型traverseNode函數(shù)
我們先來看看什么是洋蔥模型,如下圖:
洋蔥模型就是:從外面一層層的進(jìn)去,再一層層的從里面出來。
第一次進(jìn)入traverseNode
函數(shù)的時候會進(jìn)入洋蔥模型的第1層,先依次將nodeTransforms
數(shù)組中所有的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對當(dāng)前的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換。如果轉(zhuǎn)換函數(shù)的返回值是回調(diào)函數(shù)或者回調(diào)函數(shù)組成的數(shù)組,那就將這些回調(diào)函數(shù)依次push到第1層定義的exitFns
數(shù)組中。
然后再去處理當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),處理子節(jié)點(diǎn)的traverseChildren
函數(shù)其實(shí)也是在調(diào)用traverseNode
函數(shù),此時已經(jīng)進(jìn)入了洋蔥模型的第2層。同理在第2層也會將nodeTransforms
數(shù)組中所有的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第2層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,并且將返回的回調(diào)函數(shù)依次push到第2層定義的exitFns
數(shù)組中。
同樣的如果第2層節(jié)點(diǎn)也有子節(jié)點(diǎn),那么就會進(jìn)入洋蔥模型的第3層。在第3層也會將nodeTransforms
數(shù)組中所有的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第3層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,并且將返回的回調(diào)函數(shù)依次push到第3層定義的exitFns
數(shù)組中。
請注意此時的第3層已經(jīng)沒有子節(jié)點(diǎn)了,那么現(xiàn)在就要從一層層的進(jìn)去,變成一層層的出去。首先會將第3層exitFns
數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會對第3層的node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換,此時第3層中的node節(jié)點(diǎn)已經(jīng)被全部轉(zhuǎn)換完了。
由于第3層的node節(jié)點(diǎn)已經(jīng)被全部轉(zhuǎn)換完了,所以會出去到洋蔥模型的第2層。同樣將第2層exitFns
數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會對第2層的node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換。值得一提的是由于第3層的node節(jié)點(diǎn)也就是第2層的children節(jié)點(diǎn)已經(jīng)被完全轉(zhuǎn)換了,所以在執(zhí)行第2層轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)時就可以根據(jù)子節(jié)點(diǎn)的情況來處理父節(jié)點(diǎn)。
同理將第2層的node節(jié)點(diǎn)全部轉(zhuǎn)換完了后,會出去到洋蔥模型的第1層。將第1層exitFns
數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會對第1層的node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換。
當(dāng)出去階段的第1層全部處理完后了,transform
函數(shù)內(nèi)處理內(nèi)置的v-for等指令也就處理完了。執(zhí)行完transform
函數(shù)后,描述template解構(gòu)的模版AST抽象語法樹也被處理成了描述render函數(shù)結(jié)構(gòu)的javascript AST抽象語法樹。后續(xù)只需要執(zhí)行generate
函數(shù),進(jìn)行普通的字符串拼接就可以得到render函數(shù)。
繼續(xù)debug
搞清楚了traverseNode
函數(shù),接著來debug看看demo中的v-for指令和v-model指令是如何被處理的。
-
v-for指令對應(yīng)的是
transformFor
轉(zhuǎn)換函數(shù)。 -
v-model指令對應(yīng)的是
transformModel
轉(zhuǎn)換函數(shù)。
transformFor轉(zhuǎn)換函數(shù)
通過前面我們知道了用于處理v-for
指令的transformFor
轉(zhuǎn)換函數(shù)是在nodeTransforms
數(shù)組中,每次處理node節(jié)點(diǎn)都會執(zhí)行。我們給transformFor
轉(zhuǎn)換函數(shù)打3個斷點(diǎn),分別是:
-
進(jìn)入
transformFor
轉(zhuǎn)換函數(shù)之前。 -
調(diào)用
transformFor
轉(zhuǎn)換函數(shù),第1次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后。 -
調(diào)用
transformFor
轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù),第2次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后。
我們將代碼走到第1個斷點(diǎn),看看執(zhí)行transformFor
轉(zhuǎn)換函數(shù)之前input標(biāo)簽的node節(jié)點(diǎn)是什么樣的,如下圖:
從上圖中可以看到input標(biāo)簽的node節(jié)點(diǎn)中還是有一個v-for的props屬性,說明此時v-for指令還沒被處理。
我們接著將代碼走到第2個斷點(diǎn),看看調(diào)用transformFor
轉(zhuǎn)換函數(shù)第1次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后是什么樣的,如下圖:
從上圖中可以看到原本的input的node節(jié)點(diǎn)已經(jīng)被替換成了一個新的node節(jié)點(diǎn),新的node節(jié)點(diǎn)的children才是原來的node節(jié)點(diǎn)。并且input節(jié)點(diǎn)props屬性中的v-for指令也被消費(fèi)了。新節(jié)點(diǎn)的source.content
里存的是v-for="item in msgList"
中的msgList
變量。新節(jié)點(diǎn)的valueAlias.content
里存的是v-for="item in msgList"
中的item
。請注意此時arguments
數(shù)組中只有一個字段,存的是msgList
變量。
我們接著將代碼走到第3個斷點(diǎn),看看調(diào)用transformFor
轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù),第2次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后是什么樣的,如下圖:
從上圖可以看到arguments
數(shù)組中多了一個字段,input標(biāo)簽現(xiàn)在是當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)。按照我們前面講的洋蔥模型,input子節(jié)點(diǎn)現(xiàn)在已經(jīng)被轉(zhuǎn)換完成了。所以多的這個字段就是input標(biāo)簽經(jīng)過transform
函數(shù)轉(zhuǎn)換后的node節(jié)點(diǎn),將轉(zhuǎn)換后的input子節(jié)點(diǎn)存到父節(jié)點(diǎn)上面,后面生成render函數(shù)時會用。
transformModel轉(zhuǎn)換函數(shù)
通過前面我們知道了用于處理v-model
指令的transformModel
轉(zhuǎn)換函數(shù)是在directiveTransforms
對象中,只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會執(zhí)行對應(yīng)的轉(zhuǎn)換函數(shù)。我們這里input上面有v-model指令,所以就會執(zhí)行transformModel
轉(zhuǎn)換函數(shù)。
我們在前面的 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章中已經(jīng)講過了處理v-model
指令是調(diào)用的@vue/compiler-dom
包的transformModel
函數(shù),很容易就可以找到@vue/compiler-dom
包的transformModel
函數(shù),然后打一個斷點(diǎn),讓斷點(diǎn)走進(jìn)transformModel
函數(shù)中,如下圖:
從上面的圖中我們可以看到在@vue/compiler-dom
包的transformModel
函數(shù)中會調(diào)用@vue/compiler-core
包的transformModel
函數(shù),拿到返回的baseResult
對象后再一些其他操作后直接return baseResult
。
從左邊的call stack調(diào)用棧中我們可以看到transformModel
函數(shù)是由一個buildProps
函數(shù)調(diào)用的,buildProps
函數(shù)是由postTransformElement
函數(shù)調(diào)用的。而postTransformElement
函數(shù)則是transformElement
轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù),transformElement
轉(zhuǎn)換函數(shù)是在nodeTransforms
數(shù)組中。
所以directiveTransforms
對象中的轉(zhuǎn)換函數(shù)調(diào)用其實(shí)是由nodeTransforms
數(shù)組中的transformElement
轉(zhuǎn)換函數(shù)調(diào)用的。如下圖:
看名字你應(yīng)該猜到了buildProps
函數(shù)的作用是生成props屬性的。點(diǎn)擊Step Out將斷點(diǎn)跳出transformModel
函數(shù),走進(jìn)buildProps
函數(shù)中,可以看到buildProps
函數(shù)中調(diào)用transformModel
函數(shù)的代碼如下圖:
從上圖中可以看到執(zhí)行directiveTransforms
對象中的轉(zhuǎn)換函數(shù)不僅可以對節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換,還會返回一個props數(shù)組。比如我們這里處理的是v-model指令,返回的props數(shù)組就是由v-model指令編譯而來的props屬性,這就是所謂的v-model語法糖。
看到這里有的小伙伴會疑惑了v-model
指令不是會生成modelValue
和onUpdate:modelValue
兩個屬性,為什么這里只有一個onUpdate:modelValue
屬性呢?
答案是只有給自定義組件上面使用v-model
指令才會生成modelValue
和onUpdate:modelValue
兩個屬性,對于這種原生input標(biāo)簽是不需要生成modelValue
屬性的,而且input標(biāo)簽本身是不接收名為modelValue
屬性,接收的是value屬性。
總結(jié)
現(xiàn)在我們再來看看最開始講的流程圖,我想你應(yīng)該已經(jīng)能將整個流程串起來了。如下圖:
transform
函數(shù)的執(zhí)行過程主要分為下面這幾步:
-
在
transform
函數(shù)中調(diào)用createTransformContext
函數(shù)生成上下文對象。在上下文對象中存儲了當(dāng)前正在轉(zhuǎn)換的node節(jié)點(diǎn)的信息,后面的traverseNode
、traverseChildren
、nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)、directiveTransforms
對象中的轉(zhuǎn)換函數(shù)都會依賴這個上下文對象。 -
然后執(zhí)行
traverseNode
函數(shù),traverseNode
函數(shù)是一個典型的洋蔥模型。第一次執(zhí)行traverseNode
函數(shù)的時候會進(jìn)入洋蔥模型的第一層,先將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第一層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,將轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)存到第一層的exitFns
數(shù)組中。經(jīng)過第一次轉(zhuǎn)換后v-for等指令已經(jīng)被初次處理了。 -
然后執(zhí)行
traverseChildren
函數(shù),在traverseChildren
函數(shù)中對當(dāng)前node節(jié)點(diǎn)的子節(jié)點(diǎn)執(zhí)行traverseNode
函數(shù)。此時就會進(jìn)入洋蔥模型的第二層,和上一步一樣會將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第二層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,將轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)存到第二層的exitFns
數(shù)組中。 -
假如第二層的node節(jié)點(diǎn)已經(jīng)沒有了子節(jié)點(diǎn),洋蔥模型就會從“進(jìn)入階段”變成“出去階段”。將第二層的
exitFns
數(shù)組中存的回調(diào)函數(shù)全部執(zhí)行一遍,對node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換,然后出去到第一層的洋蔥模型。經(jīng)過第二次轉(zhuǎn)換后v-for等指令已經(jīng)被完全處理了。 -
同樣將第一層中的
exitFns
數(shù)組中存的回調(diào)函數(shù)全部執(zhí)行一遍,由于此時第二層的node節(jié)點(diǎn)已經(jīng)全部處理完了,所以在exitFns
數(shù)組中存的回調(diào)函數(shù)中就可以根據(jù)子節(jié)點(diǎn)的情況來處理父節(jié)點(diǎn)。 -
執(zhí)行
nodeTransforms
數(shù)組中的transformElement
轉(zhuǎn)換函數(shù),會返回一個回調(diào)函數(shù)。在回調(diào)函數(shù)中會調(diào)用buildProps
函數(shù),在buildProps
函數(shù)中只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會執(zhí)行directiveTransforms
對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前node節(jié)點(diǎn)有v-model指令,才會去執(zhí)行transformModel
轉(zhuǎn)換函數(shù)。v-model等指令也就被處理了。
關(guān)注公眾號:前端歐陽
,解鎖我更多vue
干貨文章。還可以加我微信,私信我想看哪些vue
原理文章,我會根據(jù)大家的反饋進(jìn)行創(chuàng)作。文章來源地址http://www.zghlxwxcb.cn/news/detail-855086.html
到了這里,關(guān)于面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!