面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令?

来源:https://www.cnblogs.com/heavenYJJ/p/18142035
-Advertisement-
Play Games

前言 最近有粉絲找到我,說被面試官給問懵了。 粉絲:面試官上來就問“一個vue文件是如何渲染成瀏覽器上面的真實DOM?”,當時還挺竊喜這題真簡單。就簡單說了一下先是編譯成render函數、然後根據render函數生成虛擬DOM,最後就是根據虛擬DOM生成真實DOM。按照正常套路面試官接著會問vue響 ...


前言

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

  • 粉絲:面試官上來就問“一個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函數執行流程圖,讓你對整個流程有個大概的印象,後面的內容看著就不費勁了。如下圖:
full-progress

從上面的流程圖可以看到transform函數的執行過程主要分為下麵這幾步:

  • transform函數中調用createTransformContext函數生成上下文對象。在上下文對象中存儲了當前正在轉換的node節點的信息,後面的traverseNodetraverseChildrennodeTransforms數組中的轉換函數、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.19transform函數在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。找到transform函數的代碼,打上斷點。

從上一篇文章我們知道了transform函數是在node端執行的,所以我們需要啟動一個debug終端,才可以在node端打斷點。這裡以vscode舉例,首先我們需要打開終端,然後點擊終端中的+號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal就可以啟動一個debug終端。
debug-terminal

接著在debug終端中執行yarn dev(這裡是以vite舉例)。在瀏覽器中訪問 http://localhost:5173/,此時斷點就會走到transform函數中了。我們在debug終端中來看看調用transform函數時傳入的root變數,如下圖:
before-transform

從上圖中我們可以看到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上下文對象,對象中會維護一些狀態和方法。比如當前正在轉換的節點是哪個,當前轉換的節點的父節點是哪個,當前節點在父節點中是第幾個子節點,還有replaceNoderemoveNode等方法。

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

我們將斷點走進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位置。如下圖:

traverseChildren

進入時執行的轉換函數

我們現在回過頭來看第一部分的代碼,代碼如下:

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
}

nodeTransformsdirectiveTransforms的區別是,在遞歸遍歷轉換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函數

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

洋蔥模型就是:從外面一層層的進去,再一層層的從裡面出來。

第一次進入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節點是什麼樣的,如下圖:
transformFor1

從上圖中可以看到input標簽的node節點中還是有一個v-for的props屬性,說明此時v-for指令還沒被處理。

我們接著將代碼走到第2個斷點,看看調用transformFor轉換函數第1次對node節點進行轉換之後是什麼樣的,如下圖:
transformFor2

從上圖中可以看到原本的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節點進行轉換之後是什麼樣的,如下圖:
transformFor3

從上圖可以看到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函數中,如下圖:
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轉換函數調用的。如下圖:
directiveTransforms

看名字你應該猜到了buildProps函數的作用是生成props屬性的。點擊Step Out將斷點跳出transformModel函數,走進buildProps函數中,可以看到buildProps函數中調用transformModel函數的代碼如下圖:
buildProps

從上圖中可以看到執行directiveTransforms對象中的轉換函數不僅可以對節點進行轉換,還會返回一個props數組。比如我們這裡處理的是v-model指令,返回的props數組就是由v-model指令編譯而來的props屬性,這就是所謂的v-model語法糖。

看到這裡有的小伙伴會疑惑了v-model指令不是會生成modelValueonUpdate:modelValue兩個屬性,為什麼這裡只有一個onUpdate:modelValue屬性呢?

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

總結

現在我們再來看看最開始講的流程圖,我想你應該已經能將整個流程串起來了。如下圖:
full-progress

transform函數的執行過程主要分為下麵這幾步:

  • transform函數中調用createTransformContext函數生成上下文對象。在上下文對象中存儲了當前正在轉換的node節點的信息,後面的traverseNodetraverseChildrennodeTransforms數組中的轉換函數、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原理文章,我會根據大家的反饋進行創作。
qrcode


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 介紹 圖片預覽在應用開發中是一種常見場景,在諸如QQ、微信、微博等應用中均被廣泛使用。本模塊基於Image組件實現了簡單的圖片預覽功能。 使用說明: 雙指捏合縮放圖片大小 雙擊圖片進行圖片的大小切換 圖片在放大模式下,滑動圖片查看圖片的對應位置 效果圖預覽 實現思路 image組件的objectFi ...
  • 有的時候,我們可能需要多次執行同一塊代碼。一般情況下,語句是按順序執行的:函數中的第一個語句先執行,接著是第二個語句,依此類推。 編程語言提供了更為複雜執行路徑的多種控制結構。 迴圈語句允許我們多次執行一個語句或語句組,下麵是大多數編程語言中迴圈語句的流程圖: for 迴圈 TypeScript ...
  • 一、Shape Shape組件是用於創建2D形狀和粒子效果的組件。它可以創建包括圓形、正方形、三角形和多邊形等基本形狀,同時還可以自定義形狀。Shape組件創建各種不同的效果,例如火花、煙霧、雨滴等。在使用Shape組件時,可以通過編輯頂點、路徑和大小等屬性來控制形狀的外觀和行為。 1.創建 ...
  • 介紹 本示例主要介紹在TaskPool子線程中使用 dlopen 預載入 so 庫並使用句柄調用庫函數的方法,以及在Native中使用 pread 系統函數讀取Rawfile文件的部分文本內容,並添加 HiLog 日誌。 效果圖預覽 使用說明 rawfile路徑下存在一個有內容的文本文件rawfil ...
  • 介紹 MpChart是一個包含各種類型圖表的圖表庫,主要用於業務數據彙總,例如銷售數據走勢圖,股價走勢圖等場景中使用,方便開發者快速實現圖表UI。本示例主要介紹如何使用三方庫MpChart實現柱狀圖UI效果。如堆疊數據類型顯示,Y軸是否顯示,左Y軸位置,右Y軸位置,是否顯示X軸,是否繪製背景色,是否 ...
  • 條件語句用於基於不同的條件來執行不同的動作。 TypeScript 條件語句是通過一條或多條語句的執行結果(True 或 False)來決定執行的代碼塊。 可以通過下圖來簡單瞭解條件語句的執行過程: 條件語句 通常在寫代碼時,您總是需要為不同的決定來執行不同的動作。您可以在代碼中使用條件語句來完成該 ...
  • 一、Image 在HarmonyOS中,Image組件是用於顯示圖像文件的UI組件。它可以顯示本地圖像文件或遠程URL地址的圖像文件。Image組件的實現方式比較簡單,只需提供圖像文件路徑或URL地址即可。 Image通過調用介面來創建,介面調用形式如下: Image(src: string | ...
  • 介紹 本示例介紹用過使用ListItem組件屬性swipeAction實現列表左滑編輯效果的功能。 該場景多用於待辦事項管理、文件管理、備忘錄的記錄管理等。 效果圖預覽 使用說明: 點擊添加按鈕,選擇需要添加的待辦事項。 長按待辦事項,點擊刪除後,被勾選待辦事項被刪除。 左滑單個待辦事項,點擊刪除按 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...