<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>

    關(guān)于Virtual DOM理解和Snabbdom源碼淺析

    共 75631字,需瀏覽 152分鐘

     ·

    2021-05-27 14:49

    什么是Virtual DOM

    • Virtual DOM(虛擬DOM),在形態(tài)上表現(xiàn)為一個(gè)能夠描述DOM結(jié)構(gòu)及其屬性信息的普通的JS對(duì)象,因?yàn)椴皇钦鎸?shí)的DOM對(duì)象,所以叫虛擬DOM。
    <div></div>
    {
      sel: 'div',
      data: {},
      chidren:undefined,
      elm:undefined,
      key:undefined,
    }

    Virtual DOM 本質(zhì)上JS和DOM之間的一個(gè)映射緩存??梢灶?lèi)比 CPU 和硬盤(pán),既然硬盤(pán)這么慢,我們就在它們之間加個(gè)緩存:既然 DOM 這么慢,我們就在它們 JS 和 DOM 之間加個(gè)緩存。CPU(JS)只操作內(nèi)存(Virtual DOM),最后的時(shí)候再把變更寫(xiě)入硬盤(pán)(DOM)。

    為什么需要Virtual DOM

    • 手動(dòng)操作DOM比較麻煩,還需要考慮瀏覽器兼容性問(wèn)題,雖然有jquery等庫(kù)簡(jiǎn)化DOM操作,但是隨著項(xiàng)目的復(fù)雜DOM操作復(fù)雜提升。
    • 為了簡(jiǎn)化DOM的復(fù)雜操作于是出現(xiàn)了MVVM框架,MVVM框架解決了視圖和狀態(tài)的同步問(wèn)題。
    • 為了簡(jiǎn)化視圖的操作我們可以使用模板引擎,但是模板引擎沒(méi)有解決跟蹤狀態(tài)變化的問(wèn)題,于是Virtual DOM出現(xiàn)了。
    • Virtual DOM的好處是當(dāng)狀態(tài)改變時(shí)不需要立即更新DOM,只需要?jiǎng)?chuàng)建一個(gè)虛擬樹(shù)來(lái)描述DOM,Virtual DOM內(nèi)部將弄清除如何有效(diff)的更新DOM。
    • 虛擬DOM可以維護(hù)程序的狀態(tài),跟蹤上一次的狀態(tài),通過(guò)比較前后兩次狀態(tài)的差異更新真實(shí)DOM。

    Virtual DOM的作用

    1、減少對(duì)真實(shí)DOM的操作

    真實(shí)DOM 因?yàn)闉g覽器廠商需要實(shí)現(xiàn)眾多的規(guī)范(各種 HTML5 屬性、DOM事件),即使創(chuàng)建一個(gè)空的 div 也要付出昂貴的代價(jià)。如以下代碼,打印空的div屬性一共298個(gè)。而這僅僅是第一層。真正的 DOM 元素非常龐大。直接操作DOM可能會(huì)導(dǎo)致頻繁的回流和重繪。

    const div = document.createElement('div');
    const arr = [];
    for(key in div){arr.push(key)};
    console.log(arr.length); // 298

    對(duì)復(fù)雜的文檔DOM結(jié)構(gòu)(復(fù)雜視圖情況下提升渲染性能),提供一種方便的工具,進(jìn)行最小化地DOM操作。既然我們可以用JS對(duì)象表示DOM結(jié)構(gòu),那么當(dāng)數(shù)據(jù)狀態(tài)發(fā)生變化而需要改變DOM結(jié)構(gòu)時(shí),我們先通過(guò)JS對(duì)象表示的虛擬DOM計(jì)算出實(shí)際DOM需要做的最小變動(dòng)(Virtual DOM會(huì)使用diff算法計(jì)算出如果有效的更新dom,只更新?tīng)顟B(tài)改變的DOM),然后再操作實(shí)際DOM,從而避免了粗放式的DOM操作帶來(lái)的性能問(wèn)題,減少對(duì)真實(shí)DOM的操作。

    2、無(wú)需手動(dòng)操作 DOM,維護(hù)視圖和狀態(tài)的關(guān)系

    我們不再需要手動(dòng)去操作 DOM,只需要寫(xiě)好 View-Model 的代碼邏輯,MVVM框架會(huì)根據(jù)虛擬 DOM 和 數(shù)據(jù)雙向綁定,幫我們以可預(yù)期的方式更新視圖,極大提高我們的開(kāi)發(fā)效率。

    3、跨平臺(tái)

    虛擬DOM是對(duì)真實(shí)的渲染內(nèi)容的一層抽象,是真實(shí)DOM的描述,因此,它可以實(shí)現(xiàn)“一次編碼,多端運(yùn)行”,可以實(shí)現(xiàn)SSR(Nuxt.js/Next.js)、原生應(yīng)用(Weex/React Native)、小程序(mpvue/uni-app)等。

    Virtual DOM有什么不足

    上面我們也說(shuō)到了在復(fù)雜視圖情況下提升渲染性能。雖然虛擬 DOM + 合理的優(yōu)化,足以應(yīng)對(duì)絕大部分應(yīng)用的性能需求,但在一些性能要求極高的應(yīng)用中虛擬DOM 無(wú)法進(jìn)行針對(duì)性的極致優(yōu)化。首次渲染大量DOM時(shí),由于多了一層虛擬DOM的計(jì)算,會(huì)比innerHTML插入慢。

    下方是尤大自己的見(jiàn)解。https://www.zhihu.com/question/31809713/answer/53544875

    Virtual DOM庫(kù)

    virtual-dom

    一個(gè)JavaScript DOM模型,支持元素創(chuàng)建,差異計(jì)算和補(bǔ)丁操作,以實(shí)現(xiàn)高效的重新渲染。

    • 源代碼庫(kù)地址:https://github.com/Matt-Esch/virtual-dom.git
    • 已經(jīng)有五六年沒(méi)有維護(hù)了

    snabbdom

    一個(gè)虛擬DOM庫(kù),重點(diǎn)放在簡(jiǎn)單性,模塊化,強(qiáng)大的功能和性能上。

    • 源代碼庫(kù)地址:https://github.com/snabbdom/snabbdom.git
    • 最近一直在維護(hù)

    為什么要介紹Virtual DOM庫(kù)Snabbdom

    • Vue2.x內(nèi)部使用的Virtual DOM就是改造的Snabbdom;
    • 核心代碼大約200行;
    • 通過(guò)模塊可擴(kuò)展;
    • 源碼使用TypeScript開(kāi)發(fā);
    • 最快的Virtual DOM之一;
    • 最近在維護(hù)

    Snabbdom核心

    • 使用 h()函數(shù)創(chuàng)建 JavaScript 對(duì)象(Vnode)描述真實(shí) DOM
    • init()設(shè)置模塊,創(chuàng)建 patch()
    • patch()比較新舊兩個(gè) Vnode
    • 把變化的內(nèi)容更新到真實(shí) DOM 樹(shù)上

    Snabbdom搭建項(xiàng)目

    第一步,初始化項(xiàng)目

    npm init -y

    or

    yarn init -y

    第二步,安裝依賴(lài)

    安裝snabbdom

    npm install snabbdom

    or

    yarn add snabbdom

    安裝parcel-bundler

    npm install parcel-bundler

    or

    yarn add parcel-bundler

    第三步,創(chuàng)建文件夾/文件,編輯文件

    在根目錄下創(chuàng)建一個(gè)名為src的文件目錄,然后在里面創(chuàng)建一個(gè)main.js文件。最后,在根目錄下創(chuàng)建一個(gè)index.html文件。

    package.json文件可以編輯如下,更利于操作。

    "scripts": {
        "serve""parcel index.html --open",
        "build""parcel build index.html"
     },

    第四步,編輯文件內(nèi)容

    index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>snabbdomApp</title>
      </head>
      <body>
        <div id="app"></div>
        <script src="src/main.js"></script>
      </body>
    </html>

    main.js

    主要介紹snabbdom中兩個(gè)核心init()、h()。

    • init() 是一個(gè)高階函數(shù),返回patch()(對(duì)比兩個(gè)VNode的差異更新到真實(shí)DOM);
    • h() 返回虛擬節(jié)點(diǎn)VNode;

    示例1

    import { h, init } from 'snabbdom';

    // init函數(shù)參數(shù):數(shù)組(模塊)
    // 返回值:patch函數(shù):作用是對(duì)比兩個(gè)Vnode的差異更新到真實(shí)dom
    const patch = init([]);
    // h函數(shù)
    // 第一個(gè)參數(shù):標(biāo)簽+選擇器;
    // 第二個(gè)參數(shù):如果是字符串則是標(biāo)簽的內(nèi)容
    let vnode = h('div#container''Hello World');

    const app = document.querySelector('#app');
    // patch函數(shù)
    // 第一個(gè)參數(shù):可以是DOM元素(內(nèi)部會(huì)把DOM元素轉(zhuǎn)化為Vnode),也可以是Vnode;
    // 第二個(gè)參數(shù):Vnode
    // 返回值:Vnode
    let oldVnode = patch(app, vnode);

    // 新舊Vnode對(duì)比
    vnode = h('div''Hello Snabbdom');
    patch(oldVnode, vnode);

    示例2

    import { h, init } from 'snabbdom';

    const patch = init([]);
    // 可放置子元素
    let vnode = h('div#container', [h('h1''1'), h('h2''2')]);

    const app = document.querySelector('#app');

    const oldVnode = patch(app, vnode);

    vnode = h('div''Hello Snabbdom');
    patch(oldVnode, vnode);

    setInterval(() => {
      // 清除頁(yè)面元素
      patch(oldVnode, h('!'));
    }, 3000);

    示例3

    常用模塊

    • The attributes module

    設(shè)置DOM元素的特性,使用setAttribute添加和更新特性。

    • The props module

    允許設(shè)置DOM元素的屬性。

    • The class module

    提供了一種動(dòng)態(tài)切換元素上的類(lèi)的簡(jiǎn)單方法。

    • The style module

    允許在元素上設(shè)置CSS屬性。請(qǐng)注意,如果樣式屬性作為樣式對(duì)象的屬性被移除,樣式模塊并不會(huì)移除它們。為了移除一個(gè)樣式,應(yīng)該將其設(shè)置為空字符串。

    • The dataset module

    允許在DOM元素上設(shè)置自定義數(shù)據(jù)屬性(data-*)。

    • The eventlisteners module

    提供了附加事件監(jiān)聽(tīng)器的強(qiáng)大功能。

    import {
      h,
      init,
      classModule,
      propsModule,
      styleModule,
      eventListenersModule,
    from 'snabbdom';

    const patch = init([
      styleModule,
      classModule,
      propsModule,
      eventListenersModule,
    ]);

    let vnode = h(
      'div#container',
      {
        style: {
          color'#000',
        },
        on: {
          click: eventHandler,
        },
      },
      [
        h('p''p1', h('a', { class: { activetrueselectedtrue } }, 'Toggle')),
        h('p''p2'),
        h('a', { props: { href'/' } }, 'Go to'),
      ]
    );

    function eventHandler({
      console.log('1');
    }
    const app = document.querySelector('#app');

    patch(app, vnode);

    snabbdom源碼淺析

    源碼地址:https://github.com/snabbdom/snabbdom.git以下分析snabbdom版本3.0.1。

    源碼核心文件目錄及其文件

    核心文件夾是**src目錄。**里面包含了如下文件夾及其目錄:

    • helpers:里面只有一個(gè)文件attachto.ts,這個(gè)文件主要作用是定義了幾個(gè)類(lèi)型在vnode.ts文件中使用。
    • modules:里面存放著snabbdom模塊,分別是attributes.ts、class.ts、dataset.ts、eventlisteners.ts、props.ts、style.ts這6個(gè)模塊。另外一個(gè)module.ts這個(gè)文件為它們提供了鉤子函數(shù)。
    • h.ts:創(chuàng)建Vnode。
    • hook.ts:提供鉤子函數(shù)。
    • htmldomapi:提供了DOM API。
    • index.ts:snabbdom 入口文件。
    • init.ts:導(dǎo)出了patch函數(shù)。
    • is.ts:導(dǎo)出了兩個(gè)方法。一個(gè)方法是判斷是否是數(shù)組,另一個(gè)判斷是否是字符串或數(shù)字。
    • jsx.ts:與jsx相關(guān)。
    • thunk.ts:與優(yōu)化key相關(guān)。
    • tovnode.ts:真實(shí)DOM 轉(zhuǎn)化為 虛擬DOM。
    • vnode.ts:定義了Vnode的結(jié)構(gòu)。

    核心文件淺析

    h.ts

    h 函數(shù)最早見(jiàn)于 hyperscript,使用 JavaScript 創(chuàng)建超文本,Snabbdom 中的 h 函數(shù)不是用來(lái)創(chuàng)建超文本,而是創(chuàng)建 Vnode。 在使用 Vue2.x 的時(shí)候見(jiàn)過(guò) h 函數(shù),它的參數(shù)就是h函數(shù),但是Vue加強(qiáng)了h函數(shù),使其支持組件機(jī)制。

    new Vue({
      router,
      store,
      render:h => h(App)
    }).$mount('#app)

    以上是h.ts文件中的內(nèi)容,可以看到它導(dǎo)出了多個(gè)h方法,這種方式叫做函數(shù)重載。在JS中暫時(shí)沒(méi)有,目前TS支持這種機(jī)制(但也只是通過(guò)調(diào)整代碼參數(shù)層面上,因?yàn)樽罱KTS還是要轉(zhuǎn)換為JS)。方法名相同,參數(shù)個(gè)數(shù)或類(lèi)型不同的方法叫做函數(shù)重載。所以通過(guò)參數(shù)個(gè)數(shù)或類(lèi)型不同來(lái)區(qū)分它們。

    // 這里通過(guò)參數(shù)不同來(lái)區(qū)分不同的函數(shù)

    function add(a, b{
      console.log(a + b);
    }

    function add(a, b, c{
      console.log(a + b + c);
    }

    add(12);
    add(123);

    從上面代碼層面上我們知道了通過(guò)函數(shù)重載這種方法可以在通過(guò)參數(shù)個(gè)數(shù)或類(lèi)型不同輕松地實(shí)現(xiàn)了相應(yīng)情況調(diào)用相應(yīng)參數(shù)的方法。

    那么,我們來(lái)具體看下源碼是怎么實(shí)現(xiàn)函數(shù)重載的。

    通過(guò)源碼我們看到,通過(guò)傳入不同的類(lèi)型的參數(shù)調(diào)用對(duì)應(yīng)的代碼,最后將將參數(shù)傳入到vnode方法中,創(chuàng)建一個(gè)Vnode,并返回這個(gè)方法。

    那么接下來(lái),我們看下vnode方法的實(shí)現(xiàn)。

    vnode.ts

    我們打開(kāi)vnode.ts這個(gè)文件,這個(gè)文件主要是導(dǎo)出了一個(gè)vnode方法,并且定義了幾個(gè)接口。我們看到以下代碼中vnode中的參數(shù)含義就知道在h.ts文件中函數(shù)參數(shù)的意思,是相對(duì)應(yīng)的。

    截屏2021-05-22 09.50.53.png

    init.ts

    在介紹init.ts文件之前的,我們需要知道這樣的一個(gè)概念:

    • init()是一個(gè)高階函數(shù),返回patch()
    • patch(oldVnode,newVnode)
    • 把新節(jié)點(diǎn)中的變化的內(nèi)容渲染到真實(shí)DOM,最后返回新節(jié)點(diǎn)作為下一次處理的舊節(jié)點(diǎn)

    這個(gè)概念我們?cè)谏厦嬉呀?jīng)闡述了。**init()**就是通過(guò)這個(gè)文件導(dǎo)出的。

    在看init.ts源碼之前,我們還需要了解Vnode是渲染到真實(shí)DOM的整體流程。這樣,看源碼才不會(huì)有誤解。

    整體流程:

    1. 對(duì)比新舊Vnode是否相同節(jié)點(diǎn)(節(jié)點(diǎn)數(shù)據(jù)中的key、sel、is相同)
    2. 如果不是相同節(jié)點(diǎn),刪除之前的內(nèi)容,重新渲染
    3. 如果是相同節(jié)點(diǎn),再判斷新的Vnode是否有text,如果有并且和oldVnode 的text 不同,直接更新文本內(nèi)容
    4. 如果新的Vnode 有children,判斷子節(jié)點(diǎn)是否有變化,判斷子節(jié)點(diǎn)的過(guò)程就是diff 算法
    5. diff 算法過(guò)程只進(jìn)行同層級(jí)節(jié)點(diǎn)比較

    Diff算法的作用是用來(lái)計(jì)算出 Virtual DOM 中被改變的部分,然后針對(duì)該部分進(jìn)行原生DOM操作,而不用重新渲染整個(gè)頁(yè)面。

    1. 同級(jí)對(duì)比

    對(duì)比的時(shí)候,只針對(duì)同級(jí)的對(duì)比,減少算法復(fù)雜度。

    1. 就近復(fù)用

    為了盡可能不發(fā)生 DOM 的移動(dòng),會(huì)就近復(fù)用相同的 DOM 節(jié)點(diǎn),復(fù)用的依據(jù)是判斷是否是同類(lèi)型的 dom 元素。 看到這里你可能就會(huì)想到Vue中列表渲染為什么推薦加上key,我們需要使用key來(lái)給每個(gè)節(jié)點(diǎn)做一個(gè)唯一標(biāo)識(shí),Diff算法就可以正確的識(shí)別此節(jié)點(diǎn),找到正確的位置區(qū)插入新的節(jié)點(diǎn)。key的作用主要是為了高效的更新虛擬DOM。

    我們先看下init.ts中的大體源碼。

    我們先簡(jiǎn)單地來(lái)看下sameVnode方法。判斷是否是相同的虛擬節(jié)點(diǎn)。

    // 是否是相同節(jié)點(diǎn)
    function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
      const isSameKey = vnode1.key === vnode2.key;
      const isSameIs = vnode1.data?.is === vnode2.data?.is;
      const isSameSel = vnode1.sel === vnode2.sel;

      return isSameSel && isSameKey && isSameIs;
    }

    是否是Vnode。

    // 是否是vnode
    function isVnode(vnode: any): vnode is VNode {
      return vnode.sel !== undefined;
    }

    注冊(cè)一系列的鉤子,在不同的階段觸發(fā)。

    // 定義一些鉤子函數(shù)
    const hooks: Array<keyof Module> = [
      "create",
      "update",
      "remove",
      "destroy",
      "pre",
      "post",
    ];

    下面呢,主要看下導(dǎo)出的init方法。也是init.ts中最主要的部分,從68行到472行。

    // 導(dǎo)出init函數(shù)
    export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI{
      let i: number;
      let j: number;
      const cbs: ModuleHooks = {
        create: [],
        update: [],
        remove: [],
        destroy: [],
        pre: [],
        post: [],
      };
      // 初始化轉(zhuǎn)化成虛擬節(jié)點(diǎn)的api
      const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
      // 把傳入的所有模塊的鉤子函數(shù),統(tǒng)一存儲(chǔ)到cbs對(duì)象中
      // 最終構(gòu)建的cbs對(duì)象的形式cbs = {create:[fn1,fn2],update:[],....}
      for (i = 0; i < hooks.length; ++i) {
        // cbs.create= [], cbs.update = []...
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          // modules 傳入的模塊數(shù)組
          // 獲取模塊中的hook函數(shù)
          // hook = modules[0][create]...
          const hook = modules[j][hooks[i]];
          if (hook !== undefined) {
            // 把獲取到的hook函數(shù)放入到cbs 對(duì)應(yīng)的鉤子函數(shù)數(shù)組中
            (cbs[hooks[i]] as any[]).push(hook);
          }
        }
      }

      function emptyNodeAt(elm: Element{
        const id = elm.id ? "#" + elm.id : "";
        const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
        return vnode(
          api.tagName(elm).toLowerCase() + id + c,
          {},
          [],
          undefined,
          elm
        );
      }

      function createRmCb(childElm: Node, listeners: number{
        return function rmCb({
          if (--listeners === 0) {
            const parent = api.parentNode(childElm) as Node;
            api.removeChild(parent, childElm);
          }
        };
      }

      /*
      1.觸發(fā)鉤子函數(shù)init
      2.把vnode轉(zhuǎn)換為DOM對(duì)象,存儲(chǔ)到vnode.elm中
      - sel是!--》創(chuàng)建注釋節(jié)點(diǎn)
      - sel不為空 --》創(chuàng)建對(duì)應(yīng)的DOM對(duì)象;觸發(fā)模塊的鉤子函數(shù)create;創(chuàng)建所有子節(jié)點(diǎn)對(duì)應(yīng)的DOM對(duì)象;觸發(fā)鉤子函數(shù)create;如果是vnode有inset鉤子函數(shù),追加到隊(duì)列
      - sel為空 --》創(chuàng)建文本節(jié)點(diǎn)
      3.返回vnode.elm
      */

      function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any;
        let data = vnode.data;
        if (data !== undefined) {
          // 執(zhí)行init鉤子函數(shù)
          const init = data.hook?.init;
          if (isDef(init)) {
            init(vnode);
            data = vnode.data;
          }
        }
        // 把vnode轉(zhuǎn)換成真實(shí)dom對(duì)象(沒(méi)有渲染到頁(yè)面)
        const children = vnode.children;
        const sel = vnode.sel;
        if (sel === "!") {
          // 如果選擇器是!,創(chuàng)建注釋節(jié)點(diǎn)
          if (isUndef(vnode.text)) {
            vnode.text = "";
          }
          vnode.elm = api.createComment(vnode.text!);
        } else if (sel !== undefined) {
          // 如果選擇器不為空
          // 解析選擇器
          // Parse selector
          const hashIdx = sel.indexOf("#");
          const dotIdx = sel.indexOf(".", hashIdx);
          const hash = hashIdx > 0 ? hashIdx : sel.length;
          const dot = dotIdx > 0 ? dotIdx : sel.length;
          const tag =
            hashIdx !== -1 || dotIdx !== -1
              ? sel.slice(0Math.min(hash, dot))
              : sel;
          const elm = (vnode.elm =
            isDef(data) && isDef((i = data.ns))
              ? api.createElementNS(i, tag, data)
              : api.createElement(tag, data));
          if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
          if (dotIdx > 0)
            elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g" "));
          // 執(zhí)行模塊的create鉤子函數(shù)
          for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
          // 如果vnode中有子節(jié)點(diǎn),創(chuàng)建子Vnode對(duì)應(yīng)的DOM元素并追加到DOM樹(shù)上
          if (is.array(children)) {
            for (i = 0; i < children.length; ++i) {
              const ch = children[i];
              if (ch != null) {
                api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
              }
            }
          } else if (is.primitive(vnode.text)) {
            // 如果vode的text值是string/number,創(chuàng)建文本節(jié)點(diǎn)并追加到DOM樹(shù)
            api.appendChild(elm, api.createTextNode(vnode.text));
          }
          const hook = vnode.data!.hook;
          if (isDef(hook)) {
            // 執(zhí)行傳入的鉤子 create
            hook.create?.(emptyNode, vnode);
            if (hook.insert) {
              insertedVnodeQueue.push(vnode);
            }
          }
        } else {
          // 如果選擇器為空,創(chuàng)建文本節(jié)點(diǎn)
          vnode.elm = api.createTextNode(vnode.text!);
        }
        // 返回新創(chuàng)建的DOM
        return vnode.elm;
      }

      function addVnodes(
        parentElm: Node,
        before: Node | null,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number,
        insertedVnodeQueue: VNodeQueue
      
    {
        for (; startIdx <= endIdx; ++startIdx) {
          const ch = vnodes[startIdx];
          if (ch != null) {
            api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
          }
        }
      }

      function invokeDestroyHook(vnode: VNode{
        const data = vnode.data;
        if (data !== undefined) {
          // 執(zhí)行的destroy 鉤子函數(shù)
          data?.hook?.destroy?.(vnode);
          // 調(diào)用模塊的destroy鉤子函數(shù)
          for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
          // 執(zhí)行子節(jié)點(diǎn)的destroy鉤子函數(shù)
          if (vnode.children !== undefined) {
            for (let j = 0; j < vnode.children.length; ++j) {
              const child = vnode.children[j];
              if (child != null && typeof child !== "string") {
                invokeDestroyHook(child);
              }
            }
          }
        }
      }

      function removeVnodes(
        parentElm: Node,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number
      
    ): void 
    {
        for (; startIdx <= endIdx; ++startIdx) {
          let listeners: number;
          let rm: () => void;
          const ch = vnodes[startIdx];
          if (ch != null) {
            // 如果sel 有值
            if (isDef(ch.sel)) {
              invokeDestroyHook(ch);

              // 防止重復(fù)調(diào)用
              listeners = cbs.remove.length + 1;
              // 創(chuàng)建刪除的回調(diào)函數(shù)
              rm = createRmCb(ch.elm!, listeners);
              for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
              // 執(zhí)行remove鉤子函數(shù)
              const removeHook = ch?.data?.hook?.remove;
              if (isDef(removeHook)) {
                removeHook(ch, rm);
              } else {
                // 如果沒(méi)有鉤子函數(shù),直接調(diào)用刪除元素的方法
                rm();
              }
            } else {
              // Text node
              // 如果是文本節(jié)點(diǎn),直接是調(diào)用刪除元素的方法
              api.removeChild(parentElm, ch.elm!);
            }
          }
        }
      }

      function updateChildren(
        parentElm: Node,
        oldCh: VNode[],
        newCh: VNode[],
        insertedVnodeQueue: VNodeQueue
      
    {
        let oldStartIdx = 0;
        let newStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];
        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx: KeyToIndexMap | undefined;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;

        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(
              parentElm,
              oldStartVnode.elm!,
              api.nextSibling(oldEndVnode.elm!)
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
          } else {
            if (oldKeyToIdx === undefined) {
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
            if (isUndef(idxInOld)) {
              // New element
              api.insertBefore(
                parentElm,
                createElm(newStartVnode, insertedVnodeQueue),
                oldStartVnode.elm!
              );
            } else {
              elmToMove = oldCh[idxInOld];
              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(
                  parentElm,
                  createElm(newStartVnode, insertedVnodeQueue),
                  oldStartVnode.elm!
                );
              } else {
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
              }
            }
            newStartVnode = newCh[++newStartIdx];
          }
        }
        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
          if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            addVnodes(
              parentElm,
              before,
              newCh,
              newStartIdx,
              newEndIdx,
              insertedVnodeQueue
            );
          } else {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
          }
        }
      }
      /*
    對(duì)比兩個(gè)新舊節(jié)點(diǎn),然后找到差異并更新DOM
    第一部分
    1.觸發(fā)prepatch鉤子函數(shù)
    2.觸發(fā)update鉤子函數(shù)
    第二部分
    1.新節(jié)點(diǎn)有text屬性,且不等于舊節(jié)點(diǎn)的text屬性 -》如果舊節(jié)點(diǎn)有children,移除舊節(jié)點(diǎn)children對(duì)應(yīng)的DOM元素;設(shè)置新節(jié)點(diǎn)對(duì)應(yīng)的DOM元素的textContent
    2.新舊節(jié)點(diǎn)都有children,且不相等-》調(diào)用updateChildren();對(duì)比子節(jié)點(diǎn),并且更新子節(jié)點(diǎn)的差異
    3.只有新節(jié)點(diǎn)有children屬性-》如果舊節(jié)點(diǎn)有text屬性,清空對(duì)應(yīng)DOM元素的textContent;添加所有的子節(jié)點(diǎn)
    4.只有舊節(jié)點(diǎn)有children屬性-》移除所有舊節(jié)點(diǎn)
    5.只有舊節(jié)點(diǎn)有text屬性=》清空對(duì)應(yīng)的DOM元素的textContent
    第三部分
    1.觸發(fā)postpatch鉤子函數(shù)
    */

      function patchVnode(
        oldVnode: VNode,
        vnode: VNode,
        insertedVnodeQueue: VNodeQueue
      
    {
        const hook = vnode.data?.hook;
        // 首先執(zhí)行prepatch鉤子函數(shù)
        hook?.prepatch?.(oldVnode, vnode);
        const elm = (vnode.elm = oldVnode.elm)!;
        const oldCh = oldVnode.children as VNode[];
        const ch = vnode.children as VNode[];
        // 如果新舊vnode相同返回
        if (oldVnode === vnode) return;
        if (vnode.data !== undefined) {
          // 執(zhí)行模塊的update鉤子函數(shù)
          for (let i = 0; i < cbs.update.length; ++i)
            cbs.update[i](oldVnode, vnode);
          // 執(zhí)行update鉤子函數(shù)
          vnode.data.hook?.update?.(oldVnode, vnode);
        }

        // 如果是vnode.text 未定義
        if (isUndef(vnode.text)) {
          // 如果是新舊節(jié)點(diǎn)都有 children
          if (isDef(oldCh) && isDef(ch)) {
            // 使用diff算法對(duì)比子節(jié)點(diǎn),更新子節(jié)點(diǎn)
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
          } else if (isDef(ch)) {
            // 如果新節(jié)點(diǎn)有children,舊節(jié)點(diǎn)沒(méi)有children
            // 如果舊節(jié)點(diǎn)有text,清空dom 元素的內(nèi)容
            if (isDef(oldVnode.text)) api.setTextContent(elm, "");
            // 批量添加子節(jié)點(diǎn)
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
          } else if (isDef(oldCh)) {
            // 如果舊節(jié)點(diǎn)有children,新節(jié)點(diǎn)沒(méi)有children
            // 批量移除子節(jié)點(diǎn)
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          } else if (isDef(oldVnode.text)) {
            // 如果舊節(jié)點(diǎn)有 text,清空 DOM 元素
            api.setTextContent(elm, "");
          }
        } else if (oldVnode.text !== vnode.text) {
          // 如果沒(méi)有設(shè)置 vnode.text
          if (isDef(oldCh)) {
            // 如果舊節(jié)點(diǎn)有children,移除
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          }
          // 設(shè)置 DOM 元素的textContent為 vnode.text
          api.setTextContent(elm, vnode.text!);
        }
        // 最后執(zhí)行postpatch鉤子函數(shù)
        hook?.postpatch?.(oldVnode, vnode);
      }

      // init 內(nèi)部返回 patch 函數(shù),把vnode渲染成真實(shí)dom,并返回vnode
      return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
        let i: number, elm: Node, parent: Node;
        // 保存新插入的節(jié)點(diǎn)的隊(duì)列,為了觸發(fā)鉤子函數(shù)
        const insertedVnodeQueue: VNodeQueue = [];
        // 執(zhí)行模塊的pre 鉤子函數(shù)
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
        // 如果oldVnode不是Vnode,創(chuàng)建Vnode并設(shè)置elm
        if (!isVnode(oldVnode)) {
          // 把Dom元素轉(zhuǎn)化成空的Vnode
          oldVnode = emptyNodeAt(oldVnode);
        }
        // 如果新舊節(jié)點(diǎn)是相同節(jié)點(diǎn)
        if (sameVnode(oldVnode, vnode)) {
          // 找節(jié)點(diǎn)的差異并更新DOM,這里的原理就是diff算法
          patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
          // 如果新舊節(jié)點(diǎn)不同,vnode創(chuàng)建對(duì)應(yīng)的DOM

          // 獲取當(dāng)前的DOM元素
          elm = oldVnode.elm!;
          // 獲取父元素
          parent = api.parentNode(elm) as Node;
          // 創(chuàng)建Vnode對(duì)應(yīng)的DOM元素,并觸發(fā)init/create 鉤子函數(shù)
          createElm(vnode, insertedVnodeQueue);

          if (parent !== null) {
            // 如果父元素不為空,把vnode對(duì)應(yīng)的DOM插入到父元素中
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
            // 移除舊節(jié)點(diǎn)
            removeVnodes(parent, [oldVnode], 00);
          }
        }
        // 執(zhí)行insert 鉤子函數(shù)
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
        }
        // 執(zhí)行模塊的post 鉤子函數(shù)
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
        // 返回vnode 作為下次更新的舊節(jié)點(diǎn)
        return vnode;
      };
    }

    接下來(lái),我們分開(kāi)介紹init方法中的內(nèi)容。

    將各個(gè)模塊的鉤子方法,掛到統(tǒng)一的鉤子上
    • 初始化的時(shí)候,將每個(gè) modules 下的相應(yīng)的鉤子都追加都一個(gè)數(shù)組里面
    • 在進(jìn)行 patch 的各個(gè)階段,觸發(fā)對(duì)應(yīng)的鉤子去處理對(duì)應(yīng)的事情
    • 這種方式比較方便擴(kuò)展。新增鉤子的時(shí)候,不需要更改到主要的流程
    • 這些模塊的鉤子,主要用在更新節(jié)點(diǎn)的時(shí)候,會(huì)在不同的生命周期里面去觸發(fā)對(duì)應(yīng)的鉤子,從而更新這些模塊。
      let i: number;
      let j: number;
      const cbs: ModuleHooks = {
        create: [],
        update: [],
        remove: [],
        destroy: [],
        pre: [],
        post: [],
      };
      // 把傳入的所有模塊的鉤子函數(shù),統(tǒng)一存儲(chǔ)到cbs對(duì)象中
      // 最終構(gòu)建的cbs對(duì)象的形式cbs = {create:[fn1,fn2],update:[],....}
      for (i = 0; i < hooks.length; ++i) {
        // cbs.create= [], cbs.update = []...
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          // modules 傳入的模塊數(shù)組
          // 獲取模塊中的hook函數(shù)
          // hook = modules[0][create]...
          const hook = modules[j][hooks[i]];
          if (hook !== undefined) {
            // 把獲取到的hook函數(shù)放入到cbs 對(duì)應(yīng)的鉤子函數(shù)數(shù)組中
            (cbs[hooks[i]] as any[]).push(hook);
          }
        }
      }
    patch方法

    init 方法最后返回一個(gè) patch 方法 。

    主要的邏輯如下 :

    • 觸發(fā) pre 鉤子
    • 如果舊節(jié)點(diǎn)非 vnode, 則新創(chuàng)建空的 vnode
    • 新舊節(jié)點(diǎn)為 sameVnode 的話(huà),則調(diào)用 patchVnode 更新 vnode , 否則創(chuàng)建新節(jié)點(diǎn)
    • 觸發(fā)收集到的新元素 insert 鉤子
    • 觸發(fā) post 鉤子
      // init 內(nèi)部返回 patch 函數(shù),把vnode渲染成真實(shí)dom,并返回vnode
      return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
        let i: number, elm: Node, parent: Node;
        // 保存新插入的節(jié)點(diǎn)的隊(duì)列,為了觸發(fā)鉤子函數(shù)
        const insertedVnodeQueue: VNodeQueue = [];
        // 執(zhí)行模塊的pre 鉤子函數(shù)
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
        // 如果oldVnode不是Vnode,創(chuàng)建Vnode并設(shè)置elm
        if (!isVnode(oldVnode)) {
          // 把Dom元素轉(zhuǎn)化成空的Vnode
          oldVnode = emptyNodeAt(oldVnode);
        }
        // 如果新舊節(jié)點(diǎn)是相同節(jié)點(diǎn)
        if (sameVnode(oldVnode, vnode)) {
          // 找節(jié)點(diǎn)的差異并更新DOM,這里的原理就是diff算法
          patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
          // 如果新舊節(jié)點(diǎn)不同,vnode創(chuàng)建對(duì)應(yīng)的DOM

          // 獲取當(dāng)前的DOM元素
          elm = oldVnode.elm!;
          // 獲取父元素
          parent = api.parentNode(elm) as Node;
          // 創(chuàng)建Vnode對(duì)應(yīng)的DOM元素,并觸發(fā)init/create 鉤子函數(shù)
          createElm(vnode, insertedVnodeQueue);

          if (parent !== null) {
            // 如果父元素不為空,把vnode對(duì)應(yīng)的DOM插入到父元素中
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
            // 移除舊節(jié)點(diǎn)
            removeVnodes(parent, [oldVnode], 00);
          }
        }
        // 執(zhí)行insert 鉤子函數(shù)
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
        }
        // 執(zhí)行模塊的post 鉤子函數(shù)
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
        // 返回vnode 作為下次更新的舊節(jié)點(diǎn)
        return vnode;
      };
    patchVnode方法

    主要的邏輯如下 :

    • 觸發(fā) prepatch 鉤子
    • 觸發(fā) update 鉤子, 這里主要為了更新對(duì)應(yīng)的 module 內(nèi)容
    • 非文本節(jié)點(diǎn)的情況 , 調(diào)用 updateChildren 更新所有子節(jié)點(diǎn)
    • 文本節(jié)點(diǎn)的情況 , 直接 api.setTextContent(elm, vnode.text as string)

    這里在對(duì)比的時(shí)候,就會(huì)直接更新元素內(nèi)容了。并不會(huì)等到對(duì)比完才更新 DOM 元素。

      /*
    對(duì)比兩個(gè)新舊節(jié)點(diǎn),然后找到差異并更新DOM
    第一部分
    1.觸發(fā)prepatch鉤子函數(shù)
    2.觸發(fā)update鉤子函數(shù)
    第二部分
    1.新節(jié)點(diǎn)有text屬性,且不等于舊節(jié)點(diǎn)的text屬性 -》如果舊節(jié)點(diǎn)有children,移除舊節(jié)點(diǎn)children對(duì)應(yīng)的DOM元素;設(shè)置新節(jié)點(diǎn)對(duì)應(yīng)的DOM元素的textContent
    2.新舊節(jié)點(diǎn)都有children,且不相等-》調(diào)用updateChildren();對(duì)比子節(jié)點(diǎn),并且更新子節(jié)點(diǎn)的差異
    3.只有新節(jié)點(diǎn)有children屬性-》如果舊節(jié)點(diǎn)有text屬性,清空對(duì)應(yīng)DOM元素的textContent;添加所有的子節(jié)點(diǎn)
    4.只有舊節(jié)點(diǎn)有children屬性-》移除所有舊節(jié)點(diǎn)
    5.只有舊節(jié)點(diǎn)有text屬性=》清空對(duì)應(yīng)的DOM元素的textContent
    第三部分
    1.觸發(fā)postpatch鉤子函數(shù)
    */

      function patchVnode(
        oldVnode: VNode,
        vnode: VNode,
        insertedVnodeQueue: VNodeQueue
      
    {
        const hook = vnode.data?.hook;
        // 首先執(zhí)行prepatch鉤子函數(shù)
        hook?.prepatch?.(oldVnode, vnode);
        const elm = (vnode.elm = oldVnode.elm)!;
        const oldCh = oldVnode.children as VNode[];
        const ch = vnode.children as VNode[];
        // 如果新舊vnode相同返回
        if (oldVnode === vnode) return;
        if (vnode.data !== undefined) {
          // 執(zhí)行模塊的update鉤子函數(shù)
          for (let i = 0; i < cbs.update.length; ++i)
            cbs.update[i](oldVnode, vnode);
          // 執(zhí)行update鉤子函數(shù)
          vnode.data.hook?.update?.(oldVnode, vnode);
        }

        // 如果是vnode.text 未定義
        if (isUndef(vnode.text)) {
          // 如果是新舊節(jié)點(diǎn)都有 children
          if (isDef(oldCh) && isDef(ch)) {
            // 使用diff算法對(duì)比子節(jié)點(diǎn),更新子節(jié)點(diǎn)
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
          } else if (isDef(ch)) {
            // 如果新節(jié)點(diǎn)有children,舊節(jié)點(diǎn)沒(méi)有children
            // 如果舊節(jié)點(diǎn)有text,清空dom 元素的內(nèi)容
            if (isDef(oldVnode.text)) api.setTextContent(elm, "");
            // 批量添加子節(jié)點(diǎn)
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
          } else if (isDef(oldCh)) {
            // 如果舊節(jié)點(diǎn)有children,新節(jié)點(diǎn)沒(méi)有children
            // 批量移除子節(jié)點(diǎn)
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          } else if (isDef(oldVnode.text)) {
            // 如果舊節(jié)點(diǎn)有 text,清空 DOM 元素
            api.setTextContent(elm, "");
          }
        } else if (oldVnode.text !== vnode.text) {
          // 如果沒(méi)有設(shè)置 vnode.text
          if (isDef(oldCh)) {
            // 如果舊節(jié)點(diǎn)有children,移除
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          }
          // 設(shè)置 DOM 元素的textContent為 vnode.text
          api.setTextContent(elm, vnode.text!);
        }
        // 最后執(zhí)行postpatch鉤子函數(shù)
        hook?.postpatch?.(oldVnode, vnode);
      }
    updateChildren 方法

    patchVnode 里面最重要的方法,也是整個(gè) diff 里面的最核心方法。

    主要的邏輯如下:

    1. 優(yōu)先處理特殊場(chǎng)景,先對(duì)比兩端。也就是
    • 舊 vnode 頭 vs 新 vnode 頭
    • 舊 vnode 尾 vs 新 vnode 尾
    • 舊 vnode 頭 vs 新 vnode 尾
    • 舊 vnode 尾 vs 新 vnode 頭
    1. 首尾不一樣的情況,尋找 key 相同的節(jié)點(diǎn),找不到則新建元素
    2. 如果找到 key,但是,元素選擇器變化了,也新建元素
    3. 如果找到 key,并且元素選擇沒(méi)變, 則移動(dòng)元素
    4. 兩個(gè)列表對(duì)比完之后,清理多余的元素,新增添加的元素

    不提供 key 的情況下,如果只是順序改變的情況,例如第一個(gè)移動(dòng)到末尾。這個(gè)時(shí)候,會(huì)導(dǎo)致其實(shí)更新了后面的所有元素。

      // 更新子節(jié)點(diǎn)
        function updateChildren(
            parentElm: Node,
            oldCh: Array<VNode>,
            newCh: Array<VNode>,
            insertedVnodeQueue: VNodeQueue
        
    {
            let oldStartIdx = 0,
                newStartIdx = 0;

            let oldEndIdx = oldCh.length - 1;

            let oldStartVnode = oldCh[0];
            let oldEndVnode = oldCh[oldEndIdx];

            let newEndIdx = newCh.length - 1;

            let newStartVnode = newCh[0];
            let newEndVnode = newCh[newEndIdx];

            let oldKeyToIdx: any;
            let idxInOld: number;
            let elmToMove: VNode;
            let before: any;

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
                if (oldStartVnode == null) {
                    // 移動(dòng)索引,因?yàn)楣?jié)點(diǎn)處理過(guò)了會(huì)置空,所以這里向右移
                    oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
                } else if (oldEndVnode == null) {
                    // 原理同上
                    oldEndVnode = oldCh[--oldEndIdx];
                } else if (newStartVnode == null) {
                    // 原理同上
                    newStartVnode = newCh[++newStartIdx];
                } else if (newEndVnode == null) {
                    // 原理同上
                    newEndVnode = newCh[--newEndIdx];
                } else if (sameVnode(oldStartVnode, newStartVnode)) {
                    // 從左對(duì)比
                    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
                    oldStartVnode = oldCh[++oldStartIdx];
                    newStartVnode = newCh[++newStartIdx];
                } else if (sameVnode(oldEndVnode, newEndVnode)) {
                    // 從右對(duì)比
                    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
                    oldEndVnode = oldCh[--oldEndIdx];
                    newEndVnode = newCh[--newEndIdx];
                } else if (sameVnode(oldStartVnode, newEndVnode)) {
                    // Vnode moved right
                    // 最左側(cè) 對(duì)比 最右側(cè)
                    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
                    // 移動(dòng)元素到右側(cè)指針的后面
                    api.insertBefore(
                        parentElm,
                        oldStartVnode.elm as Node,
                        api.nextSibling(oldEndVnode.elm as Node)
                    );
                    oldStartVnode = oldCh[++oldStartIdx];
                    newEndVnode = newCh[--newEndIdx];
                } else if (sameVnode(oldEndVnode, newStartVnode)) {
                    // Vnode moved left
                    // 最右側(cè)對(duì)比最左側(cè)
                    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
                    // 移動(dòng)元素到左側(cè)指針的后面
                    api.insertBefore(
                        parentElm,
                        oldEndVnode.elm as Node,
                        oldStartVnode.elm as Node
                    );
                    oldEndVnode = oldCh[--oldEndIdx];
                    newStartVnode = newCh[++newStartIdx];
                } else {
                    // 首尾都不一樣的情況,尋找相同 key 的節(jié)點(diǎn),所以使用的時(shí)候加上key可以調(diào)高效率
                    if (oldKeyToIdx === undefined) {
                        oldKeyToIdx = createKeyToOldIdx(
                            oldCh,
                            oldStartIdx,
                            oldEndIdx
                        );
                    }
                    idxInOld = oldKeyToIdx[newStartVnode.key as string];

                    if (isUndef(idxInOld)) {
                        // New element
                        // 如果找不到 key 對(duì)應(yīng)的元素,就新建元素
                        api.insertBefore(
                            parentElm,
                            createElm(newStartVnode, insertedVnodeQueue),
                            oldStartVnode.elm as Node
                        );
                        newStartVnode = newCh[++newStartIdx];
                    } else {
                        // 如果找到 key 對(duì)應(yīng)的元素,就移動(dòng)元素
                        elmToMove = oldCh[idxInOld];
                        if (elmToMove.sel !== newStartVnode.sel) {
                            api.insertBefore(
                                parentElm,
                                createElm(newStartVnode, insertedVnodeQueue),
                                oldStartVnode.elm as Node
                            );
                        } else {
                            patchVnode(
                                elmToMove,
                                newStartVnode,
                                insertedVnodeQueue
                            );
                            oldCh[idxInOld] = undefined as any;
                            api.insertBefore(
                                parentElm,
                                elmToMove.elm as Node,
                                oldStartVnode.elm as Node
                            );
                        }
                        newStartVnode = newCh[++newStartIdx];
                    }
                }
            }
            // 新舊數(shù)組其中一個(gè)到達(dá)末尾
            if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
                if (oldStartIdx > oldEndIdx) {
                    // 如果舊數(shù)組先到達(dá)末尾,說(shuō)明新數(shù)組還有更多的元素,這些元素都是新增的,說(shuō)以一次性插入
                    before =
                        newCh[newEndIdx + 1] == null
                            ? null
                            : newCh[newEndIdx + 1].elm;
                    addVnodes(
                        parentElm,
                        before,
                        newCh,
                        newStartIdx,
                        newEndIdx,
                        insertedVnodeQueue
                    );
                } else {
                    // 如果新數(shù)組先到達(dá)末尾,說(shuō)明新數(shù)組比舊數(shù)組少了一些元素,所以一次性刪除
                    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
                }
            }
        }

    addVnodes方法

    主要功能就是添加 Vnodes 到 真實(shí) DOM 中。

      function addVnodes(
        parentElm: Node,
        before: Node | null,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number,
        insertedVnodeQueue: VNodeQueue
      
    {
        for (; startIdx <= endIdx; ++startIdx) {
          const ch = vnodes[startIdx];
          if (ch != null) {
            api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
          }
        }
      }
    removeVnodes方法

    主要邏輯如下:

    • 循環(huán)觸發(fā) destroy 鉤子,遞歸觸發(fā)子節(jié)點(diǎn)的鉤子
    • 觸發(fā) remove 鉤子,利用 createRmCb , 在所有監(jiān)聽(tīng)器執(zhí)行后,才調(diào)用 api.removeChild,刪除真正的 DOM 節(jié)點(diǎn)
      function invokeDestroyHook(vnode: VNode{
        const data = vnode.data;
        if (data !== undefined) {
          // 執(zhí)行的destroy 鉤子函數(shù)
          data?.hook?.destroy?.(vnode);
          // 調(diào)用模塊的destroy鉤子函數(shù)
          for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
          // 執(zhí)行子節(jié)點(diǎn)的destroy鉤子函數(shù)
          if (vnode.children !== undefined) {
            for (let j = 0; j < vnode.children.length; ++j) {
              const child = vnode.children[j];
              if (child != null && typeof child !== "string") {
                invokeDestroyHook(child);
              }
            }
          }
        }
      }
    //創(chuàng)建一個(gè)刪除的回調(diào),多次調(diào)用這個(gè)回調(diào),直到監(jiān)聽(tīng)器都沒(méi)了,就刪除元素
    function createRmCb(childElm: Node, listeners: number{
        return function rmCb({
          if (--listeners === 0) {
            const parent = api.parentNode(childElm) as Node;
            api.removeChild(parent, childElm);
          }
        };
      }
      function removeVnodes(
        parentElm: Node,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number
      
    ): void 
    {
        for (; startIdx <= endIdx; ++startIdx) {
          let listeners: number;
          let rm: () => void;
          const ch = vnodes[startIdx];
          if (ch != null) {
            // 如果sel 有值
            if (isDef(ch.sel)) {
              invokeDestroyHook(ch);

              // 防止重復(fù)調(diào)用
              listeners = cbs.remove.length + 1;
              // 創(chuàng)建刪除的回調(diào)函數(shù)
              rm = createRmCb(ch.elm!, listeners);
              for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
              // 執(zhí)行remove鉤子函數(shù)
              const removeHook = ch?.data?.hook?.remove;
              if (isDef(removeHook)) {
                removeHook(ch, rm);
              } else {
                // 如果沒(méi)有鉤子函數(shù),直接調(diào)用刪除元素的方法
                rm();
              }
            } else {
              // Text node
              // 如果是文本節(jié)點(diǎn),直接是調(diào)用刪除元素的方法
              api.removeChild(parentElm, ch.elm!);
            }
          }
        }
      }
    createElm方法

    將 vnode 轉(zhuǎn)換成真正的 DOM 元素。

    主要邏輯如下:

    • 觸發(fā) init 鉤子
    • 處理注釋節(jié)點(diǎn)
    • 創(chuàng)建元素并設(shè)置 id , class
    • 觸發(fā)模塊 create 鉤子 。
    • 處理子節(jié)點(diǎn)
    • 處理文本節(jié)點(diǎn)
    • 觸發(fā) vnodeData 的 create 鉤子
     /*
      1.觸發(fā)鉤子函數(shù)init
      2.把vnode轉(zhuǎn)換為DOM對(duì)象,存儲(chǔ)到vnode.elm中
      - sel是!--》創(chuàng)建注釋節(jié)點(diǎn)
      - sel不為空 --》創(chuàng)建對(duì)應(yīng)的DOM對(duì)象;觸發(fā)模塊的鉤子函數(shù)create;創(chuàng)建所有子節(jié)點(diǎn)對(duì)應(yīng)的DOM對(duì)象;觸發(fā)鉤子函數(shù)create;如果是vnode有inset鉤子函數(shù),追加到隊(duì)列
      - sel為空 --》創(chuàng)建文本節(jié)點(diǎn)
      3.返回vnode.elm
      */

      function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any;
        let data = vnode.data;
        if (data !== undefined) {
          // 執(zhí)行init鉤子函數(shù)
          const init = data.hook?.init;
          if (isDef(init)) {
            init(vnode);
            data = vnode.data;
          }
        }
        // 把vnode轉(zhuǎn)換成真實(shí)dom對(duì)象(沒(méi)有渲染到頁(yè)面)
        const children = vnode.children;
        const sel = vnode.sel;
        if (sel === "!") {
          // 如果選擇器是!,創(chuàng)建注釋節(jié)點(diǎn)
          if (isUndef(vnode.text)) {
            vnode.text = "";
          }
          vnode.elm = api.createComment(vnode.text!);
        } else if (sel !== undefined) {
          // 如果選擇器不為空
          // 解析選擇器
          // Parse selector
          const hashIdx = sel.indexOf("#");
          const dotIdx = sel.indexOf(".", hashIdx);
          const hash = hashIdx > 0 ? hashIdx : sel.length;
          const dot = dotIdx > 0 ? dotIdx : sel.length;
          const tag =
            hashIdx !== -1 || dotIdx !== -1
              ? sel.slice(0Math.min(hash, dot))
              : sel;
          const elm = (vnode.elm =
            isDef(data) && isDef((i = data.ns))
              ? api.createElementNS(i, tag, data)
              : api.createElement(tag, data));
          if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
          if (dotIdx > 0)
            elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g" "));
          // 執(zhí)行模塊的create鉤子函數(shù)
          for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
          // 如果vnode中有子節(jié)點(diǎn),創(chuàng)建子Vnode對(duì)應(yīng)的DOM元素并追加到DOM樹(shù)上
          if (is.array(children)) {
            for (i = 0; i < children.length; ++i) {
              const ch = children[i];
              if (ch != null) {
                api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
              }
            }
          } else if (is.primitive(vnode.text)) {
            // 如果vode的text值是string/number,創(chuàng)建文本節(jié)點(diǎn)并追加到DOM樹(shù)
            api.appendChild(elm, api.createTextNode(vnode.text));
          }
          const hook = vnode.data!.hook;
          if (isDef(hook)) {
            // 執(zhí)行傳入的鉤子 create
            hook.create?.(emptyNode, vnode);
            if (hook.insert) {
              insertedVnodeQueue.push(vnode);
            }
          }
        } else {
          // 如果選擇器為空,創(chuàng)建文本節(jié)點(diǎn)
          vnode.elm = api.createTextNode(vnode.text!);
        }
        // 返回新創(chuàng)建的DOM
        return vnode.elm;
      }

    參考資料

    https://foio.github.io/virtual-dom/;https://tech.tuya.com/xu-ni-dom/;https://github.com/snabbdom/snabbdom/;https://segmentfault.com/a/1190000017519084/;https://qastack.cn/programming/21965738/what-is-virtual-dom

    結(jié)語(yǔ)

    謝謝閱讀,如果你覺(jué)得對(duì)你有幫助,歡迎一鍵三連,另外,我自己創(chuàng)辦了一個(gè)公眾號(hào),你可以關(guān)注它。 VX搜索:前端歷劫之路 。關(guān)注后,我可以拉你進(jìn)學(xué)習(xí)交流群。掌握最新前端動(dòng)態(tài),一起學(xué)習(xí)進(jìn)步。


    瀏覽 68
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

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

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <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人妻人人揉人人躁人人 | 欧美成人在线网站 | 天堂网在线中文 |