概述

  • 了解什么是虚拟 DOM,以及虚拟DOM的作用
  • Snabbdom 的基本使用
  • Snabbdom的源码解析

什么是虚拟 DOM

  • Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫Virtual DOM
  • 真实的DOM成员
let element = document.querySelector('#app') 
let s = ''
for (var key in element) {
s += key + ',' }
console.log(s)
// 打印结果 align,title,...
...复制代码

可以发现一个DOM对象它的成员非常多,所以创建一个DOM对象的成本是非常高的。

  • 可以使用Virtual DOM来描述真实DOM,示例
{
  sel: "div",
  data: {},
  children: undefined,
  text: "Hello Virtual DOM",
  elm: undefined,
  key: undefined
}复制代码

可见创建虚拟DOM的开销较小。

为什么使用虚拟DOM

  • 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升
  • 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题,数据变视图变,视图变数据变。
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(数据变化后无法获取上一次的状态,只好删除重建),于是VirtualDOM 出现了
  • Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需创建一个虚拟树来描述DOM,Virtual DOM内部将弄清楚如何有效(diff)的更新DOM
  • 参考github上的 virtual dom的描述
    • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态的差异更新真实的 DOM

虚拟DOM的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

Virtual DOM 的实现原理_Virtual DOM

Virtual DOM库

  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM就是改造的Snabdom
    • 大约 200 SLOC (single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom
    • 最早的虚拟DOM开源库

Snabbdom 基本使用

创建项目

  • 打包工具为了方便使用 parcel
  • 创建项目,并安装 parcel
1 # 创建项目目录
2 md snabbdom-demo
3 # 进入项目目录
4 cd snabbdom-demo
5 # 创建 package.json
6 yarn init -y
7 # 本地安装 parcel
8 yarn add parcel-bundler复制代码
  • 配置 package.json 的 script
"scripts": {
  "dev": "parcel index.html --open",
  "build": "parcel build index.html"
}复制代码
  • 创建目录结构
| index.html
| package.json
└─src
    01-basicusage.js复制代码

导入Snabbdom

Snabbdom 文档

  • 看文档的意义
    • 学习任何一个库都要先看文档
    • 通过文档了解库的作用
    • 看文档中提供的示例,自己快速实现一个demo
    • 通过文档查看API的使用
  • 文档地址

github.com/snabbdom/sn…

中文翻译

安装 Snabbdom

yarn add snabbdom复制代码

导入 Snabbdom

import { init, h, thunk } from 'snabbdom'复制代码
  • init()是一个高阶函数,返回patch()
  • h()返回虚拟节点 VNode,这个函数我们在使用Vue.js的时候见过
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')复制代码
  • thunk()是一种优化策略,可以在处理不可变数据时使用
  • 注意:导入时不能使用 import anabbdom from 'snabbdom'
    • 原因: node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用 export default导出默认输出

Virtual DOM 的实现原理_Virtual DOM_02

代码演示

  1. hello world
import { h, init } from 'snabbdom'

// 1. hello world
// 参数:数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
// 返回值:VNode
let vnode = h('div#container.cls', { 
  hook: {
    init (vnode) {
      console.log(vnode.elm)
    },
    create (emptyVnode, vnode) {
      console.log(vnode.elm)
    }
  }
}, 'Hello World')

let app = document.querySelector('#app')
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNde
let oldVnode = patch(app, vnode)

// 假设的时刻
vnode = h('div', 'Hello Snabbdom')

patch(oldVnode, vnode)复制代码
  1. div中放置子元素 h1,p
// 2. div中放置子元素 h1,p
import { h, init } from 'snabbdom'

let patch = init([])

let vnode = h('div#container', [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是一个p标签')
])

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)

setTimeout(() => {
  vnode = h('div#container', [
    h('h1', 'Hello World'),
    h('p', 'Hello P')
  ])
  patch(oldVnode, vnode)

  // 清空页面元素 -- 错误
  // patch(oldVnode, null)
  // h('!')创建注释节点
  patch(oldVnode, h('!'))
}, 2000);复制代码

模块

Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块

常用模块

  • 官方提供了6个模块
    • 设置行内样式,支持动画
    • delayed/remove/destroy
    • 注册和移除事件
    • 设置 data-* 的自定义属性
    • 切换类样式
    • 注意:给元素设置类样式是通过 sel 选择器
    • 和attribute模块相似,设置DOM元素的属性 element[attr] = value
    • 不处理布尔类型的属性
    • 设置DOM元素的属性,使用 setAttribute()
    • 处理布尔类型的属性
    • attributes
    • props
    • class
    • dataset
    • eventlisteners
    • style

模块使用

  • 模块使用步骤
    • 导入需要的模块
    • init() 中注册模块
    • 使用 h() 函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
  • 代码演示
import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
  style,
  eventlisteners
])
// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
  style: {
    backgroundColor: 'red'
  },
  on: {
    click: eventHandler
  }
}, [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是p标签')
])

function eventHandler () {
  console.log('点击我了')
}

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)


vnode = h('div', 'hello')
patch(oldVnode, vnode)复制代码

Snabbdom 源码解析

概述

如何学习源码

  • 先宏观了解(核心执行过程)
  • 带着目标看源码
  • 看源码的过程要不求甚解(围绕主线)
  • 调试
  • 参考资料

Snabbdom 的核心

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

Snabbdom 源码

│ 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复制代码

h 函数

  • h()函数介绍
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app') 
    复制代码
    • h()函数最早见于 hypescript,使用 JavaScript 创建超文本
    • Snabbdom中的h()函数不是用来创建超文本,而是创建VNode
    • 在使用Vue的时候见过h()函数
  • 函数重载
    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) 
    复制代码
    // h 函数的重载
    export function h(sel: string): VNode;
    export function h(sel: string, data: VNodeData | null): VNode; 
    export function h(sel: string, children: VNodeChildren): VNode; 
    export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode;
    export function h(sel: any, b?: any, c?: any): VNode {
      var data: VNodeData = {}, children: any, text: any, i: number; // 处理参数,   实现重载的机制
      if (c !== undefined) {
        // 处理三个参数的情况 
        // sel、data、children/text
        if (b !== null) { data = 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 && b !== null) { 
        // 处理两个参数的情况
        // 如果 b 是数组
        if (is.array(b)) { children = b; }
        // 如果 b 是字符串或者数字
        else if (is.primitive(b)) { text = b; } // 如果 b 是 VNode
        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;复制代码
    核心:调用vnode创建虚拟节点并返回
    • 源码位置:src/h.ts
    • 参数个数或类型不同的函数
    • JavaScript中没有重载的概念
    • TypeScript中有重载,不过重载的实现还是通过代码调整参数
    • 概念
    • 重载的示例

vnode 函数

  • 一个VNode就是一个虚拟节点用来描述一个DOM元素,如果这个VNode有childen就是Virtual DOM
  • 源码位置:src/vnode.ts
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 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};
}
export default vnode;复制代码

VNode 渲染真实DOM

snabbdom

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

patch整体执行过程

  • 对比新旧VNode是否是相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容
  • 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
  • diff过程只进行同层级比较

Virtual DOM 的实现原理_Virtual DOM_03

init()

  • 功能:init(modules, domApi),返回patch()函数(高阶函数)
  • 为什么使用高阶函数
    • 因为patch()函数在外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
    • 通过高阶函数让init()内部形成闭包,返回的patch()可以访问到modules/domApi/cbs,而不需要重新创建
  • init()在返回patch()之前,首先收集了所有模块中的钩子函数存储到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
{}
}复制代码

patch()

  • 功能:
    • 传入新旧VNode,对比差异,把差异渲染到DOM
    • 返回新的VNode,作为下一次patch()的oldVnode
  • 执行过程:
  1. 首先执行模块中的钩子函数 pre
  2. 如果oldVnode和vnode相同(key和sel相同)
  • 调用patchVnode(),找节点的差异并更新DOM
如果 oldVnode 是DOM元素
  • 把DOM元素转换成oldVnode
  • 调用createElm()把vnode转换成真实DOM,记录到vnode.elm
  • 把刚创建的DOM元素插入到parent中
  • 移除老节点
  • 触发用户设置的create钩子函数
  • 源码位置: 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!;
    parent = api.parentNode(elm);
    // 触发 init/create 钩子函数,创建 DOM 
    createElm(vnode, insertedVnodeQueue);
    if (parent !== null) {
      // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中 
      api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); 
      // 移除老节点
      removeVnodes(parent, [oldVnode], 0, 0);
    } 
  }
  // 执行用户设置的 insert 钩子函数
  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); 
  }
  // 执行模块的 post 钩子函数
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); 
  // 返回 vnode
  return vnode;
};复制代码

createElm

  • 功能
    • createElm(vnode, insertedVnodeQueue),返回创建的DOM元素
    • 创建vnode对应的DOM元素
  • 执行过程
    • 解析选择器,设置标签的id和class属性
    • 执行模块的create钩子函数
    • 如果vnode有children,创建children的vnode对应的DOM,追加到DOM树
    • 如果vnode的text值是string/number,创建文本节点并追加到DOM树
    • 执行用户设置的create钩子函数
    • 如果有用户设置的insert函数,把vnode添加到队列中
    • 首先触发用户设置的init钩子函数
    • 如果选择器是!,创建注释节点
    • 如果选择器为空,创建文本节点
    • 如果选择器不为空
  • 源码位置: src/snabbdom.ts
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  let i: any, data = vnode.data; 
  if (data !== undefined) {
    // 执行用户设置的 init 钩子函数 
    const init = data.hook?.init; 
    if (isDef(init)) {
      init(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!);
  } 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.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));
    }
    const hook = vnode.data!.hook; 
    if (isDef(hook)) {
      // 执行用户传入的钩子 
      create hook.create?.(emptyNode, vnode); 
      if (hook.insert) {
        // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
        insertedVnodeQueue.push(vnode); 
      }
    }
  } else {
    // 如果选择器为空,创建文本节点
    vnode.elm = api.createTextNode(vnode.text!);
  }
  // 返回新创建的 DOM
  return vnode.elm; 
 }复制代码
  • 总结

Virtual DOM 的实现原理_Virtual DOM_04

addVnodes 和 removeVnodes

patchVnode

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比oldVnode和vnode的差异,把差异渲染到DOM
  • 执行过程:
    • 如果老节点有子节点,全部移除
    • 设置DOM元素的textContent为vnode.text
    • 如果oldVnode.children和vnode.children都有值
    • 如果vnode.children有值,oldVnode.children无值
    • 如果oldVnode.childern有值,vnode.children无值
    • 如果oldVonde.text有值
    • 调用updateChildern()
    • 使用diff算法对比子节点,更新子节点
    • 清空DOM元素
    • 调用addVnodes(),批量移除子节点
    • 调用removeVnodes(),批量移除节点
    • 清空DOM元素的内容
    • 首先执行模块的create钩子函数
    • 然后执行用户设置的create钩子函数
    • 首先执行用户设置的prepatch钩子函数
    • 执行create钩子函数
    • 如果vnode.text未定义
    • 如果设置了vnode.text并且和oldVnode.text不等
    • 最后执行用户设置的postpatch钩子函数

Virtual DOM 的实现原理_Virtual DOM_05

  • 源码位置: 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); 
} 
复制代码

updateChildren

  • 功能:
    • diff算法的核心,对比新旧节点的children,更新DOM
  • 执行过程:Virtual DOM 的实现原理_Virtual DOM_06Virtual DOM 的实现原理_Virtual DOM_07Virtual DOM 的实现原理_Virtual DOM_08Virtual DOM 的实现原理_Virtual DOM_09
    • 如果新节点的数组先遍历完,说明老节点有剩余,把剩余节点批量删除
    • 当老节点的所有子节点先遍历完(oldStartIdx > oldEndIdx),循环结束
    • 新节点的所有子节点先遍历完(newStartIdx > newEndIdx),循环结束
    • 遍历新节点,使用新开始节点的key在老节点数组中找相同节点
    • 如果没有找到,说明新开始节点是新节点
    • 如果找到了
    • 创建新节点对应的DOM元素,插入到DOM树中
    • 重新创建对应的DOM元素,插入到DOM树中
    • 判断新节点和找到的老节点的sel选择器是否相同
    • 如果不相同,说明节点被修改了
    • 如果相同,把 elmToMove对应的DOM元素,移动到左边
    • 调用patchVnode()对比和更新节点
    • 把旧结束节点对应的DOM元素,移动到左边
    • 更新索引
    • 调用patchVnode()对比和更新节点
    • 把旧开始节点对应的DOM元素,移动到右边Virtual DOM 的实现原理_Virtual DOM_10
    • 更新索引
    • 调用patchVnode()对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
    • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
    • oldEndVnode / new EndVnode(旧结束节点 / 新结束节点)
    • 开始节点和结束节点比较,这两种情况类似
    • 如果 旧开始节点 / 新开始节点是相同节点(key和sel相同)Virtual DOM 的实现原理_Virtual DOM_11
    • 旧开始节点 / 新结束节点是相同节点
    • 旧结束节点 / 新开始节点 是相同节点Virtual DOM 的实现原理_Virtual DOM_12
    • 如果不是以上四种情况Virtual DOM 的实现原理_Virtual DOM_13
    • 循环结束
    • 如果老节点的数组先遍历完,说明新节点有剩余,把剩余节点批量插入到右边
    • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
    • oldEndVnode / new EndVnode(旧结束节点 / 新结束节点)
    • oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)
    • oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)
    • 在进行同级别比较的时候,首先会对新老节点数组的开始和结尾设置编辑索引,遍历的过程中移动索引
    • 在对开始和结束节点比较的时候,总共有四种情况
    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为O(n^3)
    • 在DOM操作的时候我们很少很少把一个父节点移动/更新到某一个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为O(n)
  • 源码位置:src/snabbdom.ts

模块源码

  • 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

和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性

  • style.ts

操作行内样式 可以使动画更平滑

  • hero.ts

自定义的模块,examples/hero 示例中使用