vue3早已具備拋棄虛擬DOM的能力了

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

前言 jquery時代更新視圖是直接對DOM進行操作,缺點是頻繁操作真實 DOM,性能差。react和vue時代引入了虛擬DOM,更新視圖是對新舊虛擬DOM樹進行一層層的遍歷比較,然後找出需要更新的DOM節點進行更新。這樣做的缺點就是如果DOM樹很複雜,在進行新舊DOM樹比較的時候性能就比較差了。那 ...


前言

jquery時代更新視圖是直接對DOM進行操作,缺點是頻繁操作真實 DOM,性能差。react和vue時代引入了虛擬DOM,更新視圖是對新舊虛擬DOM樹進行一層層的遍歷比較,然後找出需要更新的DOM節點進行更新。這樣做的缺點就是如果DOM樹很複雜,在進行新舊DOM樹比較的時候性能就比較差了。那麼有沒有一種方法是不需要去遍歷新舊DOM樹就可以知道哪些DOM需要更新呢?

答案是:在編譯時我們就能夠知道哪些節點是靜態的,哪些是動態的。在更新視圖時只需要對這些動態的節點進行靶向更新,就可以省去對比新舊虛擬DOM帶來的開銷。vue3也是這樣做的,甚至都可以拋棄虛擬DOM。但是考慮到渲染函數的靈活性和需要相容vue2,vue3最終還是保留了虛擬DOM。 這篇文章我們來講講vue3是如何找出動態節點,以及響應式變數修改後如何靶向更新。 註:本文使用的vue版本為3.4.19

靶向更新的流程

先來看看我畫的整個靶向更新的流程,如下圖:
full-progress

整個流程主要分為兩個大階段:編譯時和運行時。

  • 編譯時階段找出動態節點,使用patchFlag屬性將其標記為動態節點。

  • 運行時階段分為兩塊:執行render函數階段和更新視圖階段。

    • 執行render函數階段會找出所有被標記的動態節點,將其塞到block節點的dynamicChildren屬性數組中。

    • 更新視圖階段會從block節點的dynamicChildren屬性數組中拿到所有的動態節點,然後遍歷這個數組將裡面的動態節點進行靶向更新。

一個簡單的demo

還是同樣的套路,我們通過debug一個demo,來搞清楚vue3是如何找出動態節點以及響應式變數修改後如何靶向更新的,demo代碼如下:

<template>
  <div>
    <h1>title</h1>
    <p>{{ msg }}</p>
    <button @click="handleChange">change msg</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const msg = ref("hello");

function handleChange() {
  msg.value = "world";
}
</script>

p標簽綁定了響應式變數msg,點擊button按鈕時會將msg變數的值從hello更新為world。

在之前的文章中我們知道了vue分為編譯時和運行時,由於p標簽使用了msg響應式變數,所以在編譯時就會找出p標簽。並且將其標記為動態節點,而這裡的h1標簽由於沒有使用響應式變數,所以不會被標記為動態節點。

在運行時階段點擊button按鈕修改msg變數的值,由於我們在編譯階段已經將p標簽標記為了動態節點,所以此時只需要將標記的p標簽動態節點中的文本更新為最新的值即可,省去了傳統patch函數中的比較新舊虛擬DOM的步驟。

編譯階段

在之前的 面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令?文章中我們講過了在編譯階段對vue內置的指令、模版語法是在transform函數中處理的。在transform函數中實際幹活的是一堆轉換函數,每種轉換函數都有不同的作用。比如v-for標簽就是由transformFor轉換函數處理的,而將節點標記為動態節點就是在transformElement轉換函數中處理的。

首先我們需要啟動一個debug終端,才可以在node端打斷點。這裡以vscode舉例,首先我們需要打開終端,然後點擊終端中的+號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal就可以啟動一個debug終端。
debug-terminal
然後給transformElement函數打個斷點,transformElement函數在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。

transformElement轉換函數

接著在debug終端中執行yarn dev(這裡是以vite舉例)。在瀏覽器中訪問 http://localhost:5173/,此時斷點就會走到transformElement函數中了。我們看到transformElement函數中的代碼是下麵這樣的:

const transformElement = (node, context) => {
  return function postTransformElement() {
    // ...
  }
}

從上面可以看到transformElement函數中沒有做任何事情,直接返回了一個名為postTransformElement的回調函數,我們接著給這個回調函數打上斷點,將transformElement函數的斷點給移除了。

每處理一個node節點都會走進一次postTransformElement函數這個斷點,將斷點放了,直到斷點走進處理到使用響應式變數的p標簽node節點時。在我們這個場景中簡化後的postTransformElement函數代碼如下:

const transformElement = (node, context) => {
  return function postTransformElement() {
    // 第一部分
    let vnodePatchFlag;
    let patchFlag = 0;
    const child = node.children[0];
    const type = child.type;

    // 第二部分
    const hasDynamicTextChild =
      type === NodeTypes.INTERPOLATION ||
      type === NodeTypes.COMPOUND_EXPRESSION;
    if (
      hasDynamicTextChild &&
      getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
    ) {
      patchFlag |= PatchFlags.TEXT;
    }
    if (patchFlag !== 0) {
      vnodePatchFlag = String(patchFlag)
    }

    // 第三部分
    node.codegenNode = createVNodeCall(
      vnodePatchFlag
      // ...省略
    );
  };
};

從上面可以看到簡化後的postTransformElement函數主要分為三部分,其實很簡單。

第一部分

第一部分很簡單定義了vnodePatchFlagpatchFlag這兩個變數,patchFlag變數的作用是標記節點是否為動態節點,vnodePatchFlag變數除了標記節點為動態節點之外還保存了一些額外的動態節點信息。child變數中存的是當前節點的子節點,type變數中存的是當前子節點的節點類型。

第二部分

const hasDynamicTextChild =
  type === NodeTypes.INTERPOLATION ||
  type === NodeTypes.COMPOUND_EXPRESSION;

我們接著來看第二部分,其中的hasDynamicTextChild變數表示當前子節點是否為動態文本子節點,很明顯我們這裡的p標簽使用了響應式變數msg,其文本子節點當然是動態的,所以hasDynamicTextChild變數的值為true。

接著我們來看第二部分的這段if語句:

if (
  hasDynamicTextChild &&
  getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
  patchFlag |= PatchFlags.TEXT;
}

我們先來看這段if語句的條件,如果hasDynamicTextChild為true表示當前子節點是動態文本子節點。getConstantType函數是判斷動態文本節點涉及到的變數是不是不會改變的常量,為什麼判斷了hasDynamicTextChild還要判斷getConstantType呢?

答案是如果我們給p標簽綁定一個不會改變的常量,因為確實綁定了變數,hasDynamicTextChild的值還是為true。但是由於我們綁定的是不會改變的常量,所以p標簽中的文本節點永遠都不會改變。比如下麵這個demo:

<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>

<script setup lang="ts">
const count = 10;
</script>

我們接著來看if語句裡面的內容patchFlag |= PatchFlags.TEXT,如果if的判斷結果為true,那麼就使用“按位或”的運算符。由於此時的patchFlag變數的值為0,所以經過“按位或”的運算符計算下來patchFlag變數的值變成了PatchFlags.TEXT變數的值。我們先來看看PatchFlags中有哪些值:

enum PatchFlags {
  TEXT = 1,         // 二進位值為 1
  CLASS = 1 << 1,   // 二進位值為 10
  STYLE = 1 << 2,   // 二進位值為 100
  // ...等等等
}

這裡涉及到了位運算 <<,他的意思是向左移多少位。比如TEXT表示向左移0位,二進位表示為1。CLASS表示為左移一位,二進位表示為10。STYLE表示為左移兩位,二進位表示為100。

現在你明白了為什麼給patchFlag賦值要使用“按位或”的運算符了吧,假如當前p標簽除了有動態的文本節點,還有動態的class。那麼patchFlag就會進行兩次賦值,分別是:patchFlag |= PatchFlags.TEXTpatchFlag |= PatchFlags.CLASS。經過兩次“按位或”的運算符進行計算後,patchFlag的二進位值就是11,二進位值信息中包含動態文本節點和動態class,從右邊數的第一位1表示動態文本節點,從右邊數的第二位1表示動態class。如下圖:
or

這樣設計其實很精妙,後面拿到動態節點進行更新時,只需要將動態節點的patchFlagPatchFlags中的枚舉進行&"按位與"運算就可以知道當前節點是否是動態文本節點、動態class的節點。上面之所以沒有涉及到PatchFlags.CLASS相關的代碼,是因為當前例子中不存在動態class,所以我省略了。

我們接著來看第二部分的第二個if語句,如下:

if (patchFlag !== 0) {
  vnodePatchFlag = String(patchFlag)
}

這段代碼很簡單,如果patchFlag !== 0表示當前節點是動態節點。然後將patchFlag轉換為字元串賦值給vnodePatchFlag變數,在dev環境中vnodePatchFlag字元串中還包含節點是哪種動態類型的信息。如下圖:
vnodePatchFlag

第三部分

我們接著將斷點走到第三部分,這一塊也很簡單。將createVNodeCall方法的返回值賦值給codegenNode屬性,codegenNode屬性中存的就是節點經過transform轉換函數處理後的信息。

node.codegenNode = createVNodeCall(
  vnodePatchFlag
  // ...省略
);

我們將斷點走到執行完createVNodeCall函數後,看看當前的p標簽節點是什麼樣的。如下圖:
codegenNode

從上圖中可以看到此時的p標簽的node節點中有了一個patchFlag屬性,經過編譯處理後p標簽已經被標記成了動態節點。

執行render函數階段

經過編譯階段的處理p標簽已經被標記成了動態節點,並且生成了render函數。此時編譯階段的任務已經完了,該到瀏覽器中執行的運行時階段了。首先我們要在瀏覽器中找到編譯後的js文件。

其實很簡單直接在network上面找到你的那個vue文件就行了,比如我這裡的文件是index.vue,那我只需要在network上面找叫index.vue的文件就行了。但是需要註意一下network上面有兩個index.vue的js請求,分別是template模塊+script模塊編譯後的js文件,和style模塊編譯後的js文件。

那怎麼區分這兩個index.vue文件呢?很簡單,通過query就可以區分。由style模塊編譯後的js文件的URL中有type=style的query,如下圖所示:
network

接下來我們來看看編譯後的index.vue,簡化的代碼如下:

import {
  createElementBlock as _createElementBlock,
  createElementVNode as _createElementVNode,
  defineComponent as _defineComponent,
  openBlock as _openBlock,
  toDisplayString as _toDisplayString,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";

const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    // ...省略
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _createElementVNode("h1", null, "title", -1),
      _createElementVNode(
        "p",
        null,
        _toDisplayString($setup.msg),
        1
        /* TEXT */
      ),
      _createElementVNode(
        "button",
        { onClick: $setup.handleChange },
        "change msg"
      ),
    ])
  );
}
_sfc_main.render = _sfc_render;
export default _sfc_main;

從上面的代碼可以看到經過編譯後生成了一個render函數,執行這個render函數就會生成虛擬DOM。仔細來看這個render函數的返回值結構,這裡使用return返回了一個括弧。在括弧中有兩項,分別是openBlock函數的返回值和createElementBlock函數的返回值。那麼這裡的return返回的到底是什麼呢?

答案是會先執行openBlock函數,然後將createElementBlock函數執行後的值返回。

現在我們思考一個問題,在編譯階段我們只是將p標簽標記成了動態節點,如果還有其他標簽也是動態節點那麼也會將其標記成動態節點。這些動態節點的標記還是在DOM樹中的每個標簽中,如果響應式變數的值改變,那麼豈不還是需要去遍歷DOM樹?

答案是在執行render函數生成虛擬DOM的時候會生成一個block節點作為根節點,並且將這些標記的動態節點收集起來塞到block根節點的dynamicChildren屬性數組中。在dynamicChildren屬性數組中存的是平鋪的DOM樹中的所有動態節點,和動態節點在DOM樹中的位置無關。

那麼根block節點又是怎麼收集到所有的動態子節點的呢?

我們先來搞清楚render函數中的那一堆嵌套函數的執行順序,我們前面已經講過了首先會執行返回的括弧中的第一項openBlock函數,然後再執行括弧中的第二項createElementBlock函數。createElementBlock函數是一個層層嵌套的結構,執行順序是內層先執行,外層再執行。所以接下來會先執行裡層createElementVNode生成h1標簽的虛擬DOM,然後執行createElementVNode生成p標簽的虛擬DOM,最後執行createElementVNode生成button標簽的虛擬DOM。內層的函數執行完了後再去執行外層的createElementBlock生成div標簽的虛擬DOM。如下圖:
sort

從上圖中可以看到render函數中主要就執行了這三個函數:

  • openBlock函數

  • createElementVNode函數

  • createElementBlock函數

openBlock函數

我們先來看最先執行的openBlock函數,在我們這個場景中簡化後的代碼如下:

let currentBlock;

function openBlock() {
  currentBlock = [];
}

首先會定義一個全局變數currentBlock,裡面會存DOM樹中的所有的動態節點。在openBlock函數中會將其初始化為一個空數組,所以openBlock函數需要第一個執行。

createElementVNode函數

我們接著來看createElementVNode函數,在我們這個場景中簡化後的代碼如下:

export { createBaseVNode as createElementVNode };

function createBaseVNode() {
  const vnode = {
    // ...省略
  };
  if (vnode.patchFlag > 0) {
    currentBlock.push(vnode);
  }
  return vnode;
}

createElementVNode函數在內部其實叫createBaseVNode函數,從上面的代碼中可以看到他除了會生成虛擬DOM之外,還會去判斷當前節點是否為動態節點。如果是動態節點,那麼就將其push到全局的currentBlock數組中。比如我們這裡的p標簽綁定了msg變數,當執行createElementVNode函數生成p標簽的虛擬DOM時就會將p標簽的node節點收集起來push到currentBlock數組中。

createElementBlock函數

我們來看最後執行的createElementBlock函數,在我們這個場景中簡化後的代碼如下:

function createElementBlock() {
  return setupBlock(
    createBaseVNode()
    // ...省略
  );
}

createElementBlock函數會先執行createBaseVNode也就是上一步說的createElementVNode函數生成最外層div標簽對應的虛擬DOM。由於外層div標簽沒有被標記為動態節點,所以執行createElementVNode函數也就只生成div標簽的虛擬DOM。

然後將div標簽的虛擬DOM作為參數去執行setupBlock函數,setupBlock函數的代碼如下:

function setupBlock(vnode) {
  vnode.dynamicChildren = currentBlock;
  return vnode;
}

此時子節點生成虛擬DOM的createElementVNode函數全部都已經執行完了,這個div標簽也就是我們的根節點,

我們前面講過了執行順序是內層先執行,外層再執行,所以執行到最外層的div標簽時,子節點已經全部都執行完成了。此時currentBlock數組中已經存了所有的動態子節點,將currentBlock數組賦值給根block節點(這裡是div節點)的dynamicChildren屬性。

現在你知道我們前面提的那個問題,根block節點是怎麼收集到所有的動態子節點的呢?

後續更新視圖執行patch函數時只需要拿到根節點的dynamicChildren屬性,就可以拿到DOM樹中的所有動態子節點。

更新視圖階段

當響應式變數改變後,對應的視圖就需要更新。對應我們這個場景中就是,點擊button按鈕後,p標簽中的內容從原來的hello,更新為world。

按照傳統的patch函數此時需要去遍歷比較老的虛擬DOM和新的虛擬DOM,然後找出來p標簽是需要修改的node節點,然後將其文本節點更新為最新值"world"。

但是我們在上一步生成虛擬DOM階段已經將DOM樹中所有的動態節點收集起來,存在了根block節點的dynamicChildren屬性中。我們接著來看在新的patch函數中是如何讀取dynamicChildren屬性,以及如何將p標簽的文本節點更新為最新值"world"。

處理div根節點

在source面板中找到vue源碼中的patch函數,給patch函數打上斷點。然後點擊button按鈕修改msg變數的值,導致render函數重新執行,接著會走進了patch函數進行視圖更新。此時代碼已經走到了patch函數的斷點,在我們這個場景中簡化後的patch函數代碼如下:

const patch = (n1, n2) => {
  processElement(n1, n2);
};

從上面可以看到簡化後的patch函數中實際是調用了processElement函數,接著將斷點走進processElement函數,在我們這個場景中簡化後的processElement函數代碼如下:

const processElement = (n1, n2) => {
  patchElement(n1, n2);
};

從上面可以看到在processElement函數中依然不是具體實現視圖更新的地方,在裡面調用了patchElement函數。接著將斷點走進patchElement函數,在我們這個場景中簡化後的patchElement函數代碼如下:

const patchElement = (n1, n2) => {
  const el = (n2.el = n1.el);
  let { patchFlag, dynamicChildren } = n2;
  patchFlag = n1.patchFlag;

  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }

  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // 處理動態class
    }
    if (patchFlag & PatchFlags.STYLE) {
      // 處理動態style
    }
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children);
      }
    }
  }
};

從上面可以看到patchElement函數是實際幹活的地方了,我們在控制臺中來看看接收n1、n2這兩個參數是什麼樣的。

先來看看n1舊虛擬DOM ,如下圖:
n1

從上面可以看到此時的n1為根block節點,此時p標簽中的文本還是更新前的文本"hello",dynamicChildren屬性為收集到的所有動態子節點。

接著來看n2新虛擬DOM,如下圖:
n2

從上面可以看到新虛擬DOM中p標簽中的文本節點已經是更新後的文本"world"了。

我們接著來看patchElement函數中的代碼,第一次處理div根節點時patchElement函數中只會執行部分代碼。後面處理p標簽時還會走進patchElement函數才會執行剩下的代碼,當前執行的代碼如下:

const patchElement = (n1, n2) => {
  let { patchFlag, dynamicChildren } = n2;
  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }
};

從根block節點(也就是n2新虛擬DOM)中拿到dynamicChildren。這個dynamicChildren數組我們前面講過了,裡面存的是DOM樹中所有的動態節點。然後調用patchBlockChildren函數去處理所有的動態節點,我們將斷點走進patchBlockChildren函數中,在我們這個場景中簡化後的patchBlockChildren函數代碼如下:

const patchBlockChildren = (oldChildren, newChildren) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i];
    const newVNode = newChildren[i];
    patch(oldVNode, newVNode);
  }
};

patchBlockChildren函數中會去遍歷所有的動態子節點,在我們這個場景中,oldVNode也就是舊的p標簽的node節點,newVNode是新的p標簽的node節點。然後再去調用patch函數將這個p標簽動態節點更新為最新的文本節點。

如果按照vue2傳統的patch函數的流程,應該是進行遍歷舊的n1虛擬DOM和新的n2虛擬DOM。然後才能找出p標簽是需要更新的節點,接著執行上面的patch(oldVNode, newVNode)將p標簽更新為最新的文本節點。

而在vue3中由於我們在編譯階段就找出來p標簽是動態節點,然後將其收集到根block節點的dynamicChildren屬性中。在更新階段執行patch函數時,就省去了遍歷比較新舊虛擬DOM的過程,直接從dynamicChildren屬性中就可以將p標簽取出來將其更新為最新的文本節點。

處理p標簽節點

我們接著來看此時執行patch(oldVNode, newVNode)是如何處理p標簽的。前面已經講過了patch函數進行層層調用後實際幹活的是patchElement函數,將斷點走進patchElement函數。再來回憶一下前面講的patchElement函數代碼:

const patchElement = (n1, n2) => {
  const el = (n2.el = n1.el);
  let { patchFlag, dynamicChildren } = n2;
  patchFlag = n1.patchFlag;

  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // 處理動態class
    }
    if (patchFlag & PatchFlags.STYLE) {
      // 處理動態style
    }
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children);
      }
    }
  }
};

此時的n1就是p標簽舊的虛擬DOM節點,n2就是p標簽新的虛擬DOM節點。我們在編譯時通過給p標簽添加patchFlag屬性將其標記為動態節點,並沒有給p標簽賦值dynamicChildren屬性。所以此時不會像處理block根節點一樣去執行patchBlockChildren函數了,而是會走後面的邏輯。

還記得我們前面講的是如何給p標簽設置patchFlag屬性嗎?

定義了一個PatchFlags枚舉:

enum PatchFlags {
  TEXT = 1,         // 二進位值為 1
  CLASS = 1 << 1,   // 二進位值為 10
  STYLE = 1 << 2,   // 二進位值為 100
  // ...等等等
}

由於一個節點可能同時是:動態文本節點、動態class節點、動態style節點。所以patchFlag中需要包含這些信息。

如果是動態文本節點,那就執行“按位或”運算符:patchFlag |= PatchFlags.TEXT。執行後patchFlag的二進位值為1

如果也是動態class節點,在前一步的執行結果基礎上再次執行“按位或”運算符:patchFlag |= PatchFlags.CLASS。執行後patchFlag的二進位值為11

如果也是動態style節點,同樣在前一步的執行結果基礎上再次執行“按位或”運算符:patchFlag |= PatchFlags.STYLE。執行後patchFlag的二進位值為111

我們前面給p標簽標記為動態節點時給c。在patchElement函數中使用patchFlag屬性進行"按位與"運算,判斷當前節點是否是動態文本節點、動態class節點、動態style節點。

patchFlag的值是1,轉換為兩位的二進位後是01。PatchFlags.CLASS1 << 1,轉換為二進位值為10。01和10進行&(按位與)操作,計算下來的值為00。所以patchFlag & PatchFlags.CLASS轉換為布爾值後為false,說明當前p標簽不是動態class標簽。如下圖:
class

同理將patchFlag轉換為三位的二進位後是001。PatchFlags.STYLE1 << 2,轉換為二進位值為100。001和100進行&(按位與)操作,計算下來的值為000。所以patchFlag & PatchFlags.CLASS轉換為布爾值後為false,說明當前p標簽不是動態style標簽。如下圖:
style

同理將patchFlag轉換為一位的二進位後還是1。PatchFlags.TEXT為1,轉換為二進位值還是1。1和1進行&(按位與)操作,計算下來的值為1。所以patchFlag & PatchFlags.TEXT轉換為布爾值後為true,說明當前p標簽是動態文本標簽。如下圖:
text

判斷到當前節點是動態文本節點,然後使用n1.children !== n2.children判斷新舊文本是否相等。如果不相等就傳入eln2.children執行hostSetElementText函數,其中的el為當前p標簽,n2.children為新的文本。我們來看看hostSetElementText函數的代碼,如下:

function setElementText(el, text) {
  el.textContent = text;
}

setElementText函數中的textContent屬性你可能用的比較少,他的作用和innerText差不多。給textContent屬性賦值就是設置元素的文字內容,在這裡就是將p標簽的文本設置為最新值"world"。

至此也就實現了當響應式變數msg修改後,靶向更新p標簽中的節點。

總結

現在來看我們最開始講的整個靶向更新的流程圖你應該很容易理解了,如下圖:
full-progress

整個流程主要分為兩個大階段:編譯時和運行時。

  • 編譯時階段找出動態節點,使用patchFlag屬性將其標記為動態節點。

  • 運行時階段分為兩塊:執行render函數階段和更新視圖階段。

    • 執行render函數階段會找出所有被標記的動態節點,將其塞到block節點的dynamicChildren屬性數組中。

    • 更新視圖階段會從block節點的dynamicChildren屬性數組中拿到所有的動態節點,然後遍歷這個數組將裡面的動態節點進行靶向更新。

如果使用了v-for或者v-if這種會改變html結構的指令,那麼就不只有根節點是block節點了。v-forv-if的節點都會生成block節點,此時的這些block節點就組成了一顆block節點樹。如果小伙伴們對使用了v-for或者v-if是如何實現靶向更新感興趣,可以參考本文的debug方式去探索。又或者在評論區留言,我會在後面的文章中安排上。

在實驗階段的Vue Vapor中已經拋棄了虛擬DOM,更多關於Vue Vapor的內容可以查看我之前的文章: 沒有虛擬DOM版本的vue(Vue Vapor)。根據vue團隊成員三咲智子 所透露未來將使用<script vapor>的方式去區分Vapor組件和目前的組件。
vapor

關註(圖1)公眾號:【前端歐陽】,解鎖我更多vue原理文章。
加我(圖2)微信回覆「666」,免費領取歐陽研究vue源碼過程中收集的源碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。
公眾號微信


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

-Advertisement-
Play Games
更多相關文章
  • 大數據面試SQL每日一題系列:最高峰同時線上主播人數。位元組,快手等大廠高頻面試題 之後會不定期更新每日一題sql系列。 SQL面試題每日一題系列內容均來自於網路以及實際使用情況收集,如有雷同,純屬巧合。 1.題目 問題1:如下為某直播平臺各主播的開播及關播時間數據明細,現在需要計算該平臺最高峰期同時 ...
  • 一、下載mysql安裝包 官網:https://dev.mysql.com/downloads/mysql/ 預設會跳轉到最新版本的下載頁面,也可以在舊版本集中選擇需要安裝的版本。 MSI Installer是安裝程式,ZIP Archive是壓縮包形式。 二、安裝mysql MSI安裝程式會有圖形 ...
  • 摘要:作為Valkey社區的Technical Steering Committee member,華為雲將持續參與社區建設。 一、背景 今年3月21日,Redis Labs宣佈從Redis 7.4版本開始,將原先比較寬鬆的BSD源碼使用協議修改為RSAv2和SSPLv1協議,意味著 Redis在O ...
  • 本文介紹基於Microsoft SQL Server軟體,實現資料庫表中多種數據查詢方法的具體操作。 目錄1 指定列或全部列查詢——查詢S表學生記錄2 指定列或全部列查詢——查詢學生姓名與出生年份3 按條件查詢及模糊查詢——查詢成績不及格學生學號4 按條件查詢及模糊查詢——查詢20-23歲間學生姓名 ...
  • 在Kafka中,Broker、Topic、Partition和Replication是四個核心概念,它們各自扮演了不同的角色並共同協作以確保數據的可靠性、可擴展性和高性能。以下是關於這四個概念的詳細解釋: Broker(代理) * Broker是Kafka集群中的一個節點,負責存儲和轉發消息。Kaf ...
  • 一、是什麼 當對一個文檔進行佈局(layout)的時候,瀏覽器的渲染引擎會根據標準之一的 CSS 基礎框盒模型(CSS basic box model),將所有元素表示為一個個矩形的盒子(box) 一個盒子由四個部分組成:content、padding、border、margin content,即 ...
  • 本文的目的,是為了讓已經有 Vue2 開發經驗的 人 ,快速掌握 Vue3 的寫法。 因此, 本篇假定你已經掌握 Vue 的核心內容 ,只為你介紹編寫 Vue3 代碼,需要瞭解的內容。 一、Vue3 里 script 的三種寫法 首先,Vue3 新增了一個叫做組合式 api 的東西,英文名叫 Com ...
  • 最近,群里在討論一個很有意思的線條動畫效果,效果大致如下: 簡單而言,就是線條沿著不規則路徑的行進動畫,其中的線條動畫可以理解為是特殊的光效。 本文,我們將一起探索,看看在不使用 JavaScript/Canvas 的基礎上,使用純 CSS/SVG 的方式,我們可以如何大致的還原上述的線條動畫效果。 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...