<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

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

    共 19734字,需瀏覽 40分鐘

     ·

    2024-05-16 08:45

    前言

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

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

    靶向更新的流程

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

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

    • 編譯時階段找出動態(tài)節(jié)點,使用patchFlag屬性將其標記為動態(tài)節(jié)點。

    • 運行時階段分為兩塊:執(zhí)行render函數(shù)階段和更新視圖階段。

      • 執(zhí)行render函數(shù)階段會找出所有被標記的動態(tài)節(jié)點,將其塞到block節(jié)點的dynamicChildren屬性數(shù)組中。

      • 更新視圖階段會從block節(jié)點的dynamicChildren屬性數(shù)組中拿到所有的動態(tài)節(jié)點,然后遍歷這個數(shù)組將里面的動態(tài)節(jié)點進行靶向更新。

    一個簡單的demo

    還是同樣的套路,我們通過debug一個demo,來搞清楚vue3是如何找出動態(tài)節(jié)點以及響應(yīng)式變量修改后如何靶向更新的,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標簽綁定了響應(yīng)式變量msg,點擊button按鈕時會將msg變量的值從hello更新為world。

    在之前的文章中我們知道了vue分為編譯時和運行時,由于p標簽使用了msg響應(yīng)式變量,所以在編譯時就會找出p標簽。并且將其標記為動態(tài)節(jié)點,而這里的h1標簽由于沒有使用響應(yīng)式變量,所以不會被標記為動態(tài)節(jié)點。

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

    編譯階段

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

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

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

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

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

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

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

    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函數(shù)主要分為三部分,其實很簡單。

    第一部分

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

    第二部分

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

    我們接著來看第二部分,其中的hasDynamicTextChild變量表示當前子節(jié)點是否為動態(tài)文本子節(jié)點,很明顯我們這里的p標簽使用了響應(yīng)式變量msg,其文本子節(jié)點當然是動態(tài)的,所以hasDynamicTextChild變量的值為true。

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

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

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

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

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

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

    我們接著來看if語句里面的內(nèi)容patchFlag |= PatchFlags.TEXT,如果if的判斷結(jié)果為true,那么就使用“按位或”的運算符。由于此時的patchFlag變量的值為0,所以經(jīng)過“按位或”的運算符計算下來patchFlag變量的值變成了PatchFlags.TEXT變量的值。我們先來看看PatchFlags中有哪些值:

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

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

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

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

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

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

    這段代碼很簡單,如果patchFlag !== 0表示當前節(jié)點是動態(tài)節(jié)點。然后將patchFlag轉(zhuǎn)換為字符串賦值給vnodePatchFlag變量,在dev環(huán)境中vnodePatchFlag字符串中還包含節(jié)點是哪種動態(tài)類型的信息。如下圖:

    第三部分

    我們接著將斷點走到第三部分,這一塊也很簡單。將createVNodeCall方法的返回值賦值給codegenNode屬性,codegenNode屬性中存的就是節(jié)點經(jīng)過transform轉(zhuǎn)換函數(shù)處理后的信息。

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

    我們將斷點走到執(zhí)行完createVNodeCall函數(shù)后,看看當前的p標簽節(jié)點是什么樣的。如下圖:

    從上圖中可以看到此時的p標簽的node節(jié)點中有了一個patchFlag屬性,經(jīng)過編譯處理后p標簽已經(jīng)被標記成了動態(tài)節(jié)點。

    執(zhí)行render函數(shù)階段

    經(jīng)過編譯階段的處理p標簽已經(jīng)被標記成了動態(tài)節(jié)點,并且生成了render函數(shù)。此時編譯階段的任務(wù)已經(jīng)完了,該到瀏覽器中執(zhí)行的運行時階段了。首先我們要在瀏覽器中找到編譯后的js文件。

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

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

    接下來我們來看看編譯后的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;

    從上面的代碼可以看到經(jīng)過編譯后生成了一個render函數(shù),執(zhí)行這個render函數(shù)就會生成虛擬DOM。仔細來看這個render函數(shù)的返回值結(jié)構(gòu),這里使用return返回了一個括號。在括號中有兩項,分別是openBlock函數(shù)的返回值和createElementBlock函數(shù)的返回值。那么這里的return返回的到底是什么呢?

    答案是會先執(zhí)行openBlock函數(shù),然后將createElementBlock函數(shù)執(zhí)行后的值返回。

    現(xiàn)在我們思考一個問題,在編譯階段我們只是將p標簽標記成了動態(tài)節(jié)點,如果還有其他標簽也是動態(tài)節(jié)點那么也會將其標記成動態(tài)節(jié)點。這些動態(tài)節(jié)點的標記還是在DOM樹中的每個標簽中,如果響應(yīng)式變量的值改變,那么豈不還是需要去遍歷DOM樹?

    答案是在執(zhí)行render函數(shù)生成虛擬DOM的時候會生成一個block節(jié)點作為根節(jié)點,并且將這些標記的動態(tài)節(jié)點收集起來塞到block根節(jié)點的dynamicChildren屬性數(shù)組中。在dynamicChildren屬性數(shù)組中存的是平鋪的DOM樹中的所有動態(tài)節(jié)點,和動態(tài)節(jié)點在DOM樹中的位置無關(guān)。

    那么根block節(jié)點又是怎么收集到所有的動態(tài)子節(jié)點的呢?

    我們先來搞清楚render函數(shù)中的那一堆嵌套函數(shù)的執(zhí)行順序,我們前面已經(jīng)講過了首先會執(zhí)行返回的括號中的第一項openBlock函數(shù),然后再執(zhí)行括號中的第二項createElementBlock函數(shù)。createElementBlock函數(shù)是一個層層嵌套的結(jié)構(gòu),執(zhí)行順序是內(nèi)層先執(zhí)行,外層再執(zhí)行。所以接下來會先執(zhí)行里層createElementVNode生成h1標簽的虛擬DOM,然后執(zhí)行createElementVNode生成p標簽的虛擬DOM,最后執(zhí)行createElementVNode生成button標簽的虛擬DOM。內(nèi)層的函數(shù)執(zhí)行完了后再去執(zhí)行外層的createElementBlock生成div標簽的虛擬DOM。如下圖:

    從上圖中可以看到render函數(shù)中主要就執(zhí)行了這三個函數(shù):

    • openBlock函數(shù)

    • createElementVNode函數(shù)

    • createElementBlock函數(shù)

    openBlock函數(shù)

    我們先來看最先執(zhí)行的openBlock函數(shù),在我們這個場景中簡化后的代碼如下:

    let currentBlock;

    function openBlock() {
      currentBlock = [];
    }

    首先會定義一個全局變量currentBlock,里面會存DOM樹中的所有的動態(tài)節(jié)點。在openBlock函數(shù)中會將其初始化為一個空數(shù)組,所以openBlock函數(shù)需要第一個執(zhí)行。

    createElementVNode函數(shù)

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

    export { createBaseVNode as createElementVNode };

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

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

    createElementBlock函數(shù)

    我們來看最后執(zhí)行的createElementBlock函數(shù),在我們這個場景中簡化后的代碼如下:

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

    createElementBlock函數(shù)會先執(zhí)行createBaseVNode也就是上一步說的createElementVNode函數(shù)生成最外層div標簽對應(yīng)的虛擬DOM。由于外層div標簽沒有被標記為動態(tài)節(jié)點,所以執(zhí)行createElementVNode函數(shù)也就只生成div標簽的虛擬DOM。

    然后將div標簽的虛擬DOM作為參數(shù)去執(zhí)行setupBlock函數(shù),setupBlock函數(shù)的代碼如下:

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

    此時子節(jié)點生成虛擬DOM的createElementVNode函數(shù)全部都已經(jīng)執(zhí)行完了,這個div標簽也就是我們的根節(jié)點,

    我們前面講過了執(zhí)行順序是內(nèi)層先執(zhí)行,外層再執(zhí)行,所以執(zhí)行到最外層的div標簽時,子節(jié)點已經(jīng)全部都執(zhí)行完成了。此時currentBlock數(shù)組中已經(jīng)存了所有的動態(tài)子節(jié)點,將currentBlock數(shù)組賦值給根block節(jié)點(這里是div節(jié)點)的dynamicChildren屬性。

    現(xiàn)在你知道我們前面提的那個問題,根block節(jié)點是怎么收集到所有的動態(tài)子節(jié)點的呢?

    后續(xù)更新視圖執(zhí)行patch函數(shù)時只需要拿到根節(jié)點的dynamicChildren屬性,就可以拿到DOM樹中的所有動態(tài)子節(jié)點。

    更新視圖階段

    當響應(yīng)式變量改變后,對應(yīng)的視圖就需要更新。對應(yīng)我們這個場景中就是,點擊button按鈕后,p標簽中的內(nèi)容從原來的hello,更新為world。

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

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

    處理div根節(jié)點

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

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

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

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

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

    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) {
          // 處理動態(tài)class
        }
        if (patchFlag & PatchFlags.STYLE) {
          // 處理動態(tài)style
        }
        if (patchFlag & PatchFlags.TEXT) {
          if (n1.children !== n2.children) {
            hostSetElementText(el, n2.children);
          }
        }
      }
    };

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

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

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

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

    從上面可以看到新虛擬DOM中p標簽中的文本節(jié)點已經(jīng)是更新后的文本"world"了。

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

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

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

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

    patchBlockChildren函數(shù)中會去遍歷所有的動態(tài)子節(jié)點,在我們這個場景中,oldVNode也就是舊的p標簽的node節(jié)點,newVNode是新的p標簽的node節(jié)點。然后再去調(diào)用patch函數(shù)將這個p標簽動態(tài)節(jié)點更新為最新的文本節(jié)點。

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

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

    處理p標簽節(jié)點

    我們接著來看此時執(zhí)行patch(oldVNode, newVNode)是如何處理p標簽的。前面已經(jīng)講過了patch函數(shù)進行層層調(diào)用后實際干活的是patchElement函數(shù),將斷點走進patchElement函數(shù)。再來回憶一下前面講的patchElement函數(shù)代碼:

    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) {
          // 處理動態(tài)class
        }
        if (patchFlag & PatchFlags.STYLE) {
          // 處理動態(tài)style
        }
        if (patchFlag & PatchFlags.TEXT) {
          if (n1.children !== n2.children) {
            hostSetElementText(el, n2.children);
          }
        }
      }
    };

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

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

    定義了一個PatchFlags枚舉:

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

    由于一個節(jié)點可能同時是:動態(tài)文本節(jié)點、動態(tài)class節(jié)點、動態(tài)style節(jié)點。所以patchFlag中需要包含這些信息。

    如果是動態(tài)文本節(jié)點,那就執(zhí)行“按位或”運算符:patchFlag |= PatchFlags.TEXT。執(zhí)行后patchFlag的二進制值為1

    如果也是動態(tài)class節(jié)點,在前一步的執(zhí)行結(jié)果基礎(chǔ)上再次執(zhí)行“按位或”運算符:patchFlag |= PatchFlags.CLASS。執(zhí)行后patchFlag的二進制值為11

    如果也是動態(tài)style節(jié)點,同樣在前一步的執(zhí)行結(jié)果基礎(chǔ)上再次執(zhí)行“按位或”運算符:patchFlag |= PatchFlags.STYLE。執(zhí)行后patchFlag的二進制值為111

    我們前面給p標簽標記為動態(tài)節(jié)點時給patchFlag賦值為1。在patchElement函數(shù)中使用patchFlag屬性進行"按位與"運算,判斷當前節(jié)點是否是動態(tài)文本節(jié)點、動態(tài)class節(jié)點、動態(tài)style節(jié)點。

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

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

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

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

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

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

    至此也就實現(xiàn)了當響應(yīng)式變量msg修改后,靶向更新p標簽中的節(jié)點。

    總結(jié)

    現(xiàn)在來看我們最開始講的整個靶向更新的流程圖你應(yīng)該很容易理解了,如下圖:

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

    • 編譯時階段找出動態(tài)節(jié)點,使用patchFlag屬性將其標記為動態(tài)節(jié)點。

    • 運行時階段分為兩塊:執(zhí)行render函數(shù)階段和更新視圖階段。

      • 執(zhí)行render函數(shù)階段會找出所有被標記的動態(tài)節(jié)點,將其塞到block節(jié)點的dynamicChildren屬性數(shù)組中。

      • 更新視圖階段會從block節(jié)點的dynamicChildren屬性數(shù)組中拿到所有的動態(tài)節(jié)點,然后遍歷這個數(shù)組將里面的動態(tài)節(jié)點進行靶向更新。

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

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


    瀏覽 105
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    97爽无码人妻AⅤ精品牛牛 | 黄色A片入口网站 | 免费a∨在线观看网站 | 久久aa| 在线观看视频黄免费 |