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