Snabbdom源码细读

590 阅读16分钟

前言

随着react、vue等前端框架崛起,虚拟DOM也被越来越多的人悉知。虚拟DOM 一个听起来好高大上的词,貌似“深不可测”的样子。但当你去深入了解之后,会发现虚拟DOM并没有那么神秘,也不是那么难以理解。很多时候只是不知道从何处下手而已。

snabbdom就是虚拟DOM的一个简洁实现。置于为什么选择snabbdom,诚如官方文档所说snabbdom:

  • 主体代码只有200多行,简单易读且性能优越
  • 功能强大、扩展性强
  • 丰富的钩子函数

并且vue2.x中的Virtual DOM部分也是基于snabbdom实现的。学习snabbdom能让你更好的理解什么是虚拟dom之外,也能更有助于你去读懂vue源码。如果你没听过或没用过snabbdom,建议你先去看看官方文档

关于Virtual Dom

什么是Virtual Dom:Virtual dom是真实DOM的抽象,可以理解为一个纯js对象。这个对象只保存了dom必要的信息。Virtual dom能很好的描述一个dom结构。操作一个轻量的js对象要远比操作dom快的多,且比较前后两个对象的不同来更新dom可以避免重复操作未变更的dom,同时也能更好的检测数据变化来更新dom。

Virtual dom的实现都会有两个过程:

  • 根据描述dom的js对象创建出真正的dom树,再应用到文档上。
  • 数据变化时,比较旧js和新js对象,得到对象之间的差异,更新dom。(diff算法)

但是对于Virtual Dom不要一味的认为就是快,快也要分场合;要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

有时对dom操作的优化能达到比Virtual dom更好的性能。Virtual Dom更实际的意义是:

  1. 为函数式的 UI 编程方式打开了大门;
  2. 可以渲染到 DOM 以外的 渲染后端。

这里是借用了尤大的回答,具体可以看看以下两篇回答内容

网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?

RxJS/Cycle.js 与 React/Vue 相比更适用于什么样的应用场景?

源码分析

由于源码是使用TypeScript写的,不是很熟悉的先去看看TypeScript

阅读源码首先要找到切入点,也就是入口。snabbdom的主要方法也就几个,分别是inithpatchtovnode。顾名思义init就是我们要的切入点。结合例子来看能更好的读懂源码,我们先看看官方给的例子

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.cn/post/6844903671906435080#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