国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

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

這篇具有很好參考價值的文章主要介紹了面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報違法"按鈕提交疑問。

前言

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

  • 粉絲:面試官上來就問“一個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)勁了。如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上面的流程圖可以看到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等指令也就被處理了。

舉個例子

還是同樣的套路,我們通過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終端。
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

接著在debug終端中執(zhí)行yarn dev(這里是以vite舉例)。在瀏覽器中訪問?http://localhost:5173/,此時斷點(diǎn)就會走到transform函數(shù)中了。我們在debug終端中來看看調(diào)用transform函數(shù)時傳入的root變量,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上圖中我們可以看到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位置。如下圖:

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

進(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
}

nodeTransformsdirectiveTransforms的區(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ù)

我們先來看看什么是洋蔥模型,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

洋蔥模型就是:從外面一層層的進(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)是什么樣的,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上圖中可以看到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)換之后是什么樣的,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上圖中可以看到原本的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)換之后是什么樣的,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上圖可以看到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ù)中,如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上面的圖中我們可以看到在@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)用的。如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

看名字你應(yīng)該猜到了buildProps函數(shù)的作用是生成props屬性的。點(diǎn)擊Step Out將斷點(diǎn)跳出transformModel函數(shù),走進(jìn)buildProps函數(shù)中,可以看到buildProps函數(shù)中調(diào)用transformModel函數(shù)的代碼如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

從上圖中可以看到執(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指令不是會生成modelValueonUpdate:modelValue兩個屬性,為什么這里只有一個onUpdate:modelValue屬性呢?

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

總結(jié)

現(xiàn)在我們再來看看最開始講的流程圖,我想你應(yīng)該已經(jīng)能將整個流程串起來了。如下圖:
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?

transform函數(shù)的執(zhí)行過程主要分為下面這幾步:

  • transform函數(shù)中調(diào)用createTransformContext函數(shù)生成上下文對象。在上下文對象中存儲了當(dāng)前正在轉(zhuǎn)換的node節(jié)點(diǎn)的信息,后面的traverseNode、traverseChildrennodeTransforms數(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)作。
面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令?文章來源地址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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • HTML簡單實(shí)現(xiàn)v-if與v-for與v-model

    HTML簡單實(shí)現(xiàn)v-if與v-for與v-model

    首先VIewModel將View和Model連接一起,Model的數(shù)據(jù)改變View的數(shù)據(jù)也變 使用Visual Studio Code 啟動Vue需要vue.js插件和導(dǎo)入CDN(包) vue.js插件:CTRL + shift + x 在搜索欄搜 索vue.js安裝即可 CDN: https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js VS創(chuàng)建一個.html的文件 div /div塊為view層(模板) script /s

    2024年02月06日
    瀏覽(20)
  • vue中v-for重復(fù)數(shù)據(jù)處理||element下拉框去除重復(fù)

    vue中v-for重復(fù)數(shù)據(jù)處理||element下拉框去除重復(fù)

    前端去重方法有多種,只說三種常用的(新老方法都有) 1-使用常規(guī)雙for循環(huán)(暴力算法)遍歷比較的方式對值進(jìn)行比較 2-使用js方法sort排序(只針對數(shù)組),但是經(jīng)常在vue等新框架中提示TypeError: arr.sort is not a function 3-使用reduce方法(_)推薦 前端js各種操作合集備忘 點(diǎn)擊路線后后面車

    2024年02月11日
    瀏覽(28)
  • vue3 v-for遍歷defineProps或者props接收的數(shù)據(jù)時,報“xx” is of type ‘unknown‘

    vue3 v-for遍歷defineProps或者props接收的數(shù)據(jù)時,報“xx” is of type ‘unknown‘

    vue中使用ts,且在使用props或者defineProps進(jìn)行父傳子時,v-for遍歷收到的數(shù)組,進(jìn)行取值時,報“xx” is of type \\\'unknown\\\' 提示:ts進(jìn)行類型推導(dǎo)造成的報錯 提示:使用接口進(jìn)行 提示:創(chuàng)建一個ts文件,放類型數(shù)據(jù),在使用的頁面進(jìn)行引用? ? ? ? ? ? 總結(jié):前兩個都有一個弊端,

    2024年02月16日
    瀏覽(28)
  • TypeScript系列, 通過vue3實(shí)例說說declare module語法怎么用[模塊聲明篇]

    TypeScript系列, 通過vue3實(shí)例說說declare module語法怎么用[模塊聲明篇]

    本系列文章是我20年開始寫的, 這個模塊聲明也是本系列的最后一課, 中間因?yàn)闀r間安排間隔了1年, 當(dāng)時答應(yīng)大家要補(bǔ)充的, 現(xiàn)在來還債??. 中間的時間我寫了vue3的入門教程, 現(xiàn)在寫了一半了吧, 帶視頻的, 如果有需要的小伙伴可以去看看. https://www.yuque.com/books/share/c0ab3348-87ab-4

    2023年04月22日
    瀏覽(28)
  • 教程3 Vue3條件渲染指令(v-if、v-else、v-else-if、v-show、v-for)

    教程3 Vue3條件渲染指令(v-if、v-else、v-else-if、v-show、v-for)

    v-if 指令用于條件性地渲染一塊內(nèi)容。這塊內(nèi)容只會在指令的表達(dá)式返回真值時才被渲染。 可以使用 v-else 為 v-if 添加一個“else 區(qū)塊”。 v-else-if 提供的是相應(yīng)于 v-if 的“else if 區(qū)塊”。它可以連續(xù)多次重復(fù)使用。 可以用來按條件顯示一個元素的指令是 v-show。 v-if、v-else、

    2024年02月11日
    瀏覽(20)
  • 解決:v-model cannot be used on v-for or v-slot scope variables because they are not writable.報錯問題

    解決:v-model cannot be used on v-for or v-slot scope variables because they are not writable.報錯問題

    在使用vue進(jìn)行前端開發(fā)時,可能會遇到循環(huán)渲染input輸入框的需求,當(dāng)使用v-for循環(huán)后,對v-model進(jìn)行值的綁定時,可能會出現(xiàn)以下錯誤,如圖所示: v-model cannot be used on v-for or v-slot scope variables because they are not writable. 錯誤代碼: 通過查閱文檔發(fā)現(xiàn),v-model 不可以直接修改 v-for

    2024年02月16日
    瀏覽(21)
  • vue中的v-for循環(huán)

    如果是一個變量,那么保存的是對象中的屬性值 如果是兩個變量,那么第一個變量保存的是屬性值,第二個變量保存的是屬性名 如果是三個變量,那么第一個變量保存的是屬性值,第二個變量保存的是屬性名,第三個變量保存的是下標(biāo) v-for=\\\"o1 in obj\\\" ? ? ? ? ?o1:屬性值 v-f

    2024年01月17日
    瀏覽(23)
  • Vue-16、Vue列表渲染(v-for的使用)

    Vue-16、Vue列表渲染(v-for的使用)

    1、vue遍歷數(shù)組 也可以這樣寫 2、遍歷對象

    2024年01月21日
    瀏覽(35)
  • Vue 常用指令 v-for 列表循環(huán)

    Vue 常用指令 v-for 列表循環(huán)

    v-for:根據(jù)數(shù)據(jù)生成列表結(jié)構(gòu),并且是響應(yīng)式的,可以十分便捷的操作列表結(jié)構(gòu)了。 至于是什么樣的列表,就看你指令使用的位置了, 列表的生成依賴于數(shù)據(jù),所以先去定義數(shù)據(jù)。 它結(jié)合的類型挺多的,數(shù)組,對象,迭代器,字符串,最常使用的是數(shù)組。這里使用數(shù)組來演

    2024年02月14日
    瀏覽(39)
  • GuLi商城-前端基礎(chǔ)Vue指令v-for

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包