snabbdom 虚拟DOM源码研究

重要通知

snabbdom非常精妙地实现了Virtual DOM,而Vue直接照搬了snabbdom的核心代码,因此精细化研究snabbdom的价值非常重大。

snabbdom基本概况

snabbdom是一个精简化、模块化、功能强大、性能卓越的虚拟 DOM 库。

<section id="container"></section>
import { 
  Fragment,
  array,
  attachTo,
  attributesModule,
  classModule,
  datasetModule,
  eventListenersModule,
  fragment,
  h, 
  htmlDomApi,
  init,
  jsx,
  primitive,
  propsModule,
  styleModule, 
  thunk, 
  toVNode, 
  vnode, 
} from "snabbdom";

const RenderDOM = init([
  // 通过传入模块初始化 patch 函数
  classModule, // 开启 classes 功能
  propsModule, // 支持传入 props
  styleModule, // 支持内联样式同时支持动画
  eventListenersModule, // 添加事件监听
]);

const container = document.getElementById("container");

const vnode = h("div#container.container", {}, 
  [
    h('h1', {
      style: { 'text-align': 'center', color: '#f00' }
    }, 'snabbdom示例'),

    h('p', {
      style: { 'text-align': 'center' }
    }, '段落'),

    "内容描述",

    h('br'),

    h("a", { 
      props: { 
        href: "/foo" 
      } 
    }, "超链接"),

    h('br'),

    h('button', {
      style: {},
      on: {
        click: function() { console.log('提交'); }
      }
    }, '提交')
  ]
);

RenderDOM(container, vnode);

snabbdom生态体系

snabbdom-to-html

snabbdom-to-html提供的服务器端 HTML 输出。

snabbdom-helpers

使用snabbdom-helpers创建紧凑的虚拟 DOM。

snabby

使用snabby的模板字符串支持。

snabbdom-looks-like

带有snabbdom-looks-like 的虚拟 DOM 断言。

snabbdom源码结构分析

VNode实现机制

VNode数据结构

function vnode(
  sel, // <String>,通过对 h() 传入一个 CSS 选择器生成
  data, // <Object>,虚拟节点用于添加 模块 信息以便在创建时访问或操作 DOM 元素、添加样式、操作 CSS classes、attributes 等
  children, // <Array>,通过 h() 传入的第三个参数(可选)生成
  text, // <string>,当仅使用文本作为子节点并通过 document.createTextNode() 创建虚拟节点时,生成 .text。
  elm, // <Element>,指向由 snabbdom 创建的真实 DOM 节点
  key, // <string | number>,标识
) {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

h(sel,b,c)创建VNode

function h(
  sel, // [比选] 字符串类型元素标签 | 选择器对象,例如div、h1、button、a等
  b, // [可选] 数据对象
  c // [可选] 子节点数组或字符串
) {
  let data = {};
  let children;
  let text;
  let i;

  if (c !== undefined) {
    if (b !== null) {
      data = b;
    }
    if (array(c)) {
      children = c;
    }
    else if (primitive(c)) {
      text = c.toString();
    }
    else if (c && c.sel) {
      children = [c];
    }
  }
  else if (b !== undefined && b !== null) {
    if (array(b)) {
      children = b;
    }
    else if (primitive(b)) {
      text = b.toString();
    }
    else if (b && b.sel) {
      children = [b];
    }
    else {
      data = b;
    }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (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] === "#")) {
    addNS(data, children, sel);
  }

  // 生成vnode结构
  return vnode(sel, data, children, text, undefined);
}

DOM节点转换为VNode

function toVNode(
  node, 
  domApi
) {
	const api = domApi !== undefined ? domApi : htmlDomApi;
	let text;
	if (api.isElement(node)) {
    const id = node.id ? "#" + node.id : "";
    const cn = node.getAttribute("class");
    const c = cn ? "." + cn.split(" ").join(".") : "";
    const sel = api.tagName(node).toLowerCase() + id + c;
    const attrs = {};
    const dataset = {};
    const data = {};
    const children = [];
    let name;
    let i, n;
    const elmAttrs = node.attributes;
    const elmChildren = node.childNodes;
    for (i = 0, n = elmAttrs.length; i < n; i++) {
      name = elmAttrs[i].nodeName;
      if (name[0] === "d" &&
        name[1] === "a" &&
        name[2] === "t" &&
        name[3] === "a" &&
        name[4] === "-") {
        dataset[name.slice(5)] = elmAttrs[i].nodeValue || "";
      }
      else if (name !== "id" && name !== "class") {
        attrs[name] = elmAttrs[i].nodeValue;
      }
    }
    for (i = 0, n = elmChildren.length; i < n; i++) {
      children.push(toVNode(elmChildren[i], domApi));
    }
    if (Object.keys(attrs).length > 0)
      data.attrs = attrs;
    if (Object.keys(dataset).length > 0)
      data.dataset = dataset;
    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, undefined, node);
	}
	else if (api.isText(node)) {
    text = api.getTextContent(node);
    return vnode(undefined, undefined, undefined, text, node);
	}
	else if (api.isComment(node)) {
    text = api.getTextContent(node);
    return vnode("!", {}, [], text, node);
	}
	else {
    return vnode("", {}, [], undefined, node);
	}
}

sameVnode(oldVnode, vnode)

判断是否为相同vnode的方法,根据key、data、sel三个数据比较。

function sameVnode(vnode1, vnode2) {
  var _a, _b;
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is);
  const isSameSel = vnode1.sel === vnode2.sel;
  const isSameTextOrFragment = !vnode1.sel && vnode1.sel === vnode2.sel
    ? typeof vnode1.text === typeof vnode2.text
    : true;
  return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
}

snabbdom初始化init函数

初始化数据

const cbs = {
  create: [],
  update: [],
  remove: [],
  destroy: [],
  pre: [],
  post: [],
};
const api = domApi !== undefined ? domApi : htmlDomApi;
for (const hook of hooks) {
  for (const module of modules) {
    const currentHook = module[hook];
    if (currentHook !== undefined) {
      cbs[hook].push(currentHook);
    }
  }
}

emptyNodeAt()

emptyNodeAt(elm):

createElm()

createElm(vnode, insertedVnodeQueue):

addVnodes()

addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue):

invokeDestroyHook()

invokeDestroyHook(vnode):

removeVnodes()

removeVnodes(parentElm, vnodes, startIdx, endIdx):

updateChildren()

updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue):

patchVnode()

patchVnode(oldVnode, vnode, insertedVnodeQueue):

patch()

patch(oldVnode, vnode):

patch实现机制

源码分析

function patch(oldVnode, vnode) {
  let i, elm, parent;
  const insertedVnodeQueue = [];
  for (i = 0; i < cbs.pre.length; ++i)
    // 执行pre钩子函数,尤其是styleModule
    cbs.pre[i]();

  // 如果oldVnode是元素,则转换为vnode对象
  if (isElement(api, oldVnode)) {
    oldVnode = emptyNodeAt(oldVnode);
  }

  // 文档片段对象
  else if (isDocumentFragment(api, oldVnode)) {
    oldVnode = emptyDocumentFragmentAt(oldVnode);
  }

  // 是否为相同vnode
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  }
  else {
    elm = oldVnode.elm;
    // 获取旧节点父级元素,用于挂载节点
    parent = api.parentNode(elm);
    createElm(vnode, insertedVnodeQueue);
    if (parent !== null) {
      // 挂载新节点
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
      // 移除挂载的旧节点
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
  }
  for (i = 0; i < cbs.post.length; ++i)
    cbs.post[i]();

  return vnode;
}

diff实现机制

diff算法,即通过对比同一层级节点,如果不相同则直接替换,如果相同则继续对比其子节点,如此往复,则实现新节点与旧节点的diff算法。

触发时机

function patch(oldVnode, vnode) {
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  }
}

function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
  const oldCh = oldVnode.children;
  const ch = vnode.children;

  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue);
    }
  }
}

源码分析

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
  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;
  let idxInOld;
  let elmToMove;
  let before;

  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];
      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;
          api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  if (newStartIdx <= newEndIdx) {
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  }
  if (oldStartIdx <= oldEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}


function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
    var _a, _b, _c, _d, _e, _f, _g, _h;
    const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook;
    (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode);
    const elm = (vnode.elm = oldVnode.elm);
    if (oldVnode === vnode)
        return;
    if (vnode.data !== undefined ||
        (isDef(vnode.text) && vnode.text !== oldVnode.text)) {
        (_c = vnode.data) !== null && _c !== void 0 ? _c : (vnode.data = {});
        (_d = oldVnode.data) !== null && _d !== void 0 ? _d : (oldVnode.data = {});
        for (let i = 0; i < cbs.update.length; ++i)
            cbs.update[i](oldVnode, vnode);
        (_g = (_f = (_e = vnode.data) === null || _e === void 0 ? void 0 : _e.hook) === null || _f === void 0 ? void 0 : _f.update) === null || _g === void 0 ? void 0 : _g.call(_f, oldVnode, vnode);
    }

    // 旧节点子集
    const oldCh = oldVnode.children;
    // 新节点子集
    const ch = vnode.children;
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch)
                updateChildren(elm, oldCh, ch, insertedVnodeQueue);
        }
        else if (isDef(ch)) {
            if (isDef(oldVnode.text))
                api.setTextContent(elm, "");
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        }
        else if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        }
        else if (isDef(oldVnode.text)) {
            api.setTextContent(elm, "");
        }
    }
    else if (oldVnode.text !== vnode.text) {
        if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        }
        api.setTextContent(elm, vnode.text);
    }
    (_h = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _h === void 0 ? void 0 : _h.call(hook, oldVnode, vnode);
}

Module模块

attributesModule

attributes模块与 props 相同,但是是使用 attr 替代 prop。

  • 代码示例
import { h } from 'snabbdom';

h('a', {
  attrs: {
    href: '/home'
  }
}, '');
  • 源码分析
const attributesModule = {
  create: updateAttrs,
  update: updateAttrs,
};

classModule

class模块提供了一种简单的方式来动态配置元素的 class 属性,这个模块值为一个对象形式的 class 数据,对象中类名需要映射为布尔值,以此来表示该类名是否应该出现在节点上。

  • 代码示例
import { h } from 'snabbdom';

h('div', {
  class: { active: true, container: true }
}, '');
  • 源码分析
const classModule = { 
  create: updateClass, 
  update: updateClass 
};

datasetModule

dataset模块允许在 DOM 元素上设置自定义 data 属性,然后通过 HTMLElement.dataset 来访问这些属性。

  • 代码示例
import { h } from 'snabbdom';

h('div', {
  dataset: {
    id: Date.now()
  }
}, '');
  • 源码分析
const datasetModule = {
  create: updateDataset,
  update: updateDataset,
};

eventListenersModule

event模块,即支持事件绑定功能。

  • 代码示例
import { h } from 'snabbdom';

h('button', {
  on: {
    click: function() {}
  }
}, '');
  • 源码分析
const eventListenersModule = {
  create: updateEventListeners,
  update: updateEventListeners,
  destroy: updateEventListeners,
};

propsModule

props模块允许你设置 DOM 元素的属性。

  • 代码示例
import { h } from 'snabbdom';

h('a', {
  props: {
    href: '/home'
  }
}, '');
  • 源码分析
const propsModule = { 
  create: updateProps, 
  update: updateProps 
};

styleModule

style模块用于让动画更加平滑,它的核心是允许你再元素上设置 CSS 属性。

  • 代码示例
import { h } from 'snabbdom';

h('div', {
  style: {
    width: '100px'
  }
}, '');
  • 源码分析
const styleModule = {
  pre: forceReflow,
  create: updateStyle,
  update: updateStyle,
  destroy: applyDestroyStyle,
  remove: applyRemoveStyle,
};

钩子hooks

  • 源码分析
const hooks = [
  "create",
  "update",
  "remove",
  "destroy",
  "pre",
  "post",
];

snabbdom应用案例

Cycle.js

  • 整体架构
  • snabbdom设计

Vue.js

  • 整体架构
  • snabbdom设计

scheme-todomvc

  • 整体架构
  • snabbdom设计

kaiju

  • 整体架构
  • snabbdom设计

Tweed

  • 整体架构
  • snabbdom设计

Cyclow

  • 整体架构
  • snabbdom设计

Tung

  • 整体架构
  • snabbdom设计

sprotty

  • 整体架构
  • snabbdom设计

Mark Text

  • 整体架构
  • snabbdom设计

puddles

  • 整体架构
  • snabbdom设计

Backbone.VDOMView

  • 整体架构
  • snabbdom设计

Rosmaro Snabbdom starter

  • 整体架构
  • snabbdom设计

Pureact

  • 整体架构
  • snabbdom设计

Snabberb

  • 整体架构
  • snabbdom设计

WebCell

  • 整体架构
  • snabbdom设计
Last Updated:
Contributors: 709992523