# snabbdom 笔记

# 虚拟 dom 库

# diff 算法

对比两个列表的元素,通过算法来进行最小变化的更改,提升 dom 渲染的性能

# 核心的概念

# 1.虚拟 dom vnode

// 通过一个对象来表示dom的内容

{
  tag: 'div',
  props: {
    id: 'app'
  },
  chidren: [
    {
      tag: 'p',
      text: 'hello world!!!'
    }
  ]
}

等同于

<div id="app"><p>hello world</p></div>

相关函数:

  1. vnode: 定义一个 vnode 结构,并返回
export interface VNode {
  // 元素名称,包含id和类选择器
  sel: string | undefined;
  // vnode数据
  data: VNodeData | undefined;
  // 子vnode
  children: Array<VNode | string> | undefined;
  // 真实dom
  elm: Node | undefined;
  // 文本内容
  text: string | undefined;
  // 标识
  key: Key | undefined;
}

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

  1. h: 调用 vnode 函数,创建一个 vnode
// 伪代码
export function h() {
  // 序列化一些参数,sel,data,children
  // 单独处理svg
  addNS(data, children, sel);
  // 返回
  return vnode(sel, data, children, text, undefined);
}

# 2. patch 更新

path 函数会传两个参数,oldVnode 和 newVnode,最终目标是要将真实的 old Dom 变成 new Dom.oldVnode 可以为真实 dom 或者 vnode,el 元素必定包含真实 dom,否则就没有更新的意义

相关函数:

  1. init: 初始化 patch 函数
// 通过module可以自定义模块的插拔,domapi可以适配不同的平台
export function init(modules, domApi) {
  // 传入vnode或者dom,来更新为新的vnode
  return function patch(oldVnode, vnode) {
    // 执行模块的钩子
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    // 将vnode转为dom
    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    }
    // 如果是同一个vnode,进行内部patch
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      // 否则直接插入新元素,删除旧元素
      insertBefore;
      removeVnodes;
    }

    // insertedVnodeQueue

    // 执行钩子
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();

    return vnode;
  };
}
  1. patchVnode: 对比两个 vnode 进行更新
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
  // 1.如果oldVnode === vnode,直接return
  // 2.如果传了data(意味着需要更新一些属性),执行各个模块的update函数,包含data.hooks内部的
  // 3.如果新vnode没有定义text,则判断children
  // 3.1.新旧vnode都有children , 并且不一样,则updateChildren(elm, oldCh, ch, insertedVnodeQueue);
  // 3.2.只有新vnode有children,如果旧vnode有text,先清空,同时添加新vnode的节点
  // 3.3.只有旧vnode有children,直接删除旧vnode
  // 3.4.只有旧vnode有text,则删除text
  // 4.如果新vnode定义了text,并且和原dom不一致
  // 4.1.如果旧vnode有children,则直接删除旧vnode,同时更新内容为新vnode的text
}
  1. updateChildren: 核心的 diff 算法更新 dom
function(parentElm,oldCh,newCh,insertedVnodeQueue){
  // 定义4个索引和4个vnode
  let oldStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newCh.length - 1;

  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];

  // 两个vnode列表进行对比,通过指针的变化来移动
  // 两个start index都超出end index的时候,循环终止
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 一开始会对比这4个vnode,old和new两两进行对比,最终将旧列表更新为新的列表
	// 当某个vnode被直接选中的时候,会有一个null的空位,这个时候直接变化指针即可
    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和新列表的最后一个vnode是同个vnode,就需要把旧的第一个vnode放到最后一个vnode的前面,使得和新的vnode列表一致
      // 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)) {
	  // 和上面正好相反,将oldEndVnode放到第一个oldStartVnode的后面一个即可
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
      api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
	  // 如果没有找个对应的相同元素,每次都拿新vnode列表的第一个vnode去旧vnode列表去找
      if (oldKeyToIdx === undefined) {
	  // 做一个map,{key:index}
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
	  // 新vnode列表的newStartVnode作为key,去旧vnode列表寻找
      idxInOld = oldKeyToIdx[newStartVnode.key as string];
	  // 如果没有找到,则是一个新元素,直接插入
      if (isUndef(idxInOld)) {
        // New element
        api.insertBefore(
          parentElm,
          createElm(newStartVnode, insertedVnodeQueue),
          oldStartVnode.elm!
        );
      } else {
	  // 如果存在index
        elmToMove = oldCh[idxInOld];
		// key相同,属性有变化,直接插入新的vnode
        if (elmToMove.sel !== newStartVnode.sel) {
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        } else {
		  // 不然就更新两个vnode,将找到的element放到第一个oldStartVnode的后面一个即可
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
          oldCh[idxInOld] = undefined as any;
          api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
        }
      }
	  // 更新指针
      newStartVnode = newCh[++newStartIdx];
    }
  }
}


# 3. modules

通过模块的形式注入相关的更新

  1. style
  2. class
  3. prop
  4. event ...

每个模块都有对应的钩子去执行逻辑

// 比如class
export const classModule: Module = { create: updateClass, update: updateClass };