Snabbdom源碼細讀

前言

隨着react、vue等前端框架崛起,虛擬DOM也被愈來愈多的人悉知。虛擬DOM 一個聽起來好高大上的詞,貌似「深不可測」的樣子。但當你去深刻了解以後,會發現虛擬DOM並無那麼神祕,也不是那麼難以理解。不少時候只是不知道從何處下手而已。html

snabbdom就是虛擬DOM的一個簡潔實現。置於爲何選擇snabbdom,誠如官方文檔所說snabbdom:前端

  • 主體代碼只有200多行,簡單易讀且性能優越
  • 功能強大、擴展性強
  • 豐富的鉤子函數

而且vue2.x中的Virtual DOM部分也是基於snabbdom實現的。學習snabbdom能讓你更好的理解什麼是虛擬dom以外,也能更有助於你去讀懂vue源碼。若是你沒聽過或沒用過snabbdom,建議你先去看看官方文檔vue

關於Virtual Dom

在Virtual dom以前,咱們一直是經過js操做dom,而操做dom是很繁瑣的一件事,雖然有jQuery、zepto之類的庫簡化了dom的操做,但依舊繁瑣。衆所周知一個dom對象是很龐大的,在頻繁操做大量dom時還有嚴重的性能問題。node

Virtual dom在解決這兩類問題上獲得了很大的提高。Virtual dom是真實DOM的抽象,能夠理解爲一個純js對象。這個對象只保存了dom必要的信息。Virtual dom能很好的描述一個dom結構。操做一個輕量的js對象要遠比操做dom快的多,且比較先後兩個對象的不一樣來更新dom能夠避免重複操做未變動的dom,同時也能更好的檢測數據變化來更新dom。react

Virtual dom的實現都會有兩個過程:git

  • 根據描述dom的js對象建立出真正的dom樹,再應用到文檔上。
  • 數據變化時,比較舊js和新js對象,獲得對象之間的差別,更新dom。(diff算法)

源碼分析

PS:閱讀博客前本身必定要先去看看源碼哦~^_^github

因爲源碼是使用TypeScript寫的,不是很熟悉的先去看看TypeScript算法

閱讀源碼首先要找到切入點,也就是入口。snabbdom的主要方法也就幾個,分別是inithpatchtovnode。顧名思義init就是咱們要的切入點。結合例子來看能更好的讀懂源碼,咱們先看看官方給的例子api

var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes

var container = document.getElementById('container');

var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
  h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
  ' and this is just normal text',
  h('a', {props: {href: '/foo'}}, 'I\'ll take you places!') ]); // Patch into empty DOM element – this modifies the DOM as a side effect patch(container, vnode); var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
  h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', {props: {href: '/bar'}}, 'I\'ll take you places!') ]); // Second `patch` invocation patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state // to unmount from the DOM and clean up, simply pass null patch(newVnode, null) 複製代碼

例子很簡單,就是snabbdom調用init方法,傳入一個包含模塊的數組,有classpropsstyleeventlisteners這幾個內置模塊,固然你也能夠添加本身擴展的模塊。最後返回一個patch。接着調用h函數來生成vnode。以後調用patch函數更新dom。patch接收兩個參數,第一個參數是舊的vnode對象,第二參數是新的vnode對象。patch能夠根據兩個對象的差別更新dom。數組

init 函數

init函數在源碼的src/snabbdom.ts文件。源碼以下

// 模塊實現鉤子函數的key(hook key)
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);
  
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  // 對模塊內定義的鉤子函數合併到一個對象中。{create:[fn, fn, ...], update: [...], ...}
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as Array<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: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue
  ) {
    // ...
  }

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

  function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void {
    // ...
  }

  function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
    // ...
  }

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

  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    // ...
  };
}
複製代碼

init傳入兩個參數,第一個是模塊數組,第二個參數是domApi,是可選的。

const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
複製代碼

能夠看到若是沒有傳入domApi這個參數,domApi默認是htmlDomApihtmlDomApi就是DOM原生的一些操做dom的api。在src/htmldomapi.ts文件。 init方法裏還定義了一系列的方法,這些方法是一些對VNodediff以及建立真實domhook相關的函數,這裏先不分析;直接看到init最後是返回了一個patch函數。

先來看看h函數

h 函數

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}, children: any, text: any, i: number;
  
  // c主要是格式化children屬性,c若是是數組則賦值給children
  // 若是是‘string’類型或‘number’類型,則賦值給text
  // 若是c含有sel屬性則轉成數組
  if (c !== undefined) {
    data = b;
    if (is.array(c)) { children = c; }
    else if (is.primitive(c)) { text = c; }
    else if (c && c.sel) { children = [c]; }
  // b主要是格式化data屬性,邏輯同上
  } else if (b !== undefined) {
    if (is.array(b)) { children = b; }
    else if (is.primitive(b)) { text = b; }
    else if (b && b.sel) { children = [b]; }
    else { data = b; }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      // 若是children裏面的項是文本或數值,則都轉成vnode對象
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  // 針對svg的處理
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel);
  }
  return vnode(sel, data, children, text, undefined);
}
複製代碼

h函數的使用方式有不少種,以上代碼就是對傳入的參數進行格式化,最終返回一個VNode對象

h函數裏用到vnode方法定義在src/vnode.ts文件裏。同時咱們也能夠看看snabbdom是如何定義一個Virtual dom對象的

export interface VNode {
  sel: string | undefined;  // 選擇器
  data: VNodeData | undefined;  // 描述dom的對象
  children: Array<VNode | string> | undefined;  // 子節點
  elm: Node | undefined; // 真實dom元素的引用
  text: string | undefined; // dom字體文本
  key: Key | undefined; // 用於diff時提高性能的key
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: Array<any>; // for thunks
  [key: string]: any; // for any other 3rd party module
}

// vnode方法返回VNode對象
export function vnode(sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode {
  let key = data === undefined ? undefined : data.key;
  return {sel, data, children, text, elm, key};
}
複製代碼

接下來就來看看在init定義的一些方法,先來看createElm函數

createElm 函數

// 這個函數是建立真實的dom元素
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    // 調用h函數時傳入的對象,即第二個參數。
    // 若是傳入init鉤子函數,則在dom建立前調用
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        i(vnode);
        data = vnode.data;
      }
    }
    let children = vnode.children, sel = vnode.sel;
    // 若是選擇器傳入的是‘!’,則建立註釋節點
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text as string);
    } 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(0, Math.min(hash, dot)) : sel;
      // 建立元素
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag);
      // 設置id、class屬性
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      // 調用模塊的create鉤子函數
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      // 是否有子節點,有則遞歸建立
      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)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        // 調用該節點傳入create鉤子函數
        if (i.create) i.create(emptyNode, vnode);
        // 若是有insert鉤子函數,則填充insertedVnodeQueue數組,避免再次遍歷
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    } else {
      // sel爲undefined,則建立文本節點
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    return vnode.elm;
  }
複製代碼

能夠看到createElm節點的邏輯也是比較簡單的。先調用了元素的init鉤子函數,接着判斷sel選擇器

  • 若是sel!,則建立註釋節點,賦值給vnode.elm(下面兩種狀況一樣會賦值給vnode.elm)
  • 若是sel不是undefined,即有值,這會對sel解析,取得標籤tag,若是獲取獲得id和class,則會調用setAttribute設置id和class。若是有子節點則遞歸調用。以後若是存在hook,則觸發create hook;用insertedVnodeQueue存儲insert hook,在patch時觸發。
  • 若是sel獲取不到,則建立文本節點。

最後返回vnode.elm

addVnodes 函數

function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        // 建立節點並插入到before節點前
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }
複製代碼

addVnodes函數就是用來插入節點的

emptyNodeAt 函數

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);
  }
複製代碼

emptyNodeAt函數就是把傳入elem對象轉成空的VNode對象,這個對象中只保留了Elementtag、id、class

createRmCb 函數

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

createRmCb函數做用是移除dom元素。在removeVnodes函數會調用。createRmCb函數裏有if (--listeners === 0)這個斷定條件,這個是針對模塊鉤子函數的斷定條件。只有當全部的remove hook調用完了,纔會移除dom。

invokeDestroyHook 函數

function invokeDestroyHook(vnode: VNode) {
    let i: any, j: number, data = vnode.data;
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (j = 0; j < vnode.children.length; ++j) {
          i = vnode.children[j];
          if (i != null && typeof i !== "string") {
            // 含有子節點,且不是字符串類型,遞歸調用
            invokeDestroyHook(i);
          }
        }
      }
    }
  }
複製代碼

顧名思義invokeDestroyHook這個函數就是在dom銷燬前觸發destroy hook的。

removeVnodes 函數

function removeVnodes(parentElm: Node,
                        vnodes: Array<VNode>,
                        startIdx: number,
                        endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          // 先觸發destroy hook
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm as Node, listeners);
          // 觸發模塊的remove hook
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
            i(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }
複製代碼

removeVnodes也很簡單,就是移除dom

看完上面那些函數如今就來看看Snabbdom的核心函數了

patch 函數

function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    // 定義insert隊列數組,用於存儲insert hook
    const insertedVnodeQueue: VNodeQueue = [];
    // 調用模塊(module )的 pre 鉤子函數 
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 若是傳入的是 Element,這個轉成空的VNode對象
    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    }
    
    // 相同的VNode對象(sel和key相同則認爲相同),則調用patchVnode
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);
      
      // 不相同,則建立新的dom
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        // 插入新的dom節點
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        // 移除舊的dom節點
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    // 調用insert hook
    // 從如下代碼能夠看出,以及init hook合併中,能夠看出模塊是不支持insert hook的
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    // 調用模塊的 post 鉤子函數
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  }
複製代碼

patch函數就是init最終返回的函數,也是能更新dom的函數,是Snabbdom所暴露的核心函數。patch裏面的邏輯仍是容易理解的

patchVnode 函數

patchVnodeSnabbdom最核心的函數,也是Virtual DOM的核心,負責VNodediff,並將差別更新到dom上。

patchVnode裏還調用了另外一個核心函數:updateChildren函數。這個函數也是作diff處理的,處理的是VNode複雜的部分,也是Virtual DOM比較複雜的部分。

updateChildren暫時先無論,先來看看patchVnode是怎麼實現diff

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    let i: any, hook: any;
    // 調用元素的prepatch鉤子函數
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
    }
    // elm指向vnode.elm和oldVnode.elm
    const elm = vnode.elm = (oldVnode.elm as Node);
    let oldCh = oldVnode.children;
    let ch = vnode.children;
    // 節點相同,結束patchVnode不作後續處理
    if (oldVnode === vnode) return;
    if (vnode.data !== undefined) {
      // 調用模塊的update hook
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      i = vnode.data.hook;
      // 調用元素的update hook
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    // 新節點不是文本節點
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新舊節點都存在children節點且不相等,調用updateChildren進行diff更新
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
      } else if (isDef(ch)) {
        // 新節點有children,舊節點沒有children
        // 則先移除舊的文本節點,再插入新的vnode做爲children節點
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        // 舊節點有children,新節點沒有,則直接移除舊節點的children
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      } else if (isDef(oldVnode.text)) {
        // 以上狀況都不知足,且舊節點有text,則置空
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新節點是文本,且文本內容不一樣。而且舊節點含有子節點,
      // 則刪除舊節點的子節點,同時更新text文本
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      api.setTextContent(elm, vnode.text as string);
    }
    // 調用元素的postpatch hook
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }
複製代碼

updateChildren 函數

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語法,斷定條件是新舊的startIndex都要小於endIndex,每次只處理一個節點
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 若果oldStartVnode該節點不存在則跳過繼續下一個節點,下面三個判斷同理
      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更新,繼續循環比較下一個節點
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 若是新舊節點的末尾節點相同則調用patchVnode更新,繼續循環比較上一個節點
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 參考內容來自:https://juejin.im/post/5b9200865188255c672e8cfd#heading-8
        // 舊開始節點等於新的結束節點,說明節點向右移動了
        // 至於爲何是向右移:oldStartVnode 和 newEndVnode 相同,固然是 vnode 右移了
        // 具體移動到哪,由於新節點處於末尾,因此添加到舊結束節點的後面,舊節點會隨着 updateChildren 的調用向左移
        // 注意這裏須要移動 dom,由於節點右移了,而爲何是插入 oldEndVnode 的後面呢?
        // 能夠分爲兩個狀況來理解:
        // 1. 當循環剛開始,下標都尚未移動,那移動到 oldEndVnode 的後面就至關因而最後面,是合理的
        // 2. 循環已經執行過一部分了,由於每次比較結束後,下標都會向中間靠攏,並且每次都會處理一個節點,
        // 這時下標左右兩邊已經處理完成,能夠把下標開始到結束區域當成是並未開始循環的一個總體,
        // 因此插入到 oldEndVnode 後面是合理的(在當前循環來講,也至關因而最後面,同 1)
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        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
        // 舊的結束節點等於新的開始節點,說明節點是向左移動了,邏輯同上
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      // 若果以上條件都不知足,則根據key來作判斷,
      // 那麼這個vnode有多是在中間的某個位置,
      // 也有節點多是新建立的節點
      } else {
        // 若是沒有oldKeyToIdx,則建立oldCh的key到index的map映射
        // 以便於經過key拿到對應的index下標,從而獲取到對應的舊節點
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 經過新vnode的key去拿在oldCh下對應的下標
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        // 下標不存在,則說明是vnode是全新的
        if (isUndef(idxInOld)) { // New element
          // 把新的dom插入到oldStartVnode前面
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx];
        } else {
          // 下標存在,取到oldCh對應的vnode
          elmToMove = oldCh[idxInOld];
          // 即便下標存在但sel不一樣,也建立新的節點並插入到oldStartVnode前面
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else {
            // 節點相同,調用patchVnode更新
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            // 該節點已經處理過了,把這個節點置空,當下次循環到這個節點時跳過
            oldCh[idxInOld] = undefined as any;
            // 把這個節點插入到oldStartVnode前面
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    // 當循環結束後,有可能存在還未處理的vnode。
    // 由於oldCh和和新的newCh數組長度不可能老是相同的;
    // 這裏有兩種狀況:
    // 1. 舊vnode處理完了,新vnode還有沒處理完的
    // 2. 新vnode的處理完了,舊vnode的還有沒處理完的
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      // 說明oldCh已經先處理完了,還有新的vnode沒有處理完,則插入剩下的vnode到dom
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      // oldCh有多餘的vnode,則刪除多餘的dom(vnode對應的dom)
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }
複製代碼

看完updateChildren能夠知道,對於新舊vnode的diff策略是:

第一步比較初始vnode是否相同,相同則patchVnode;第二步在在比較末尾vnode是否相同;第三步是初始末尾交叉比較;最後是經過key來處理

若是對於Vnode節點移動不是很理解的能夠看看這個兩篇博客的圖解:

最後

到這裏snabbdom的核心實現就分析完了。還有thunk函數和一些內置modules能夠自行閱讀。能夠發現,Virtual DOM 並無咱們想的那麼複雜。

推薦一篇講解diff的好文:難以想象的 react diff

相關文章
相關標籤/搜索