1. 面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

        共 19760字,需瀏覽 40分鐘

         ·

        2024-04-19 09:10

        前言

        最近有粉絲找到我,說被面試官給問懵了。

        • 粉絲:面試官上來就問“一個(gè)vue文件是如何渲染成瀏覽器上面的真實(shí)DOM?”,當(dāng)時(shí)還挺竊喜這題真簡單。就簡單說了一下先是編譯成render函數(shù)、然后根據(jù)render函數(shù)生成虛擬DOM,最后就是根據(jù)虛擬DOM生成真實(shí)DOM。按照正常套路面試官接著會(huì)問vue響應(yīng)式原理和diff算法,結(jié)果面試官不講武德問了我“那render函數(shù)又是怎么生成的呢?”。

        • 我:之前寫過一篇 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章專門講過這個(gè)吖。

        • 粉絲:我就是按照你文章回答的面試官,底層其實(shí)是調(diào)用的一個(gè)叫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)時(shí)在想這回算是穩(wěn)了,結(jié)果跟著就翻車了。

        • 粉絲:面試官接著又讓我講“**transform函數(shù)內(nèi)具體是如何處理vue內(nèi)置的v-for、v-model等指令?”,你的文章中沒有具體講過這個(gè)吖,我只有說不知道。面試官接著又問:generate函數(shù)是如何進(jìn)行字符串拼接得到的render函數(shù)呢?**,我還是回答的不知道。

        • 我:我的鍋,接下來就先安排一篇文章來講講transform函數(shù)內(nèi)具體是如何處理vue內(nèi)置的v-for、v-model等指令?。

        先來看個(gè)流程圖

        先來看一下我畫的transform函數(shù)執(zhí)行流程圖,讓你對整個(gè)流程有個(gè)大概的印象,后面的內(nèi)容看著就不費(fèi)勁了。如下圖:

        從上面的流程圖可以看到transform函數(shù)的執(zhí)行過程主要分為下面這幾步:

        • transform函數(shù)中調(diào)用createTransformContext函數(shù)生成上下文對象。在上下文對象中存儲了當(dāng)前正在轉(zhuǎn)換的node節(jié)點(diǎn)的信息,后面的traverseNodetraverseChildren、nodeTransforms數(shù)組中的轉(zhuǎn)換函數(shù)、directiveTransforms對象中的轉(zhuǎn)換函數(shù)都會(huì)依賴這個(gè)上下文對象。

        • 然后執(zhí)行traverseNode函數(shù),traverseNode函數(shù)是一個(gè)典型的洋蔥模型。第一次執(zhí)行traverseNode函數(shù)的時(shí)候會(huì)進(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ù)。此時(shí)就會(huì)進(jìn)入洋蔥模型的第二層,和上一步一樣會(huì)將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),洋蔥模型就會(huì)從“進(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í)行一遍,由于此時(shí)第二層的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ù),會(huì)返回一個(gè)回調(diào)函數(shù)。在回調(diào)函數(shù)中會(huì)調(diào)用buildProps函數(shù),在buildProps函數(shù)中只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會(huì)執(zhí)行directiveTransforms對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前node節(jié)點(diǎn)有v-model指令,才會(huì)去執(zhí)行transformModel轉(zhuǎn)換函數(shù)。v-model等指令也就被處理了。

        舉個(gè)例子

        還是同樣的套路,我們通過debug一個(gè)簡單的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指令,還渲染了一個(gè)p標(biāo)簽。p標(biāo)簽中的內(nèi)容由foo變量、bar字符串、baz變量拼接而來的。

        我們在上一篇 看不懂來打我,vue3如何將template編譯成render函數(shù) 文章中已經(jīng)講過了,將template模版編譯成模版AST抽象語法樹的過程中不會(huì)處理v-for、v-model等內(nèi)置指令,而是將其當(dāng)做普通的props屬性處理。

        比如我們這個(gè)demo,編譯成模版AST抽象語法樹后。input標(biāo)簽對應(yīng)的node節(jié)點(diǎn)中就增加了三個(gè)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í)行的,所以我們需要啟動(dòng)一個(gè)debug終端,才可以在node端打斷點(diǎn)。這里以vscode舉例,首先我們需要打開終端,然后點(diǎn)擊終端中的+號旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。

        接著在debug終端中執(zhí)行yarn dev(這里是以vite舉例)。在瀏覽器中訪問 http://localhost:5173/,此時(shí)斷點(diǎn)就會(huì)走到transform函數(shù)中了。我們在debug終端中來看看調(diào)用transform函數(shù)時(shí)傳入的root變量,如下圖:

        從上圖中我們可以看到transform函數(shù)接收的第一個(gè)參數(shù)root變量是一個(gè)模版AST抽象語法樹,為什么說他是模版AST抽象語法樹呢?因?yàn)檫@棵樹的結(jié)構(gòu)和template模塊中的結(jié)構(gòu)一模一樣,root變量也就是模版AST抽象語法樹是對template模塊進(jìn)行描述。

        根節(jié)點(diǎn)的children下面只有一個(gè)div子節(jié)點(diǎn),對應(yīng)的就是最外層的div標(biāo)簽。div節(jié)點(diǎn)children下面有兩個(gè)子節(jié)點(diǎn),分別對應(yīng)的是input標(biāo)簽和p標(biāo)簽。input標(biāo)簽中有三個(gè)props,分別對應(yīng)input標(biāo)簽上面的v-for指令、key屬性、v-model指令。從這里我們可以看出來此時(shí)vue內(nèi)置的指令還沒被處理,在執(zhí)行parse函數(shù)生成模版AST抽象語法樹階段只是將其當(dāng)做普通的屬性處理后,再塞到props屬性中。

        p標(biāo)簽中的內(nèi)容由兩部分組成:<p>標(biāo)題是:{{ title }}</p>。此時(shí)我們發(fā)現(xiàn)p標(biāo)簽的children也是有兩個(gè),分別是寫死的文本和title變量。

        我們接著來看transform函數(shù),在我們這個(gè)場景中簡化后的代碼如下:

        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í)就是在某個(gè)范圍內(nèi)的“全局變量”,在這個(gè)范圍內(nèi)的任意地方都可以拿到這個(gè)“全局變量”。舉兩個(gè)例子:

        在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)建一個(gè)上下文對象,然后注入到組件樹中。

        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)換的過程中我們有的時(shí)候需要拿到父節(jié)點(diǎn)進(jìn)行一些操作,比如將當(dāng)前節(jié)點(diǎn)替換為一個(gè)新的節(jié)點(diǎn),又或者直接刪掉當(dāng)前節(jié)點(diǎn)。

        所以在這里會(huì)維護(hù)一個(gè)context上下文對象,對象中會(huì)維護(hù)一些狀態(tài)和方法。比如當(dāng)前正在轉(zhuǎn)換的節(jié)點(diǎn)是哪個(gè),當(dāng)前轉(zhuǎn)換的節(jié)點(diǎn)的父節(jié)點(diǎn)是哪個(gè),當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中是第幾個(gè)子節(jié)點(diǎn),還有replaceNode、removeNode等方法。

        上下文中的一些屬性和方法

        我們將斷點(diǎn)走進(jìn)createTransformContext函數(shù)中,簡化后的代碼如下:

        function createTransformContext(
          root,
          {
            nodeTransforms = [],
            directiveTransforms = {},
            // ...省略
          }
        ) {
          const context = {
            // 所有的node節(jié)點(diǎn)都會(huì)將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í)很簡單,第一個(gè)參數(shù)為需要轉(zhuǎn)換的模版AST抽象語法樹,第二個(gè)參數(shù)對傳入的options進(jìn)行解構(gòu),拿到options.nodeTransforms數(shù)組和options.directiveTransforms對象。

        nodeTransforms數(shù)組中存了一堆轉(zhuǎn)換函數(shù),在樹的遞歸遍歷過程中會(huì)將nodeTransforms數(shù)組中的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍。directiveTransforms對象中也存了一堆轉(zhuǎn)換函數(shù),和nodeTransforms數(shù)組的區(qū)別是,只會(huì)執(zhí)行node節(jié)點(diǎn)的指令在directiveTransforms對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如node節(jié)點(diǎn)中只有v-model指令,那就只會(huì)執(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ù)中,在我們這個(gè)場景中簡化后的代碼如下:

        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ù)接收兩個(gè)參數(shù),第一個(gè)參數(shù)為當(dāng)前需要處理的node節(jié)點(diǎn),第一次調(diào)用時(shí)傳的就是樹的根節(jié)點(diǎn)。第二個(gè)參數(shù)是上下文對象。

        我們再來看traverseNode函數(shù)的內(nèi)容,內(nèi)容主要分為三部分。分別是:

        • nodeTransforms數(shù)組內(nèi)的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,如果轉(zhuǎn)換函數(shù)的執(zhí)行結(jié)果是一個(gè)回調(diào)函數(shù),那么就將回調(diào)函數(shù)push到exitFns數(shù)組中。

        • 調(diào)用traverseChildren函數(shù)處理子節(jié)點(diǎn)。

        • exitFns數(shù)組中存的回調(diào)函數(shù)依次從末尾取出來挨個(gè)執(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ù)中會(huì)去遍歷當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),在遍歷過程中會(huì)將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位置。如下圖:

        traverseChildren

        進(jìn)入時(shí)執(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;
            }
          }
          // ...省略
        }

        首先會(huì)將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指令就會(huì)被nodeTransforms數(shù)組中的transformFor轉(zhuǎn)換函數(shù)處理。

        看到這里有的小伙伴就會(huì)問了,怎么沒有在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
        }

        nodeTransformsdirectiveTransforms的區(qū)別是,在遞歸遍歷轉(zhuǎn)換node節(jié)點(diǎn)時(shí),每次都會(huì)將nodeTransforms數(shù)組中的所有轉(zhuǎn)換函數(shù)都全部執(zhí)行一遍。比如當(dāng)前轉(zhuǎn)換的node節(jié)點(diǎn)中沒有使用v-if指令,但是在轉(zhuǎn)換當(dāng)前node節(jié)點(diǎn)時(shí)還是會(huì)執(zhí)行nodeTransforms數(shù)組中的transformIf轉(zhuǎn)換函數(shù)。

        directiveTransforms是在遞歸遍歷轉(zhuǎn)換node節(jié)點(diǎn)時(shí),只會(huì)執(zhí)行node節(jié)點(diǎn)中存在的指令對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前轉(zhuǎn)換的node節(jié)點(diǎn)中有使用v-model指令,所以就會(huì)執(zhí)行directiveTransforms對象中的transformModel轉(zhuǎn)換函數(shù)。由于node節(jié)點(diǎn)中沒有使用v-html指令,所以就不會(huì)執(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ù)的返回值是一個(gè)回調(diào)函數(shù)或者由回調(diào)函數(shù)組成的數(shù)組。將這些回調(diào)函數(shù)push進(jìn)exitFns數(shù)組中,在退出時(shí)會(huì)將這些回調(diào)函數(shù)倒序全部執(zhí)行一遍。

        執(zhí)行完回調(diào)函數(shù)后會(huì)判斷上下文中的currentNode是否為空,如果為空那么就return掉整個(gè)traverseNode函數(shù),后面的traverseChildren等函數(shù)都不會(huì)執(zhí)行了。如果context.currentNode不為空,那么就將本地的node變量更新成context上下文中的currentNode。

        為什么需要判斷context上下文中的currentNode呢?原因是經(jīng)過轉(zhuǎn)換函數(shù)的處理后當(dāng)前節(jié)點(diǎn)可能會(huì)被刪除了,也有可能會(huì)被替換成一個(gè)新的節(jié)點(diǎn),所以在每次執(zhí)行完轉(zhuǎn)換函數(shù)后都會(huì)更新本地的node變量,保證在下一個(gè)的轉(zhuǎn)換函數(shù)執(zhí)行時(shí)傳入的是最新的node節(jié)點(diǎn)。

        退出時(shí)執(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ù)中會(huì)將當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)全部都處理了,所以當(dāng)代碼執(zhí)行到這里時(shí)所有的子節(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)的時(shí)候我們會(huì)將context.currentNode更新為子節(jié)點(diǎn),所以在處理完子節(jié)點(diǎn)后需要將context.currentNode更新為當(dāng)前節(jié)點(diǎn)。這樣在執(zhí)行轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù)時(shí),context.currentNode始終就是指向的是當(dāng)前的node節(jié)點(diǎn)。

        請注意這里是倒序取出exitFns數(shù)組中存的回調(diào)函數(shù),在進(jìn)入時(shí)會(huì)按照順序去執(zhí)行nodeTransforms數(shù)組中的轉(zhuǎn)換函數(shù)。在退出時(shí)會(huì)倒序去執(zhí)行存下來的回調(diào)函數(shù),比如在nodeTransforms數(shù)組中transformIf函數(shù)排在transformFor函數(shù)前面。transformIf用于處理v-if指令,transformFor用于處理v-for指令。在進(jìn)入時(shí)transformIf函數(shù)會(huì)比transformFor函數(shù)先執(zhí)行,所以在組件上面同時(shí)使用v-if和v-for指令,會(huì)是v-if指令先生效。在退出階段時(shí)transformIf函數(shù)會(huì)比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è)計(jì)很高明,如果你還沒反應(yīng)過來,別著急我接下來會(huì)講他高明在哪里。

        洋蔥模型traverseNode函數(shù)

        我們先來看看什么是洋蔥模型,如下圖:

        洋蔥模型就是:從外面一層層的進(jìn)去,再一層層的從里面出來。

        第一次進(jìn)入traverseNode函數(shù)的時(shí)候會(huì)進(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ù),此時(shí)已經(jīng)進(jìn)入了洋蔥模型的第2層。同理在第2層也會(huì)將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),那么就會(huì)進(jìn)入洋蔥模型的第3層。在第3層也會(huì)將nodeTransforms數(shù)組中所有的轉(zhuǎn)換函數(shù)全部執(zhí)行一遍,對第3層的node節(jié)點(diǎn)進(jìn)行第一次轉(zhuǎn)換,并且將返回的回調(diào)函數(shù)依次push到第3層定義的exitFns數(shù)組中。

        請注意此時(shí)的第3層已經(jīng)沒有子節(jié)點(diǎn)了,那么現(xiàn)在就要從一層層的進(jìn)去,變成一層層的出去。首先會(huì)將第3層exitFns數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會(huì)對第3層的node節(jié)點(diǎn)進(jìn)行第二次轉(zhuǎn)換,此時(shí)第3層中的node節(jié)點(diǎn)已經(jīng)被全部轉(zhuǎn)換完了。

        由于第3層的node節(jié)點(diǎn)已經(jīng)被全部轉(zhuǎn)換完了,所以會(huì)出去到洋蔥模型的第2層。同樣將第2層exitFns數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會(huì)對第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ù)時(shí)就可以根據(jù)子節(jié)點(diǎn)的情況來處理父節(jié)點(diǎn)。

        同理將第2層的node節(jié)點(diǎn)全部轉(zhuǎn)換完了后,會(huì)出去到洋蔥模型的第1層。將第1層exitFns數(shù)組中存的回調(diào)函數(shù)依次從末尾開始全部執(zhí)行一遍,會(huì)對第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)都會(huì)執(zhí)行。我們給transformFor轉(zhuǎn)換函數(shù)打3個(gè)斷點(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個(gè)斷點(diǎn),看看執(zhí)行transformFor轉(zhuǎn)換函數(shù)之前input標(biāo)簽的node節(jié)點(diǎn)是什么樣的,如下圖:

        從上圖中可以看到input標(biāo)簽的node節(jié)點(diǎn)中還是有一個(gè)v-for的props屬性,說明此時(shí)v-for指令還沒被處理。

        我們接著將代碼走到第2個(gè)斷點(diǎn),看看調(diào)用transformFor轉(zhuǎn)換函數(shù)第1次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后是什么樣的,如下圖:

        從上圖中可以看到原本的input的node節(jié)點(diǎn)已經(jīng)被替換成了一個(gè)新的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。請注意此時(shí)arguments數(shù)組中只有一個(gè)字段,存的是msgList變量。

        我們接著將代碼走到第3個(gè)斷點(diǎn),看看調(diào)用transformFor轉(zhuǎn)換函數(shù)返回的回調(diào)函數(shù),第2次對node節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換之后是什么樣的,如下圖:

        從上圖可以看到arguments數(shù)組中多了一個(gè)字段,input標(biāo)簽現(xiàn)在是當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)。按照我們前面講的洋蔥模型,input子節(jié)點(diǎn)現(xiàn)在已經(jīng)被轉(zhuǎn)換完成了。所以多的這個(gè)字段就是input標(biāo)簽經(jīng)過transform函數(shù)轉(zhuǎn)換后的node節(jié)點(diǎn),將轉(zhuǎn)換后的input子節(jié)點(diǎn)存到父節(jié)點(diǎn)上面,后面生成render函數(shù)時(shí)會(huì)用。

        transformModel轉(zhuǎn)換函數(shù)

        通過前面我們知道了用于處理v-model指令的transformModel轉(zhuǎn)換函數(shù)是在directiveTransforms對象中,只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會(huì)執(zhí)行對應(yīng)的轉(zhuǎn)換函數(shù)。我們這里input上面有v-model指令,所以就會(huì)執(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ù),然后打一個(gè)斷點(diǎn),讓斷點(diǎn)走進(jìn)transformModel函數(shù)中,如下圖:

        從上面的圖中我們可以看到在@vue/compiler-dom包的transformModel函數(shù)中會(huì)調(diào)用@vue/compiler-core包的transformModel函數(shù),拿到返回的baseResult對象后再一些其他操作后直接return baseResult。

        從左邊的call stack調(diào)用棧中我們可以看到transformModel函數(shù)是由一個(gè)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)換,還會(huì)返回一個(gè)props數(shù)組。比如我們這里處理的是v-model指令,返回的props數(shù)組就是由v-model指令編譯而來的props屬性,這就是所謂的v-model語法糖。

        看到這里有的小伙伴會(huì)疑惑了v-model指令不是會(huì)生成modelValueonUpdate:modelValue兩個(gè)屬性,為什么這里只有一個(gè)onUpdate:modelValue屬性呢?

        答案是只有給自定義組件上面使用v-model指令才會(huì)生成modelValueonUpdate:modelValue兩個(gè)屬性,對于這種原生input標(biāo)簽是不需要生成modelValue屬性的,而且input標(biāo)簽本身是不接收名為modelValue屬性,接收的是value屬性。

        總結(jié)

        現(xiàn)在我們再來看看最開始講的流程圖,我想你應(yīng)該已經(jīng)能將整個(gè)流程串起來了。如下圖:

        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ù)都會(huì)依賴這個(gè)上下文對象。

        • 然后執(zhí)行traverseNode函數(shù),traverseNode函數(shù)是一個(gè)典型的洋蔥模型。第一次執(zhí)行traverseNode函數(shù)的時(shí)候會(huì)進(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ù)。此時(shí)就會(huì)進(jìn)入洋蔥模型的第二層,和上一步一樣會(huì)將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),洋蔥模型就會(huì)從“進(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í)行一遍,由于此時(shí)第二層的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ù),會(huì)返回一個(gè)回調(diào)函數(shù)。在回調(diào)函數(shù)中會(huì)調(diào)用buildProps函數(shù),在buildProps函數(shù)中只有當(dāng)node節(jié)點(diǎn)中有對應(yīng)的指令才會(huì)執(zhí)行directiveTransforms對象中對應(yīng)的轉(zhuǎn)換函數(shù)。比如當(dāng)前node節(jié)點(diǎn)有v-model指令,才會(huì)去執(zhí)行transformModel轉(zhuǎn)換函數(shù)。v-model等指令也就被處理了。

        瀏覽 148
        10點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        10點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 成人h视频在线观看无码 | 好粗好大好爽视频 | 古言荒淫h女h | 国产一级在线播放 | 第一页在线视频 |