Snabbdom源码解析

Snabbdom源码解析

一.Snabbdom的核心

1.使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
2.init() 设置模块,创建 patch()
3.patch() 比较新旧两个 VNode
4.把变化的内容更新到真实 DOM 树上

二.src 目录结构

│ h.ts	               h() 函数,用来创建 VNode
│ hooks.ts             所有钩子函数的定义
│  htmldomapi.ts       对 DOM API 的包装
│ is.ts                判断数组和原始值的函数
│  jsx-global.d.ts     jsx 的类型声明文件
│ jsx.ts               处理 jsx 
│  snabbdom.bundle.ts  入口,已经注册了模块
│  snabbdom.ts         初始化,返回 init/h/thunk 
│ thunk.ts             优化处理,对复杂视图不可变值得优化
│ tovnode.ts           DOM 转换成 VNode
│  vnode.ts            虚拟节点定义
│
├─helpers
│      attachto.ts     定义了 vnode.ts 中 AttachData 的数据结构
│
└─modules              所有模块定义
        attributes.ts
        class.ts
        dataset.ts
        eventlisteners.ts
        hero.ts        example 中使用到的自定义钩子 
		    module.ts      定义了模块中用到的钩子函数 props.ts
        style.ts

三.Snabbdom源码

3.1 h函数

h()函数介绍

在使用 Vue 的时候见过 h() 函数

new Vue({
  router,
store,
  render: h => h(App)
}).$mount('#app')

h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本

Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode 函数重载

函数重载
  1. 参数个数类型不同的函数
  2. JavaScript 中没有重载的概念
  3. TypeScript 中有重载,不过重载的实现还是通过代码调整参数

重载的示意

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/h.ts
// 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;
  // 处理参数,实现重载的机制. 
  // 处理三个参数的情况:sel、data、children/text
  if (c !== undefined) {
    data = b;
    // 如果 b 是数组
    if (is.array(c)) { children = c; }
    // 如果 c 是字符串或者数字
    else if (is.primitive(c)) { text = c; }
    // 如果 c 是 VNode
    else if (c && c.sel) { children = [c]; }
  } 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) {
    // 处理 children 中的原始值(string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果 child 是 string/number,创建文本节点
      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
  return vnode(sel, data, children, text, undefined);
};
// 导出模块
export default h;

3.2 VNode

一个VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是Virtual DOM.

源码位置:src/vnode.ts
import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'

export type Key = string | number;

export interface VNode {
  // 选择器
  sel: string | undefined;
  // 节点数据:属性/样式/事件等
  data: VNodeData | undefined;
  // 子节点,和 text 只能互斥
  children: Array<VNode | string> | undefined;
  // 记录 vnode 对应的真实 DOM
  elm: Node | undefined;
  // 节点中的内容,和 children 只能互斥
  text: string | undefined;
  // 优化用
  key: Key | undefined;
}

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
}

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;
  //返回一个虚拟DOM对象
  return {sel, data, children, text, elm, key};
}

export default vnode;

3.3 Patch的整体过程

Patch的过程也就是VNode渲染成真实的Dom过程.

参数

patch(oldVnode,newVnode)

功能

打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点.

  1. 对比新旧 VNode 是否相同节点(节点的 key(唯一值) 和 sel(选择器) 相同)
  2. 如果不是相同节点,删除之前的内容,重新渲染
  3. 如果是相同节点
    再判断新的 VNode 是否有 text
    如果有并且和 oldVnode 的 text 不同,直接更新文本内容
  4. 如果新的 VNode 有 children
    判断子节点是否有变化,判断同层级子节点的过程使用的就是diff 算法
源码位置src/snabbdom.ts
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
  let i: number, elm: Node, parent: Node;
  // 保存新插入节点的队列,为了触发钩子函数
  const insertedVnodeQueue: VNodeQueue = [];
  // 执行模块的 pre 钩子函数
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
	// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
  if (!isVnode(oldVnode)) {
    // 把 DOM 元素转换成空的 VNode
    oldVnode = emptyNodeAt(oldVnode);
  }
  // 如果新旧节点是相同节点(key 和 sel 相同)
  if (sameVnode(oldVnode, vnode)) {
    // 找节点的差异并更新 DOM
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    // 如果新旧节点不同,vnode 创建对应的 DOM
    // 获取当前的 DOM 元素
    elm = oldVnode.elm as Node;
    parent = api.parentNode(elm);
		// 触发 init/create 钩子函数,创建 DOM
    createElm(vnode, insertedVnodeQueue);

    if (parent !== null) {
      // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
      api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
      // 移除老节点
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }
 	// 执行用户设置的 insert 钩子函数
  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]();
  // 返回 vnode
  return vnode;
};

3.4 init

参数

init(modules, domApi)

功能

先收集了所有模块中的钩子函数存储到 cbs 对象中,最后返回 patch() 函数(高阶函数) .

为什么要使用高阶函数?

1.因为 patch() 函数在外部会调用多次,每次调用依赖一些参数,比如: modules/domApi/cbs
2.通过高阶函数让init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而 不需要重新创建

源码位置:src/snabbdom.ts
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);
// 初始化 api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
// 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ...
]
for (i = 0; i < hooks.length; ++i) {
  // cbs['create'] = []
  cbs[hooks[i]] = [];
  for (j = 0; j < modules.length; ++j) {
    // const hook = modules[0]['create']
    const hook = modules[j][hooks[i]];
    if (hook !== undefined) {
      (cbs[hooks[i]] as Array<any>).push(hook);
    }
} }
  ......
  ......
  ......
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode
{} 
}

3.5 createElm

参数

createElm(vnode, insertedVnodeQueue)

功能

返回创建的 DOM 元素,创建 vnode 对应的 DOM 元素.

执行过程:
  • 首先触发用户设置的 init 钩子函数
  • 如果选择器是!,创建注释节点
  • 如果选择器为,创建文本节点
  • 如果选择器不为空
    • 解析选择器,设置标签的 id 和 class 属性
    • 执行模块create 钩子函数
    • 如果 vnode 有 children创建子 vnode 对应的 DOM,追加到 DOM 树
    • 如果 vnode 的 text 值是 string/number创建文本节点并追击到 DOM 树
    • 执行用户设置的 create 钩子函数
    • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中

在这里插入图片描述

源码位置:src/snabbdom.ts
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
      // 执行用户设置的 init 钩子函数
      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 = '';
      }
      // 如果选择器不为空
      // 解析选择器
      // Parse selector
      vnode.elm = api.createComment(vnode.text as string);
    } else if (sel !== undefined) {
      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);
      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);
      // 如果 vnode 中有子节点,创建子 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,创建文本节点并追加到 DOM 树
        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);
        // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    } else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    // 返回新创建的 DOM
    return vnode.elm;
  }

3.6 patchVnode

参数

patchVnode(oldVnode, vnode, insertedVnodeQueue)

功能

对比 oldVnodevnode差异,把差异渲染到 DOM.

执行过程:
  • 首先执行用户设置的 prepatch 钩子函数
  • 执行 create 钩子函数
  • 首先执行模块create 钩子函数
  • 然后执行用户设置的 create 钩子函数
  • 如果 vnode.text 未定义
    • 如果 oldVnode.children 和 vnode.children都有值
      • 调用 updateChildren()
      • 使用 diff 算法对比子节点,更新子节点
    • 如果 vnode.children 有值, oldVnode.children 无值
      • 清空 DOM 元素
      • 调用 addVnodes(),批量添加子节点
    • 如果 oldVnode.children 有值, vnode.children 无值
      • 调用 removeVnodes() ,批量移除子节点
      • 如果 oldVnode.text 有值
        - 清空 DOM 元素的内容
  • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
    • 如果老节点有子节点全部移除
    • 设置 DOM 元素的 textContentvnode.text
  • 最后执行用户设置的 postpatch 钩子函数

在这里插入图片描述

源码位置:src/snabbdom.ts
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:VNodeQueue) {
    const hook = vnode.data?.hook;
    // 首先执行用户设置的 prepatch 钩子函数 hook?.prepatch?.(oldVnode, vnode);
    const elm = vnode.elm = oldVnode.elm!; 
    let oldCh = oldVnode.children as VNode[]; 
    let ch = vnode.children as VNode[];
    // 如果新老 vnode 相同返回
    if (oldVnode === vnode) return;
    if (vnode.data !== undefined) {
        // 执行模块的 update 钩子函数
        for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,vnode);
        // 执行用户设置的 update 钩子函数 
        vnode.data.hook?.update?.(oldVnode, vnode);
    }
    // 如果 vnode.text 未定义
    if (isUndef(vnode.text)) {
        // 如果新老节点都有 children
        if (isDef(oldCh) && isDef(ch)) {
            // 使用 diff 算法对比子节点,更新子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch,insertedVnodeQueue);
        } else if (isDef(ch)) {
            // 如果新节点有 children,老节点没有 children
            // 如果老节点有text,清空dom 元素的内容
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            // 批量添加子节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        } else if (isDef(oldCh)) {
            // 如果老节点有children,新节点没有children
            // 批量移除子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        } else if (isDef(oldVnode.text)) { 
            // 如果老节点有 text,清空 DOM 元素 
            api.setTextContent(elm, '');
        }
    } else if (oldVnode.text !== vnode.text) {
            // 如果没有设置 vnode.text
            if (isDef(oldCh)) {
                // 如果老节点有 children,移除
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            }
            // 设置 DOM 元素的 textContent 为 vnode.text 
            api.setTextContent(elm, vnode.text!);
    }
    // 最后执行用户设置的 postpatch 钩子函数
      hook?.postpatch?.(oldVnode, vnode);
}

3.7 updateChildren

功能

diff 算法的核心是对比新旧节点的 children,更新 DOM

执行过程:

要对比两棵树的差异,可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,然后对比每一次的差异,这样的时间复杂度为 O(n^3),显然这样很低效.然而在DOM 操作时很少会把一个父节点移动/更新到某一个子节点,因此只需要找同级别子节点依次比较,然后依次再找下一级别的节点比较,这样算法的时间复杂度为 O(n) .所以diff算法的核心是对比新旧节点的childern,更新DOM.为了保证真实DOM中的顺序和新节点保持一致可以用while循环首尾节点进行对比,同时对新老节点数组的开始和结尾节点设置标记索引,循环的过程中向中间移动索引,这样既能实现排序也能减小时间复杂度.

在这里插入图片描述

Diff算法过程(对比同级节点)

  • 当新开始节点索引<=新结束索引当旧开始节点索引<=旧结束索引,while循环开始
    • 两两节点比较,有四种比较方式(必须按顺序):
      • 旧开始节点新开始节点比较,如果匹配上了(key 和 sel 相同),那么旧开始节点(真实dom)不动.
        (同时++oldStartIdx,++newStartIdx)开始下一次while循环.
      • 旧结束节点新结束节点比较,如果匹配上了(key 和 sel 相同),那么旧结束节点(真实dom)不动.
        (同时–oldEndIdx,–newEndIdx)开始下一次while循环.
      • 旧开始节点新结束节点比较,如果匹配上了(key 和 sel 相同),那么把旧开始节点(真实dom)移动到旧结束节点的后面.(同时++oldStartIdx,–newEndIdx)开始下一次while循环.
      • 旧结束节点新开始节点比较,如果匹配上了(key 和 sel 相同),那么把旧结束节点(真实dom)移动到旧开始节点的前面.
        (同时–oldEndIdx,++newStartIdx)开始下一次while循环.
    • 如果不是以上四种情况
      • 遍历新节点,使用新节点的 key在老节点数组中找相同节点
        • 如果匹配上了并且是相同节点,把该旧节点(elmToMove.elm)移到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
        • 如果匹配上了但不是相同节点,创建该新节点并插入到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
        • 如果没有匹配上,创建新节点并插入到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
  • 当新开始节点索引>新结束索引或者当旧开始节点索引>旧结束索引,while循环结束
    • 如果老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx).说明新节点有剩余把newStartIdx到newEndIdx中间剩余节点批量插入旧结束节点的后面.循环结束
    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余把oldStartIdx到oldEndIdx之间剩余节点批量删除.循环结束
      在这里插入图片描述
分析如下节点的更新步骤

在这里插入图片描述

注: 这张图是我从网上copy下来的,用作diff算法的分析.如下:

  1. oldStartIdx = 0 ; newStartIdx = 0 ; oldEndIdx = 4, newEndIdx = 2;
  2. oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx条件成立进入while循环
  3. 先进行4种新旧节点的顺序比较:b和a,e和e.e节点匹配成功,e节点位置保持不动,索引向中间移动
    (oldEndIdx = 3, newEndIdx = 1) 该步结束.
  4. while循环条件成立,继续判断比较: b和a,f和b,b和b. b节点匹配成功,b移动到旧结束节点f的后面,索引向中间移动
    (oldStartIdx = 1, newEndIdx = 0) 该步结束.
  5. while循环条件成立,继续判断比较: a和a. a节点匹配成功,a节点位置保持不动,索引向中间移动
    (oldStartIdx = 2, newStartIdx = 1) 该步结束.
  6. newStartIdx>newEndIdx,while循环结束,新节点数组先遍历完成,说明老节点有剩余.
  7. 目前旧节点排序为a b d f e
  8. 删除oldStartIdx = 2 到 oldEndIdx = 3之间的旧节点,也就是d和f.
  9. 如图在移动b的时候位置是错误的.
如下是DIff算法核心源码:src/snabbdom.ts
function updateChildren(parentElm: Node,oldCh: VNode[],newCh: 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: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    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)) {
        // 1. 比较老开始节点和新的开始节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); 
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 2. 比较老结束节点和新的结束节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); 
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        // Vnode moved right
        // 3. 比较老开始节点和新的结束节点
        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
        // 4. 比较老结束节点和新的开始节点
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); 
        api.insertBefore(parentElm, oldEndVnode.elm!,oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        // 开始节点和结束节点都不相同
        // 使用 newStartNode 的 key 再老节点数组中找相同节点 
        // 先设置记录 key 和 index 的对象
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,oldEndIdx);
        }
        // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引 
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        // 如果是新的vnode
        if (isUndef(idxInOld)) {
          // New element
          // 如果没找到,newStartNode 是新节点
          // 创建元素插入 DOM 树
          api.insertBefore(parentElm, createElm(newStartVnode,
          insertedVnodeQueue), oldStartVnode.elm!);
          // 重新给 newStartVnode 赋值,指向下一个新节点 
          newStartVnode = newCh[++newStartIdx];
        } else {
          // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历 
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) {
            // 如果新旧节点的选择器不同
            // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中 
            api.insertBefore(parentElm, createElm(newStartVnode,
            insertedVnodeQueue), oldStartVnode.elm!);
          } else {
            // 如果相同,patchVnode()
            // 把 elmToMove 对应的 DOM 元素,移动到左边 
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); 
            oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, elmToMove.elm!,
            oldStartVnode.elm!);
          }
          // 重新给 newStartVnode 赋值,指向下一个新节点
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
            // 如果老节点数组先遍历完成,说明有新的节点剩余 
            // 把剩余的新节点都插入到右边
            before = newCh[newEndIdx+1] == null ? null :newCh[newEndIdx+1].elm;
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,insertedVnodeQueue);
      } else {
        // 如果新节点数组先遍历完成,说明老节点有剩余
        // 批量删除老节点
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
}
 

3.8 调试 updateChildren

不带key的情况
<ul> 
  <li>首页</li>
  <li>微博</li>
  <li>视频</li>
</ul>

<ul> 
  <li>首页</li>
  <li>视频</li>
  <li>微博</li>
</ul>
import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
  h('li', '首页'),
  h('li', '视频'),
  h('li', '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
  h('li', '首页'),
  h('li', '微博'),
  h('li', '视频')
])
patch(oldVnode, vnode)

经过调试可以发现,判断老旧对应的三个LI节点都为sameNode,而在第二个和第三个LI的时候都操作了DOM(对node的文本进行了赋值).

带key的情况
<ul>
	<li key="a">首页</li> 
  <li key="b">微博</li> 
  <li key="c">视频</li>
</ul>

<ul>
	<li key="a">首页</li> 
  <li key="c">视频</li> 
  <li key="b">微博</li>
</ul>
import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
  h('li', { key: 'a' }, '首页'),
  h('li', { key: 'b' }, '视频'),
  h('li', { key: 'c' }, '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
  h('li', { key: 'a' }, '首页'),
  h('li', { key: 'c' }, '微博'),
  h('li', { key: 'b' }, '视频'),
])

patch(oldVnode, vnode)

经过调试可以发现,带key属性能更加精确的判断节点是否是sameNode.

总结

通过以上调试 updateChildren,我们发现不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只 需要更新`一次 DOM 操作(第二个节点与第三个节点换位了),所以带 key 的情况可以减少 DOM 的操作,如果 li 中的子项 比较多,更能体现出带 key 的优势。

3.9 Modules 源码

patch() -> patchVnode() -> updateChildren(). Snabbdom 为了保证核心库的精简,把处理元素的属性/事件/样式等工作,放置到模块中,模块可以按照需要引入, 模块的使用可以查看官方文档,模块实现的核心是基于 Hooks.

Hooks

预定义的钩子函数的名称

源码位置:src/hooks.ts
export interface Hooks {
  // patch 函数开始执行的时候触发
  pre?: PreHook;
  // createElm 函数开始之前的时候触发
  // 在把 VNode 转换成真实 DOM 之前触发
  init?: InitHook;
  // createElm 函数末尾调用
  // 创建完真实 DOM 后触发
  create?: CreateHook;
  // patch 函数末尾执行
  // 真实 DOM 添加到 DOM 树中触发
  insert?: InsertHook;
  // patchVnode 函数开头调用
  // 开始对比两个 VNode 的差异之前触发 
  prepatch?: PrePatchHook;
  // patchVnode 函数开头调用
  // 两个 VNode 对比过程中触发,比 prepatch 稍晚 
  update?: UpdateHook;
  // patchVnode 的最末尾调用
  // 两个 VNode 对比结束执行
  postpatch?: PostPatchHook;
  // removeVnodes -> invokeDestroyHook 中调用 
  // 在删除元素之前触发,子节点的 destroy 也被触发 
  destroy?: DestroyHook;
  // removeVnodes 中调用 
  // 元素被删除的时候触发 
  remove?: RemoveHook; 
  // patch 函数的最后调用 
  // patch 全部执行完毕触发 
  post?: PostHook;
}
Modules 模块文件

Snabbdom 提供的所有模块在:src/modules 文件夹下

│ attributes.ts	       使用 setAttribute/removeAttribute 操作属性,能够处理 boolean 类型的属性
│ class.ts             切换类样式
│ dataset.ts           操作元素的 data-* 属性
│ eventlisteners.ts		 注册和移除事件
│ module.ts            定义模块遵守的钩子函数
│ props.ts					   入口,已经注册了模块
│ snabbdom.ts          和 attributes.ts 类似,但是是使用 elm[attrName] = value的方式操作属性
│ style.ts             操作行内样式,可以使动画更平滑
│ hero.ts              自定义的模块,examples/hero 示例中使用
attributes.ts
模块到出成员
export const attributesModule = {
  create: updateAttrs,
  update: updateAttrs
} as Module;
export default attributesModule;
updateAttrs 函数功能
  • 更新节点属性
  • 如果节点属性值是 true 设置空置
  • 如果节点属性值是 false 移除属性
updateAttrs 实现
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
  var key: string, elm: Element = vnode.elm as Element,
    oldAttrs = (oldVnode.data as VNodeData).attrs,
    attrs = (vnode.data as VNodeData).attrs; // 新老节点没有 attrs 属性,返回
    if (!oldAttrs && !attrs) return;
    // 新老节点的 attrs 属性相同,返回
    if (oldAttrs === attrs) return;
    oldAttrs = oldAttrs || {};
    attrs = attrs || {};
    // update modified attributes, add new attributes // 遍历新节点的属性
    for (key in attrs) {
      const cur = attrs[key]; const old = oldAttrs[key]; // 如果新老节点的属性值不同
      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);
          } else if (key.charCodeAt(3) === colonChar) {
            // ascii 120 -> :
            // Assume xml namespace
            elm.setAttributeNS(xmlNS, key, cur);
          } else if (key.charCodeAt(5) === colonChar) {
            // Assume xlink namespace
            // <svg xmlns:xlink="http://www.w3.org/1999/xlink">
            elm.setAttributeNS(xlinkNS, key, cur);
          } else {
            elm.setAttribute(key, cur);
          }
        }
      } 
    }
      // 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
      // 如果老节点的属性在新节点中不存在,移除 
    for (key in oldAttrs) {
      if (!(key in attrs)) {
        elm.removeAttribute(key);
      }
    }

    export const attributesModule = {
        create: updateAttrs,
        update: updateAttrs
    } as Module;
    
    export default attributesModule;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值