帶你了解虛擬 DOM(Virtual DOM)—— Snabbdom 的使用以及源碼解析

Study Notes

本博主會持續(xù)更新各種前端的技術灶似,如果各位道友喜歡肆汹,可以關注痰滋、收藏、點贊下本博主的文章站刑。

虛擬 DOM(Virtual DOM)

什么是 Virtual DOM

  • Virtual DOM(虛擬 DOM)另伍,是由普通的 JS 對象來描述 DOM 對象,因為不是真實的 DOM 對象绞旅,所以叫 Virtual DOM
  • 真實 DOM 成員
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
}
console.log(s)
// 打印結(jié)果
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
ntTiming,previousElementSibling,nextElementSibling,children,firstElement
Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
veEventListener,dispatchEvent
  • 可以使用 Virtual DOM 來描述真實 DOM摆尝,示例
{
  "sel": "div",
  "data": {},
  "children": undefined,
  "text": "Hello Virtual DOM",
  "elm": undefined,
  "key": undefined
}

為什么使用 Virtual DOM

  • 手動操作 DOM 比較麻煩愕宋,還需要考慮瀏覽器兼容性問題,雖然有 jQuery 等庫簡化 DOM 操作结榄,但是隨著項目的復雜 DOM 操作復雜提升
  • 為了簡化 DOM 的復雜操作于是出現(xiàn)了各種 MVVM 框架中贝,MVVM 框架解決了視圖和狀態(tài)的同步問題
  • 為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態(tài)變化的問題臼朗,于是 Virtual DOM 出現(xiàn)了
  • Virtual DOM 的好處是當狀態(tài)改變時不需要立即更新 DOM邻寿,只需要創(chuàng)建一個虛擬樹來描述 DOM, Virtual DOM 內(nèi)部將弄清楚如何有效(diff)的更新 DOM
  • 參考 github 上 virtual-dom 的描述
    • 虛擬 DOM 可以維護程序的狀態(tài)视哑,跟蹤上一次的狀態(tài)
    • 通過比較前后兩次狀態(tài)的差異更新真實 DOM

虛擬 DOM 的作用

  • 維護視圖和狀態(tài)的關系
  • 復雜視圖情況下提升渲染性能
  • 除了渲染 DOM 以外绣否,還可以實現(xiàn) SSR(Nuxt.js/Next.js)、原生應用(Weex/React Native)挡毅、小程序(mpvue/uni-app)等

Virtual DOM 庫

  • Snabbdom
    • Vue 2.x 內(nèi)部使用的 Virtual DOM 就是改造的 Snabbdom
    • 大約 200 SLOC(single line of code)
    • 通過模塊可擴展
    • 源碼使用 TypeScript 開發(fā)
    • 最快的 Virtual DOM 之一
  • virtual-dom

案例演示

Snabbdom

中文翻譯

Snabbdom 基本使用

為了方便使用parcel打包工具

安裝

npm i snabbdom -D

導入 Snabbdom

import { init } from 'snabbdom/init';
import { h } from 'snabbdom/h'; // helper function for creating vnodes

如果遇到下面的錯誤

Cannot resolve dependency 'snabbdom/init'

因為模塊路徑并不是 snabbdom/int蒜撮,這個路徑是作者在 package.json 中的 exports 字段設置的,而我們使用的打包工具不支持 exports 這個字段跪呈,webpack 4 也不支持段磨,webpack 5 beta 支持該字段。該字段在導入 snabbdom/init 的時候會補全路徑成 snabbdom/build/package/init.js耗绿。

{
  "exports": {
    "./init": "./build/package/init.js",
    "./h": "./build/package/h.js",
    "./helpers/attachto": "./build/package/helpers/attachto.js",
    "./hooks": "./build/package/hooks.js",
    "./htmldomapi": "./build/package/htmldomapi.js",
    "./is": "./build/package/is.js",
    "./jsx": "./build/package/jsx.js",
    "./modules/attributes": "./build/package/modules/attributes.js",
    "./modules/class": "./build/package/modules/class.js",
    "./modules/dataset": "./build/package/modules/dataset.js",
    "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
    "./modules/hero": "./build/package/modules/hero.js",
    "./modules/module": "./build/package/modules/module.js",
    "./modules/props": "./build/package/modules/props.js",
    "./modules/style": "./build/package/modules/style.js",
    "./thunk": "./build/package/thunk.js",
    "./tovnode": "./build/package/tovnode.js",
    "./vnode": "./build/package/vnode.js"
  }
}

解決方法一:安裝 Snabbdom@v0.7.4 版本

解決方法二:導入 init苹支、h,以及模塊只要把把路徑補全即可误阻。

import { h } from 'snabbdom/build/package/h';
import { init } from 'snabbdom/build/package/init';
import { classModule } from 'snabbdom/build/package/modules/class';

作者關于該問題的回復

使用

  • init() 是一個高階函數(shù)债蜜,返回 patch()
  • h() 返回虛擬節(jié)點 VNode,這個函數(shù)我們在使用 Vue.js 的時候見過
new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');
  • thunk() 是一種優(yōu)化策略究反,可以在處理不可變數(shù)據(jù)時使用

demo

/**
 * @author Wuner
 * @date 2020/8/1 10:05
 * @description
 */
import { h } from 'snabbdom/build/package/h';
import { init } from 'snabbdom/build/package/init';
// 使用init()函數(shù)創(chuàng)建patch()
// init()的參數(shù)是一個數(shù)組寻定,用于導入模塊,處理屬性/樣式/事件等
let patch = init([]);

// 使用h()函數(shù)創(chuàng)建Vnode
let vnode = h('div#second', [h('h1', '基本使用2'), h('p', 'hello world')]);

let appEl = document.querySelector('#app');

// 把vnode渲染到空的DOM元素(替換)
// 會返回新的vnode
let oldVnode = patch(appEl, vnode);

setTimeout(() => {
  vnode = h('div#second', [h('h1', '基本使用2'), h('p', 'hello snabbdom')]);
  // 把老的視圖更新到新的狀態(tài)
  oldVnode = patch(oldVnode, vnode);

  setTimeout(() => {
    // 卸載DOM精耐,文檔中patch(oldVnode,null)有誤
    // h('!')創(chuàng)建注釋
    patch(oldVnode, h('!'));
  }, 1000);
}, 2000);

Snabbdom 模塊使用

Snabbdom 的核心庫并不能處理元素的屬性/樣式/事件等狼速,如果需要處理的話,可以使用模塊

常用模塊

  • 官方提供了 6 個模塊
    • attributes
      • 設置 DOM 元素的屬性黍氮,使用 setAttribute ()
      • 處理布爾類型的屬性
    • props
      • 和 attributes 模塊相似唐含,設置 DOM 元素的屬性 element[attr] = value
      • 不處理布爾類型的屬性
    • class
      • 切換類樣式
      • 注意:給元素設置類樣式是通過 sel 選擇器
    • dataset
      • 設置 data-* 的自定義屬性
    • eventListeners
      • 注冊和移除事件
    • style
      • 設置行內(nèi)樣式,支持動畫
      • delayed/remove/destroy

demo

/**
 * @author Wuner
 * @date 2020/8/1 11:40
 * @description
 */
import { init } from 'snabbdom/build/package/init';
import { h } from 'snabbdom/build/package/h';

// 導入需要的模塊
import { styleModule } from 'snabbdom/build/package/modules/style';
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners';

// 使用 init() 函數(shù)創(chuàng)建 patch()
// init() 的參數(shù)是數(shù)組沫浆,將來可以傳入模塊,處理屬性/樣式/事件等
let patch = init([
  // 注冊模塊
  styleModule,
  eventListenersModule,
]);

// 使用 h() 函數(shù)創(chuàng)建 vnode
let vnode = h(
  'div#third',
  {
    // 設置 DOM 元素的行內(nèi)樣式
    style: {
      backgroundColor: '#999',
    },
    // 注冊事件
    on: {
      click: clickHandel,
    },
  },
  [h('h1', '模塊使用'), h('p', 'hello snabbdom module use')],
);

function clickHandel() {
  // 此處的 this 指向?qū)?vnode
  console.log('我點擊了', this.elm.innerHTML);
}

let appEl = document.querySelector('#app');
patch(appEl, vnode);

Snabbdom 源碼解析

如何學習源碼

  • 先宏觀了解
  • 帶著目標看源碼
  • 看源碼的過程要不求甚解
  • 調(diào)試
  • 參考資料

Snabbdom 的核心

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

Snabbdom 源碼結(jié)構

│-----h.ts h() -----------函數(shù)专执,用來創(chuàng)建 VNode
│-----hooks.ts -----------所有鉤子函數(shù)的定義
│-----htmldomapi.ts ------對 DOM API 的包裝
│-----init.ts ------------設置模塊,創(chuàng)建 patch()
│-----is.ts --------------判斷數(shù)組和原始值的函數(shù)
│-----jsx-global.d.ts ----jsx 的類型聲明文件
│-----jsx.ts -------------處理 jsx
│-----thunk.ts -----------優(yōu)化處理郁油,對復雜視圖不可變值得優(yōu)化
│-----tovnode.ts ---------DOM 轉(zhuǎn)換成 VNode
│-----vnode.ts -----------虛擬節(jié)點定義
│
|-----helpers
│----------attachto.ts ---定義了 vnode.ts 中 AttachData 的數(shù)據(jù)結(jié)構
│
|-----modules ------------所有模塊定義
|----------attributes.ts
|----------class.ts
|----------dataset.ts
|----------eventlisteners.ts
|----------hero.ts --------example 中使用到的自定義鉤子
|----------module.ts ------定義了模塊中用到的鉤子函數(shù)
|----------props.ts
|----------style.ts

h 函數(shù)

  • h() 函數(shù)介紹

    • 在使用 Vue 的時候見過 h() 函數(shù)

      new Vue({
        router,
        store,
        render: (h) => h(App),
      }).$mount('#app');
      
    • h() 函數(shù)最早見于 hyperscript 本股,使用 JavaScript 創(chuàng)建超文本

    • Snabbdom 中的 h() 函數(shù)不是用來創(chuàng)建超文本攀痊,而是創(chuàng)建 VNode

  • 函數(shù)重載

    • 概念

      • 參數(shù)個數(shù)或類型不同的函數(shù)
      • JavaScript 中沒有重載的概念
      • TypeScript 中有重載,不過重載的實現(xiàn)還是通過代碼調(diào)整參數(shù)
    • 重載的示意

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

源碼解析src/package/h.ts

  import ...

  export type VNodes = VNode[];
  export type VNodeChildElement = VNode | string | number | undefined | null;
  export type ArrayOrElement<T> = T | T[];
  export type VNodeChildren = ArrayOrElement<VNodeChildElement>;

  function addNS(
    data: any,
    children: VNodes | undefined,
    sel: string | undefined,
  ): void {...}

  // h函數(shù)的重載
  export function h(sel: string): VNode;
  export function h(sel: string, data: VNodeData | null): VNode;
  export function h(sel: string, children: VNodeChildren): VNode;
  export function h(
    sel: string,
    data: VNodeData | null,
    children: VNodeChildren,
  ): VNode;
  export function h(sel: any, b?: any, c?: any): VNode {
    var data: VNodeData = {};
    var children: any;
    var text: any;
    var i: number;
    //處理參數(shù)拄显,實現(xiàn)重載的機制
    if (c !== undefined) {
      // 處理三個參數(shù)的情況
      // sel苟径、data、children/text
      if (b !== null) {
        data = b;
      }
      // 如果c是數(shù)組躬审,則將c賦值給children
      if (is.array(c)) {
        children = c;
      } else if (is.primitive(c)) {
        // 如果c是數(shù)字或字符串類型棘街,則將c賦值給text
        text = c;
      } else if (c && c.sel) {
        // 如果c是vnode,則將c放到數(shù)組里承边,賦值給children
        children = [c];
      }
    } else if (b !== undefined && b !== null) {
      // 處理兩個參數(shù)的情況
      // 如果b是數(shù)組遭殉,則將b賦值給children
      if (is.array(b)) {
        children = b;
      } else if (is.primitive(b)) {
        // 如果b是數(shù)字或字符串類型,則將b賦值給text
        text = b;
      } else if (b && b.sel) {
        // 如果b是vnode博助,則將b放到數(shù)組里险污,賦值給children
        children = [b];
      } else {
        data = b;
      }
    }
    if (children !== undefined) {
      // 處理children里的原始值(string/number)
      for (i = 0; i < children.length; ++i) {
        // 如果children里的值是字符串或者數(shù)字(string/number)類型,則創(chuàng)建文本節(jié)點
        if (is.primitive(children[i]))
          children[i] = vnode(
            undefined,
            undefined,
            undefined,
            children[i],
            undefined,
          );
      }
    }
    if (
      sel[0] === 's' &&
      sel[1] === 'v' &&
      sel[2] === 'g' &&
      (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
    ) {
      // 如果是svg富岳,則添加命名空間
      addNS(data, children, sel);
    }
    // 返回vnode(虛擬節(jié)點)
    return vnode(sel, data, children, text, undefined);
  }

VNode

一個 VNode 就是一個虛擬節(jié)點用來描述一個 DOM 元素蛔糯,如果這個 VNode 有 children 就是 Virtual DOM

源碼解析src/package/vnode.ts

import ...

export type Key = string | number;

export interface VNode {
  // 選擇器
  sel: string | undefined;
  // 節(jié)點數(shù)據(jù),屬性窖式、樣式渤闷、事件機制等
  data: VNodeData | undefined;
  // 子節(jié)點,其和text屬性互斥脖镀,只能存在一個
  children: Array<VNode | string> | undefined;
  // 記錄vnode對應的真實DOM
  elm: Node | undefined;
  // 節(jié)點中的內(nèi)容飒箭,其和children屬性互斥,只能存在一個
  text: string | undefined;
  // 用于優(yōu)化
  key: Key | undefined;
}

export interface VNodeData {...}

export function vnode(
  sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined,
): VNode {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

init(src/package/init.ts)

  • patch(oldVnode, newVnode)
  • 打補丁蜒灰,把新節(jié)點中變化的內(nèi)容渲染到真實 DOM弦蹂,最后返回新節(jié)點作為下一次處理的舊節(jié)點
  • 對比新舊 VNode 是否相同節(jié)點(節(jié)點的 key 和 sel 相同)
  • 如果不是相同節(jié)點,刪除之前的內(nèi)容强窖,重新渲染
  • 如果是相同節(jié)點凸椿,再判斷新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同翅溺,直接更新文本內(nèi)容
  • 如果新的 VNode 有 children脑漫,判斷子節(jié)點是否有變化,判斷子節(jié)點的過程使用的就是 diff 算法
  • diff 過程只進行同層級比較

init()

  • 功能:init(modules, domApi)咙崎,返回 patch() 函數(shù)(高階函數(shù))
  • 為什么要使用高階函數(shù)优幸?
    • 因為 patch() 函數(shù)再外部會調(diào)用多次,每次調(diào)用依賴一些參數(shù)褪猛,比如:modules/domApi/cbs
    • 通過高階函數(shù)讓 init() 內(nèi)部形成閉包网杆,返回的 patch() 可以訪問到 modules/domApi/cbs,而不需要重新創(chuàng)建
  • init() 在返回 patch() 之前,首先收集了所有模塊中的鉤子函數(shù)存儲到 cbs 對象中

源碼解析

import ...

type NonUndefined<T> = T extends undefined ? never : T;

function isUndef(s: any): boolean {...}
function isDef<A>(s: A): s is NonUndefined<A> {...}

type VNodeQueue = VNode[];

const emptyNode = vnode('', {}, [], undefined, undefined);

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {...}

function isVnode(vnode: any): vnode is VNode {...}

type KeyToIndexMap = { [key: string]: number };

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
};

type ModuleHooks = ArraysOf<Required<Module>>;

function createKeyToOldIdx(
  children: VNode[],
  beginIdx: number,
  endIdx: number,
): KeyToIndexMap {...}

const hooks: Array<keyof Module> = [
  'create',
  'update',
  'remove',
  'destroy',
  'pre',
  'post',
];

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é)點的api
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  // 把傳入的所有模塊的鉤子函數(shù)碳却,統(tǒng)一存儲到cbs(callbacks)對象中
  for (i = 0; i < hooks.length; ++i) {
    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對應的鉤子函數(shù)數(shù)組中
        (cbs[hooks[i]] as any[]).push(hook);
      }
    }
  }

  function emptyNodeAt(elm: Element) {...}

  function createRmCb(childElm: Node, listeners: number) {...}

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {...}

  function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue,
  ) {...}

  function invokeDestroyHook(vnode: VNode) {...}

  function removeVnodes(
    parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
  ): void {...}

  function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue,
  ) {...}

  function patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue,
  ) {...}

  // init內(nèi)部返回patch函數(shù)队秩,把vnode渲染成真是的DOM,并返回vnode
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {...};
}

patch

  • 功能:
    • 傳入新舊 VNode昼浦,對比差異馍资,把差異渲染到 DOM
    • 返回新的 VNode,作為下一次 patch() 的 oldVnode
  • 執(zhí)行過程:
    • 首先執(zhí)行模塊中的鉤子函數(shù) pre
    • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
      • 調(diào)用 patchVnode()关噪,找節(jié)點的差異并更新 DOM
    • 如果 oldVnode 是 DOM 元素
      • 把 DOM 元素轉(zhuǎn)換成 oldVnode
      • 調(diào)用 createElm() 把 vnode 轉(zhuǎn)換為真實 DOM鸟蟹,記錄到 vnode.elm
      • 把剛創(chuàng)建的 DOM 元素插入到 parent 中
      • 移除老節(jié)點
      • 觸發(fā)用戶設置的 create 鉤子函數(shù)

源碼解析

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
  let i: number, elm: Node, parent: Node;
  // 保存新插入節(jié)點的隊列,為了觸發(fā)鉤子函數(shù)
  const insertedVnodeQueue: VNodeQueue = [];
  // 遍歷cbs(callbacks)執(zhí)行模塊中的pre鉤子函數(shù)
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  // 如果不是節(jié)點時色洞,為真實DOM創(chuàng)建空的虛擬節(jié)點
  if (!isVnode(oldVnode)) {
    oldVnode = emptyNodeAt(oldVnode);
  }

  // 如果老的虛擬節(jié)點和新的虛擬節(jié)點相同戏锹,則去尋找新舊節(jié)點的差異,并更新DOM
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    // 如果新舊節(jié)點不同火诸,則vnode創(chuàng)建對應的DOM
    // 獲取當前的DOM元素
    elm = oldVnode.elm!;
    // 獲取當前DOM元素的父元素
    parent = api.parentNode(elm) as Node;

    // 觸發(fā)init/create鉤子函數(shù)锦针,創(chuàng)建DOM
    createElm(vnode, insertedVnodeQueue);

    // 如果父元素不為空,則把vnode對應的DOM插入到文檔中
    if (parent !== null) {
      api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
      // 移除老節(jié)點
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }

  // 遍歷insertedVnodeQueue置蜀,如果存在用戶設置的insert鉤子函數(shù)奈搜,則執(zhí)行該函數(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
  return vnode;
};

createElm

  • 功能:
    • createElm(vnode, insertedVnodeQueue),返回創(chuàng)建的 DOM 元素
    • 創(chuàng)建 vnode 對應的 DOM 元素
  • 執(zhí)行過程:
    • 首先觸發(fā)用戶設置的 init 鉤子函數(shù)
    • 如果選擇器是!盯荤,創(chuàng)建注釋節(jié)點
    • 如果選擇器為空馋吗,創(chuàng)建文本節(jié)點
    • 如果選擇器不為空
      • 解析選擇器,設置標簽的 id 和 class 屬性
      • 執(zhí)行模塊的 create 鉤子函數(shù)
      • 如果 vnode 有 children秋秤,創(chuàng)建子 vnode 對應的 DOM宏粤,追加到 DOM 樹
      • 如果 vnode 的 text 值是 string/number,創(chuàng)建文本節(jié)點并追擊到 DOM 樹
      • 執(zhí)行用戶設置的 create 鉤子函數(shù)
      • 如果有用戶設置的 insert 鉤子函數(shù)灼卢,把 vnode 添加到隊列中

源碼解析

function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  let i: any;
  let data = vnode.data;
  // 如果存在用戶設置的init鉤子函數(shù)绍哎,且不為undefined,則執(zhí)行該鉤子函數(shù)
  if (data !== undefined) {
    const init = data.hook?.init;
    if (isDef(init)) {
      init(vnode);
      // 為什么重新賦值鞋真,是為了防止用戶設置新的
      data = vnode.data;
    }
  }
  const children = vnode.children;
  const sel = vnode.sel;
  if (sel === '!') {
    if (isUndef(vnode.text)) {
      vnode.text = '';
    }
    // 創(chuàng)建并返回一個注釋節(jié)點
    vnode.elm = api.createComment(vnode.text!);
  } else if (sel !== undefined) {
    // 如果選擇器不為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(0, Math.min(hash, dot)) : sel;
    // 如果data并且data.ns不為undefined時崇堰,創(chuàng)建一個具有指定的命名空間URI和限定名稱的元素
    // 否則創(chuàng)建一個不指定命名空間URI的元素
    const elm = (vnode.elm =
      isDef(data) && isDef((i = data.ns))
        ? api.createElementNS(i, tag)
        : api.createElement(tag));
    if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
    if (dotIdx > 0)
      elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
    // 遍歷執(zhí)行cbs(callbacks)中的create鉤子函數(shù)
    for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
    // 如果vnode中存在子節(jié)點,創(chuàng)建vnode對應的DOM元素涩咖,并追加到DOM樹上
    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)) {
      // 如果vnode中的text是string或者number類型的
      // 則創(chuàng)建文本節(jié)點海诲,并追加到DOM樹上
      api.appendChild(elm, api.createTextNode(vnode.text));
    }
    // 如果存在用戶設置的鉤子函數(shù),并且不為undefined時
    const hook = vnode.data!.hook;
    if (isDef(hook)) {
      // 如果存在執(zhí)行create鉤子函數(shù)檩互,則執(zhí)行該鉤子函數(shù)
      hook.create?.(emptyNode, vnode);
      if (hook.insert) {
        // 如果存在執(zhí)行insert鉤子函數(shù)特幔,則把vnode添加到隊列中,為后續(xù)執(zhí)行insert鉤子函數(shù)做準備
        insertedVnodeQueue.push(vnode);
      }
    }
  } else {
    // 如果選擇器為undefined時盾似,創(chuàng)建文本節(jié)點
    vnode.elm = api.createTextNode(vnode.text!);
  }
  // 返回新創(chuàng)建的DOM
  return vnode.elm;
}

patchVnode

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 對比 oldVnode 和 vnode 的差異敬辣,把差異渲染到 DOM
  • 執(zhí)行過程:
    • 首先執(zhí)行用戶設置的 prepatch 鉤子函數(shù)
    • 執(zhí)行 create 鉤子函數(shù)
      • 首先執(zhí)行模塊的 create 鉤子函數(shù)
      • 然后執(zhí)行用戶設置的 create 鉤子函數(shù)
  • 如果 vnode.text 未定義
    • 如果 oldVnode.children 和 vnode.children 都有值
      • 調(diào)用 updateChildren()
      • 使用 diff 算法對比子節(jié)點雪标,更新子節(jié)點
    • 如果 vnode.children 有值零院, oldVnode.children 無值
      • 清空 DOM 元素
      • 調(diào)用 addVnodes() 溉跃,批量添加子節(jié)點
    • 如果 oldVnode.children 有值, vnode.children 無值
      • 調(diào)用 removeVnodes() 告抄,批量移除子節(jié)點
    • 如果 oldVnode.text 有值
      • 清空 DOM 元素的內(nèi)容
  • 如果設置了 vnode.text 并且和 oldVnode.text 不同
    • 如果老節(jié)點有子節(jié)點撰茎,全部移除
    • 設置 DOM 元素的 textContent 為 vnode.text
  • 最后執(zhí)行用戶設置的 postpatch 鉤子函數(shù)

源碼解析

function patchVnode(
  oldVnode: VNode,
  vnode: VNode,
  insertedVnodeQueue: VNodeQueue,
) {
  // 如果存在用戶設置的prepatch鉤子函數(shù),則執(zhí)行該鉤子函數(shù)
  const hook = vnode.data?.hook;
  hook?.prepatch?.(oldVnode, vnode);
  const elm = (vnode.elm = oldVnode.elm!);
  const oldCh = oldVnode.children as VNode[];
  const ch = vnode.children as VNode[];
  // 如果新舊節(jié)點相同打洼,則直接返回
  if (oldVnode === vnode) return;
  if (vnode.data !== undefined) {
    // 遍歷執(zhí)行cbs(callbacks)中的update鉤子函數(shù)
    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    // 如果存在用戶設置的update鉤子函數(shù)龄糊,則執(zhí)行該鉤子函數(shù)
    vnode.data.hook?.update?.(oldVnode, vnode);
  }
  if (isUndef(vnode.text)) {
    // vnode中的text為undefined時

    if (isDef(oldCh) && isDef(ch)) {
      // 舊虛擬節(jié)點存在子節(jié)點并且新虛擬節(jié)點也存在子節(jié)點時
      // 如果新舊虛擬節(jié)點中的子節(jié)點不相同時,使用diff算法對比子節(jié)點募疮,更新子節(jié)點
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
    } else if (isDef(ch)) {
      // 舊虛擬節(jié)點不存在子節(jié)點并且新虛擬節(jié)點存在子節(jié)點時
      // 如果舊虛擬節(jié)點中存在text炫惩,則清空DOM元素的內(nèi)容
      if (isDef(oldVnode.text)) api.setTextContent(elm, '');
      // 批量添加子節(jié)點
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 舊虛擬節(jié)點存在子節(jié)點并且新虛擬節(jié)點不存在子節(jié)點時
      // 批量刪除子節(jié)點
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 如果舊虛擬節(jié)點存在text,則清空DOM元素
      api.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // 舊虛擬節(jié)點的text與新虛擬節(jié)點的text不相同時
    // 如果舊虛擬節(jié)點存在子節(jié)點阿浓,批量刪除子節(jié)點
    if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    // 將DOM元素的textContent設置為vnode.text
    api.setTextContent(elm, vnode.text!);
  }
  // 如果存在用戶設置的postpatch鉤子函數(shù)他嚷,則執(zhí)行該鉤子函數(shù)
  hook?.postpatch?.(oldVnode, vnode);
}

updateChildren

  • 功能:

    • diff 算法的核心,對比新舊節(jié)點的 children芭毙,更新 DOM
  • 執(zhí)行過程:

    • 要對比兩棵樹的差異筋蓖,我們可以取第一棵樹的每一個節(jié)點依次和第二課樹的每一個節(jié)點比較,但是這樣的時間復雜度為 O(n^3)
    • 在 DOM 操作的時候我們很少很少會把一個父節(jié)點移動/更新到某一個子節(jié)點
    • 因此只需要找同級別的子節(jié)點依次比較退敦,然后再找下一級別的節(jié)點比較粘咖,這樣算法的時間復雜度為 O(n)
  • 在進行同級別節(jié)點比較的時候,首先會對新老節(jié)點數(shù)組的開始和結(jié)尾節(jié)點設置標記索引侈百,遍歷的過程中移動索引
  • 在對開始和結(jié)束節(jié)點比較的時候瓮下,總共有四種情況
    • oldStartVnode / newStartVnode (舊開始節(jié)點 / 新開始節(jié)點)
    • oldEndVnode / newEndVnode (舊結(jié)束節(jié)點 / 新結(jié)束節(jié)點)
    • oldStartVnode / oldEndVnode (舊開始節(jié)點 / 新結(jié)束節(jié)點)
    • oldEndVnode / newStartVnode (舊結(jié)束節(jié)點 / 新開始節(jié)點)
  • 開始節(jié)點和結(jié)束節(jié)點比較,這兩種情況類似
    • oldStartVnode / newStartVnode (舊開始節(jié)點 / 新開始節(jié)點)
    • oldEndVnode / newEndVnode (舊結(jié)束節(jié)點 / 新結(jié)束節(jié)點)
  • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
    • 調(diào)用 patchVnode() 對比和更新節(jié)點
    • 把舊開始和新開始索引往后移動 oldStartIdx++ / oldEndIdx++
  • oldStartVnode / newEndVnode (舊開始節(jié)點 / 新結(jié)束節(jié)點) 相同
    • 調(diào)用 patchVnode() 對比和更新節(jié)點
    • 把 oldStartVnode 對應的 DOM 元素钝域,移動到右邊
    • 更新索引
  • oldEndVnode / newStartVnode (舊結(jié)束節(jié)點 / 新開始節(jié)點) 相同
    • 調(diào)用 patchVnode() 對比和更新節(jié)點
    • 把 oldEndVnode 對應的 DOM 元素讽坏,移動到左邊
    • 更新索引
  • 如果不是以上四種情況
    • 遍歷新節(jié)點,使用 newStartNode 的 key 在老節(jié)點數(shù)組中找相同節(jié)點
    • 如果沒有找到网梢,說明 newStartNode 是新節(jié)點
      • 創(chuàng)建新節(jié)點對應的 DOM 元素震缭,插入到 DOM 樹中
    • 如果找到了
      • 判斷新節(jié)點和找到的老節(jié)點的 sel 選擇器是否相同
      • 如果不相同,說明節(jié)點被修改了
        • 重新創(chuàng)建對應的 DOM 元素战虏,插入到 DOM 樹中
      • 如果相同拣宰,把 elmToMove 對應的 DOM 元素,移動到左邊
  • 循環(huán)結(jié)束
    • 當老節(jié)點的所有子節(jié)點先遍歷完 (oldStartIdx > oldEndIdx)烦感,循環(huán)結(jié)束
    • 新節(jié)點的所有子節(jié)點先遍歷完 (newStartIdx > newEndIdx)巡社,循環(huán)結(jié)束
  • 如果老節(jié)點的數(shù)組先遍歷完(oldStartIdx > oldEndIdx),說明新節(jié)點有剩余手趣,把剩余節(jié)點批量插入到右邊
  • 如果新節(jié)點的數(shù)組先遍歷完(newStartIdx > newEndIdx)晌该,說明老節(jié)點有剩余肥荔,把剩余節(jié)點批量刪除

源碼解析

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) {
    // 索引變化后,可能會把節(jié)點設置為null

    if (oldStartVnode == null) {
      // 當舊開始節(jié)點為null時朝群,移動舊開始索引
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
    } else if (oldEndVnode == null) {
      // 當舊結(jié)束節(jié)點為null時燕耿,移動舊結(jié)束索引
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode == null) {
      // 當新開始節(jié)點為null時,移動新開始索引
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode == null) {
      // 當新結(jié)束節(jié)點為null時姜胖,移動新結(jié)束索引
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 舊開始節(jié)點和新開始節(jié)點相同時
      // 調(diào)用patchVnode()對比和更新節(jié)點
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
      // 移動舊開始索引和移動新開始索引
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 舊結(jié)束節(jié)點和新結(jié)束節(jié)點相同時
      // 調(diào)用patchVnode()對比和更新節(jié)點
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
      // 移動舊結(jié)束索引和移動新結(jié)束索引
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      // 舊開始節(jié)點和新結(jié)束節(jié)點相同時
      // 調(diào)用patchVnode()對比和更新節(jié)點
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
      // 把舊開始節(jié)點對應的 DOM 元素誉帅,移動到右邊
      api.insertBefore(
        parentElm,
        oldStartVnode.elm!,
        api.nextSibling(oldEndVnode.elm!),
      );
      // 移動舊開始索引和移動新結(jié)束索引
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      // 舊結(jié)束節(jié)點和新開始節(jié)點相同時
      // 調(diào)用patchVnode()對比和更新節(jié)點
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
      // 把舊結(jié)束節(jié)點對應的 DOM 元素,移動到左邊
      api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
      // 移動舊結(jié)束索引和移動新開始索引
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 開始節(jié)點和結(jié)束節(jié)點都不同時
      // 使用新開始節(jié)點的key在老節(jié)點數(shù)組中找相同節(jié)點
      // 根據(jù)舊節(jié)點數(shù)組生成對應的key和index的map對象
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      // 遍歷開始節(jié)點右莱,從舊節(jié)點中找相同的key的舊節(jié)點索引
      idxInOld = oldKeyToIdx[newStartVnode.key as string];
      if (isUndef(idxInOld)) {
        // New element
        // 如果舊節(jié)點索引不存在蚜锨,則開始節(jié)點是一個新的節(jié)點
        // 創(chuàng)建DOM元素并插入DOM樹
        api.insertBefore(
          parentElm,
          createElm(newStartVnode, insertedVnodeQueue),
          oldStartVnode.elm!,
        );
      } else {
        // 舊節(jié)點索引存在時,即找到了相同key的舊節(jié)點
        // 將舊節(jié)點記錄到elmToMove中
        elmToMove = oldCh[idxInOld];
        if (elmToMove.sel !== newStartVnode.sel) {
          // 如果新舊節(jié)點選擇器不同時慢蜓,創(chuàng)建新開始節(jié)點對應的DOM元素亚再,并插入到DOM樹上
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!,
          );
        } else {
          // 新舊節(jié)點的選擇器相同時
          // 調(diào)用patchVnode()對比和更新節(jié)點
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
          // 將舊節(jié)點數(shù)組中的該索引位置的節(jié)點置為undefined
          oldCh[idxInOld] = undefined as any;
          // 把elmToMove對應的DOM元素,移到左邊
          api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
        }
      }
      // 移動新開始索引
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // 循環(huán)結(jié)束晨抡,舊節(jié)點數(shù)組先遍歷完成氛悬,或新節(jié)點數(shù)組先遍歷完成
  if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
      // 如果舊節(jié)點數(shù)組先遍歷完成,說明有新節(jié)點剩余
      // 把剩余節(jié)點批量插入到右邊
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue,
      );
    } else {
      // 如果新節(jié)點數(shù)組先遍歷完成凄诞,說明有舊節(jié)點剩余
      // 把剩余節(jié)點批量移除
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }
}

Modules 源碼

  • patch() -> patchVnode() -> updateChildren()
  • Snabbdom 為了保證核心庫的精簡圆雁,把處理元素的屬性/事件/樣式等工作,放置到模塊中
  • 模塊可以按照需要引入
  • 模塊的使用可以查看官方文檔
  • 模塊實現(xiàn)的核心是基于 Hooks

Hooks

鉤子是一種掛鉤到 DOM 節(jié)點生命周期的方法帆谍。Snabbdom 提供了豐富的鉤子可以選擇伪朽。模塊使用鉤子來擴展 Snabbdom,在普通代碼中汛蝙,鉤子用于在虛擬節(jié)點生命周期的期望點執(zhí)行任意代碼烈涮。

概覽

Name Triggered when Arguments to callback
pre patch 過程開始 none
init 一個虛擬節(jié)點被添加 vnode
create 基于一個虛擬節(jié)點,一個 DOM 被創(chuàng)建 emptyVnode, vnode
insert 一個元素被插入到 DOM 中 vnode
prepatch 一個元素即將被修補(patched) oldVnode, vnode
update 一個元素正在被更新 oldVnode, vnode
postpatch 一個元素已經(jīng)被修補完成(patched) oldVnode, vnode
destroy 元素被直接或間接刪除 vnode
remove 元素將直接從 DOM 中刪除 vnode, removeCallback
post 修補(patch)過程結(jié)束 none

以下鉤子可用于模塊:pre窖剑、create坚洽、update、destroy西土、remove讶舰、post。

以下鉤子可用于單個元素的鉤子屬性:init需了、create跳昼、insert、prepatch肋乍、update鹅颊、postpatch、destroy墓造、remove堪伍。

源碼解析src/package/hooks.ts

export interface Hooks {
  // patch 函數(shù)開始執(zhí)行的時候觸發(fā)
  pre?: PreHook;
  // createElm 函數(shù)開始之前的時候觸發(fā)
  // 在把 VNode 轉(zhuǎn)換成真實 DOM 之前觸發(fā)
  init?: InitHook;
  // createElm 函數(shù)末尾調(diào)用
  // 創(chuàng)建完真實 DOM 后觸發(fā)
  create?: CreateHook;
  // patch 函數(shù)末尾執(zhí)行
  // 真實 DOM 添加到 DOM 樹中觸發(fā)
  insert?: InsertHook;
  // patchVnode 函數(shù)開頭調(diào)用
  // 開始對比兩個 VNode 的差異之前觸發(fā)
  prepatch?: PrePatchHook;
  // patchVnode 函數(shù)開頭調(diào)用
  // 兩個 VNode 對比過程中觸發(fā)锚烦,比 prepatch 稍晚
  update?: UpdateHook;
  // patchVnode 的最末尾調(diào)用
  // 兩個 VNode 對比結(jié)束執(zhí)行
  postpatch?: PostPatchHook;
  // removeVnodes -> invokeDestroyHook 中調(diào)用
  // 在刪除元素之前觸發(fā),子節(jié)點的 destroy 也被觸發(fā)
  destroy?: DestroyHook;
  // removeVnodes 中調(diào)用
  // 元素被刪除的時候觸發(fā)
  remove?: RemoveHook;
  // patch 函數(shù)的最后調(diào)用
  // patch 全部執(zhí)行完畢觸發(fā)
  post?: PostHook;
}

attributes

updateAttrs 函數(shù)功能

  • 更新節(jié)點屬性
  • 如果節(jié)點屬性值是 true 設置空置
  • 如果節(jié)點屬性值是 false 移除屬性

源碼解析src/package/hooks.ts帝雇,其他模塊類似

function updateAttrs(oldVnode: VNode, vnode: VNode): void {
  var key: string;
  var elm: Element = vnode.elm as Element;
  var oldAttrs = (oldVnode.data as VNodeData).attrs;
  var attrs = (vnode.data as VNodeData).attrs;

  // 新舊節(jié)點沒有 attrs 屬性涮俄,返回
  if (!oldAttrs && !attrs) return;
  // 新舊節(jié)點的 attrs 屬性相同,返回
  if (oldAttrs === attrs) return;
  oldAttrs = oldAttrs || {};
  attrs = attrs || {};

  // update modified attributes, add new attributes
  // 遍歷新節(jié)點的屬性
  for (key in attrs) {
    const cur = attrs[key];
    const old = oldAttrs[key];
    // 如果新舊節(jié)點的屬性值不同
    if (old !== cur) {
      // 布爾類型值的處理
      if (cur === true) {
        elm.setAttribute(key, '');
      } else if (cur === false) {
        elm.removeAttribute(key);
      } else {
        // ascii 120 -> x
        // <svg xmlns="http://www.w3.org/2000/svg">
        if (key.charCodeAt(0) !== xChar) {
          elm.setAttribute(key, cur as any);
        } else if (key.charCodeAt(3) === colonChar) {
          // ascii 58 -> :
          // Assume xml namespace
          elm.setAttributeNS(xmlNS, key, cur as any);
        } else if (key.charCodeAt(5) === colonChar) {
          // Assume xlink namespace
          // <svg xmlns="http://www.w3.org/2000/svg">
          elm.setAttributeNS(xlinkNS, key, cur as any);
        } else {
          elm.setAttribute(key, cur as any);
        }
      }
    }
  }
  // remove removed attributes
  // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
  // the other option is to remove all attributes with value == undefined
  // 如果舊節(jié)點的屬性在新節(jié)點中不存在摊求,移除
  for (key in oldAttrs) {
    if (!(key in attrs)) {
      elm.removeAttribute(key);
    }
  }
}

Diff 算法的執(zhí)行過程

  • 循環(huán)結(jié)束
    • 當老節(jié)點的所有子節(jié)點先遍歷完 (oldStartIdx > oldEndIdx)禽拔,循環(huán)結(jié)束
    • 新節(jié)點的所有子節(jié)點先遍歷完 (newStartIdx > newEndIdx)刘离,循環(huán)結(jié)束
  • 如果老節(jié)點的數(shù)組先遍歷完(oldStartIdx > oldEndIdx)室叉,說明新節(jié)點有剩余,把剩余節(jié)點批量插入到右邊
  • 如果新節(jié)點的數(shù)組先遍歷完(newStartIdx > newEndIdx)硫惕,說明老節(jié)點有剩余茧痕,把剩余節(jié)點批量刪除
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市恼除,隨后出現(xiàn)的幾起案子踪旷,更是在濱河造成了極大的恐慌,老刑警劉巖豁辉,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件令野,死亡現(xiàn)場離奇詭異,居然都是意外死亡徽级,警方通過查閱死者的電腦和手機气破,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來餐抢,“玉大人现使,你說我怎么就攤上這事】鹾郏” “怎么了碳锈?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長欺抗。 經(jīng)常有香客問我售碳,道長,這世上最難降的妖魔是什么绞呈? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任贸人,我火速辦了婚禮,結(jié)果婚禮上报强,老公的妹妹穿的比我還像新娘灸姊。我一直安慰自己,他們只是感情好秉溉,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布力惯。 她就那樣靜靜地躺著碗誉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪父晶。 梳的紋絲不亂的頭發(fā)上哮缺,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音甲喝,去河邊找鬼尝苇。 笑死,一個胖子當著我的面吹牛埠胖,可吹牛的內(nèi)容都是我干的糠溜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼直撤,長吁一口氣:“原來是場噩夢啊……” “哼非竿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谋竖,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤红柱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蓖乘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锤悄,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年嘉抒,在試婚紗的時候發(fā)現(xiàn)自己被綠了零聚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡众眨,死狀恐怖握牧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情娩梨,我是刑警寧澤沿腰,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站狈定,受9級特大地震影響颂龙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纽什,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一措嵌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芦缰,春花似錦企巢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽或听。三九已至,卻和暖如春笋婿,著一層夾襖步出監(jiān)牢的瞬間誉裆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工缸濒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留足丢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓庇配,卻偏偏與公主長得像斩跌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子讨永,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前言 DOM是很慢的滔驶。真正的 DOM 元素非常龐大,這是因為標準就是這么設計的卿闹。而且操作它們的時候你要小心翼翼,輕...
    梁王io閱讀 1,109評論 0 1
  • 什么是 Virtual DOM Virtual DOM(虛擬 DOM)萝快,是由普通的 JS 對象來描述 DOM 對象...
    望月從良glh閱讀 258評論 0 0
  • 傳送門vue技術揭秘:https://ustbhuangyi.github.io/vue-analysis/v2/...
    拾錢運閱讀 651評論 0 2
  • snabbdom源碼 現(xiàn)在流行的前端前端庫都使用虛擬dom來提高dom渲染效率锻霎,簡單的來說虛擬dom就是用js來模...
    起飛之路閱讀 720評論 0 2
  • 動態(tài)路由:如果需要獲取動態(tài)路由id,建議使用props方式: 編程式導航: $router有兩種用法,第一種直接添...
    zxhnext閱讀 795評論 0 2