Snabbdom源码解析

Vue

# 如何学习源码

  • 先宏观了解
  • 带着目标看源码
  • 看源码的过程要不求甚解(先看主线逻辑,其他分支可以忽略)
  • 调试
  • 参考资料

# Snabbdom 的核心流程

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

# 源码目录结构

image

下面具体说一下src的目录结构

  • helpers

image

  • modules

image

# h函数

创建vnode,怎么创建VNode的?

在使用 Vue 的时候见过 h() 函数,vue中的h函数就是snabbdom中的h函数,vue对其进行了增强,实现了h组件机制,snabbdom没有组件机制。在vue中可以和使用h函数一样,还可以给里面传入选择器。

new Vue({ 
    router, 
    store, 
    render: h => h(App)
}).$mount('#app')
1
2
3
4
5
  • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本(html字符串),Snabbdom 中的 h() 函数是对hyperscript中h函数的增强,不是用来创建超文本,而是创建 VNode。

# 函数重载

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

重载的示意

// 如果在js中下面的函数会替换上面的函数
// 如果是支持重载的语言,传入两个参数执行的是第一个add函数,如果传入的是三个参数执行的是第二个add函数
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)
1
2
3
4
5
6
7
8
9
10

源码位置:src/h.ts

// 在最上面导入了依赖的模块
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'
// 定义了一些类型
export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>

// 给svg添加命名空间
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg'
  if (sel !== 'foreignObject' && children !== undefined) {
    //循环给子元素也递归添加命名空间
    ...
  }
}

// h函数的重载 TypeScript支持重载JS不支持重载
// 上面知识定义了一些形式
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 = {}
  var children: any
  var text: any
  var i: number
  // 处理三个参数的情况
  if (c !== undefined) {
    // b可以是null值,如果b有值就存在data中,data的数据是模块处理所需要的数据
    if (b !== null) {
      data = b
    }
    // 判断c的类型
    if (is.array(c)) {
      // 是数组说明是子元素
      children = c
    } else if (is.primitive(c)) {
      // 是字符串就放到text里面,标签中的文本
      text = c
      // 如果c是VNode判断sel属性即可,Vnode就变成数组
    } else if (c && c.sel) {
      children = [c]
    }
  // 两个参数的情况
  } else if (b !== undefined && b !== null) {
    ...
  }
  // 判断children是否有值,遍历children中的每一个元素判断其是否是string或者number,如果是就是文本节点,并调用vnode函数返回虚拟节点
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
    }
  }
  // 判断如果选择器是svg,就用addNS给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)
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# vnode函数 —— VNode如何创建?

// 导入依赖
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
// 接口(重点):约束实现当前接口的所有对象都拥有相同的属性
// VNode接口约束所有VNode对象都拥有相同的属性
export interface VNode {
  // 选择器,调用h函数传入的第一个参数
  sel: string | undefined
  // 节点所需要的数据,是模块所需要的数据,类型是VNodeData定义的
  data: VNodeData | undefined
  // 子节点,与text属性互斥
  children: Array<VNode | string> | undefined
  // element 元素 VNode转换成DOM中,真实DOM在elm里面
  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?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}
// 函数(重点)参数就是VNode接口
// 需要传5个参数,对应了VNode接口的前5个属性,第6个属性在下面
export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  // 第六个属性key在这里
  const key = data === undefined ? undefined : data.key
  // 返回一个js对象
  return { sel, data, children, text, elm, key }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# VNode如何渲染真实DOM

原理

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
    • 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较

# init函数

要了解patch函数就必须先了解init函数,执行过程看ppt

// 依赖模块
import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'

// 一堆类型定义
...

// 定义常量钩子函数,是个数组
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']

// 定义并导出init函数
// 两个参数,modules模块,和dom操作(可选)
// 默认将虚拟DOM转换成真实DOM,如果要将虚拟DOM转换成字符串或者其他操作,就传第二个参数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number
  let j: number
  const cbs: ModuleHooks = {
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: []
  }
  // 如果第二个参数传了就是第二个参数,如果没传就是htmlDomApi.ts里面的关于DOM操作的对象:虚拟DOM转换成真实DOM
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

  // 循环内部将modules所有的成员存储到cbs中,将来patch内部处理的时候会在某个时机调用钩子函数
  /**
   * modules.ts中规定了所有的函数的格式都必须是一个钩子函数,modules接口保证所有的模块对象都有一个或几个钩子函数
   * 例如:这些模块中都有create和update属性
   * 
   * export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }
   * 
   * 循环hook里面的属性,['create', 'update', 'remove', 'destroy', 'pre', 'post'],通过键去模块中找值(钩子函数),然后将所有的钩子函数存储到cbs中,将来在特定时机触发
   */
  for (i = 0; i < hooks.length; ++i) {
    // cbs.create = [] ,cbs.update = []
    // 为啥是数组,因为模块中的class模块中也有create,attributes模块中也有create
    cbs[hooks[i]] = []
    // 接下来遍历每个模块,将每一个模块的钩子函数放到hook常量里面,如果hook存在就直接放到cbs数组中来
    // cbs的最终形式是:cbs = { create: [fn1, fn2], update: [],...}
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }
  
  // 中间一堆关于patch函数的东西
  ...
 
  // 返回patch函数,如果在一个函数内部返回一个函数,称为高阶函数
  // 高阶函数的好处,形成闭包,patch函数调用的时候不需要再传入modules和domApi了,直接使用init定义的变量
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    // patch函数内部逻辑
    ...
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# patch函数

比较两个节点进行DOM操作,操作有:

  • 创建DOM元素
  • 移除DOM元素
  • 更新文本节点
  • 更新DOM内的子节点

具体的实现在模块中

执行过程看ppt

...
// 判断两个节点是不是同一个节点,通过key(节点的唯一值)和sel(选择器)
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

// 判断节点是否为虚拟节点,通过判断其对象上面有没有sel属性
function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined
}
...

// 导出init函数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  ...
  // 返回patch函数
  // 对比两个vnode,将vnode的差异渲染给真DOM,返回新vnode
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    // 常量 insertedVnodeQueue :插入节点的队列
    // 新插入节点放到队列中的目的,是为了后续触发这些节点上设置的钩子函数,创建vnode的时候也可以有一些自定义的钩子函数
    const insertedVnodeQueue: VNodeQueue = []
    // 触发模块中的pre钩子函数(之前所有的钩子函数都放在cbs中),pre钩子函数是在处理vnode节点前执行的函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // oldVnode可能是虚拟节点,也可能是真实DOM,例如#app,如果是真实DOM就把他们转化成虚拟节点
    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode)
    }

    // 判断是不是相同的节点
    if (sameVnode(oldVnode, vnode)) {
      // 如果是相同节点就找差异更新DOM节点
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // 如果不是相同节点就把新节点渲染成DOM插入到文档中,把老节点移除
      // 后面加感叹号是ts语法,指的是,觉得旧节点中的element不为空
      elm = oldVnode.elm!
      // 获取elm的父节点
      parent = api.parentNode(elm) as Node
      // 创建vnode对应的DOM元素,并触发对应的钩子函数,这个时候并没有渲染到页面中
      createElm(vnode, insertedVnodeQueue)
      
      if (parent !== null) {
        // 如果父节点不为空,就把vnode对应的DOM渲染到父节点中来,并且在elm之后的兄弟节点中
        // 例如:<div id="app"></div>之后
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除旧节点
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }

    // 遍历新节点队列并处罚insert钩子函数
    // 这里的感叹号都是判断其不为空,如果js来写需要用到if语句
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    // 执行cbs中的post钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    // 返回新节点
    return vnode
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

patch里面有三个重要的函数:patchVnode,createElm和removeVnodes,比较复杂内部需要解释一下:

# createElm函数

用思维导图总结一下

  // 创建vnode对应的DOM元素,并触发对应的钩子函数
  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    // vnode的属性,用h函数创建vnode的时候的第二个参数 —— 模块中需要的数据
    let data = vnode.data
    if (data !== undefined) {
      // 执行用户设置的 init 钩子函数
      const init = data.hook?.init
      // 判断init钩子函数是否有定义
      if (isDef(init)) {
        // 如果init钩子函数有定义的时候就直接调用,init是用户传过来的,有可能在内部修改data的值,所以下一步要把新的vnode.data赋值给data
        init(vnode)
        data = vnode.data
      }
    }
    // 把vnode 转换成真实 DOM 对象(没有渲染到页面上)
    /**
     * vnode.children —— vnode的子节点
     * vnode.sel —— vnode的选择器
     */
    const children = vnode.children
    const sel = vnode.sel
    // 如果是感叹号就创建一个注释节点
    if (sel === '!') {
      // 如果vnode.text是undefined就设置为空,为了成功调用createComment
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      // 创建一个注释节点
      vnode.elm = api.createComment(vnode.text!)
      // 不为undefined就创建对应的DOM元素
    } 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
      // 创建DOM元素
      // data.ns是是否有命名空间的意思
      // 如果有定义且有命名空间,会创建带有命名空间的标签,一般情况是svg
      // 否则创建普通标签
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag)
      // 给DOM元素设置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, ' '))
      // 执行cbs的create钩子函数(模块中的钩子函数)
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // 如果子节点是个数组,数组成员不为空那么就遍历数组的成员递归调用createElm
      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))
          }
        }
      // 如果是string 或者是 number就创建一个文本节点
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      // hook是不是有定义不为空就调用create钩子函数(用户传递的钩子函数)
      const hook = vnode.data!.hook
      if (isDef(hook)) {
        // 如果hook.create有值,就直接调用并传入空节点和vnode,如果create函数没有值就不调用此函数
        hook.create?.(emptyNode, vnode)
        // 如果钩子函数中有insert节点,那么会将此节点追加到钩子队列中,在patch函数的最后会遍历并执行
        if (hook.insert) {
          insertedVnodeQueue.push(vnode)
        }
      }
    
    } else {
      // 选择器为空那就是文本节点
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // 直接返回创建好的DOM元素
    return vnode.elm
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# 用户可以自己传递hook钩子函数

let vnode = h('div#app.cls', {
    hook: {
        init (vnode) {
            console.log(vnode.elm)
        },
        create (emptyVnode, vnode) {
            console.log(vnode.elm)
        }
    }
}, 'Hello World')
1
2
3
4
5
6
7
8
9
10

# addVnodes和removeVnodes

addVnodes:批量添加节点

# removeVnodes:批量删除节点

参数(四个):

  • 父节点
  • 移除的子节点,数组
  • 循环的变量
  • 循环的变量
  // 批量删除节点
  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    // 循环,循环数组中的每一个元素
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number
      let rm: () => void
      // 获取vnodes的每一个元素放到ch变量中
      const ch = vnodes[startIdx]
      if (ch != null) {
        // 判断ch中有没有sel选择器的属性,如果有就是一个元素节点
        if (isDef(ch.sel)) {
          // 内部触发destory构造函数
          invokeDestroyHook(ch)
          // remove钩子函数的个数,作用是防止我们重复调用删除节点的方法
          listeners = cbs.remove.length + 1
          // createRmCb是一个高阶函数
          // 返回了一个删除节点的函数rm
          rm = createRmCb(ch.elm!, listeners)
          // 循环遍历模块中的remove钩子函数并触发,每次调用之前都会判断listeners是否为0,就是remove钩子函数是否已经执行完了,只有为0的时候(即钩子函数都触发完毕之后)才调用removeChild方法
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          // 判断用户有没有出入remove钩子函数,如果有就触发
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            // 如果用户没有设置钩子函数,直接调用删除元素的方法
            rm()
          }
        } else { // 如果是undefined就是一个文本节点Text node,如果是文本节点就直接从父元素中移除
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }
  
  // 触发destory钩子函数
  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data
    if (data !== undefined) {
      // 获取data中的hook,用户传来的destroy函数
      data?.hook?.destroy?.(vnode)
      // 模块中的destory钩子函数
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
      // 判断有没有子节点,如果有就递归调用执行子节点的destory钩子函数
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j]
          if (child != null && typeof child !== 'string') {
            invokeDestroyHook(child)
          }
        }
      }
    }
  }

  // 批量删除节点
  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    // 循环,循环数组中的每一个元素
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number
      let rm: () => void
      // 获取vnodes的每一个元素放到ch变量中
      const ch = vnodes[startIdx]
      if (ch != null) {
        // 判断ch中有没有sel选择器的属性,如果有就是一个元素节点
        if (isDef(ch.sel)) {
          // 内部触发destory构造函数
          invokeDestroyHook(ch)
          // remove钩子函数的个数,作用是防止我们重复调用删除节点的方法
          listeners = cbs.remove.length + 1
          // createRmCb是一个高阶函数
          // 返回了一个删除节点的函数rm
          rm = createRmCb(ch.elm!, listeners)
          // 循环遍历模块中的remove钩子函数并触发,每次调用之前都会判断listeners是否为0,就是remove钩子函数是否已经执行完了,只有为0的时候(即钩子函数都触发完毕之后)才调用removeChild方法
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          // 判断用户有没有出入remove钩子函数,如果有就触发
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            // 如果用户没有设置钩子函数,直接调用删除元素的方法
            rm()
          }
        } else { // 如果是undefined就是一个文本节点Text node,如果是文本节点就直接从父元素中移除
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# addVnodes:批量增加节点

参数(四个):

  • 父节点
  • 移除的子节点,数组
  • 循环的变量
  • 循环的变量
  // 批量增加节点
  /**
   * 
   * @param parentElm 父节点
   * @param before 参考节点,插入元素的时候会插入到before之前
   * @param vnodes 添加的子节点,数组
   * @param startIdx 循环变量
   * @param endIdx 循环变量
   * @param insertedVnodeQueue 队列
   */
  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    // 循环遍历所有节点,如果节点不为null的话就转换成虚拟DOM,在调用insertBefore,插入到bdefore元素之前
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# patchVnode

脑图ppt

// 创建key和老节点索引的关系
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  // 定义map常量,是一个空对象
  const map: KeyToIndexMap = {}
  // map的key是节点的key,值是索引
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key
    if (key !== undefined) {
      map[key] = i
    }
  }
  // 返回map
  return map
}

// 对比新旧节点更新差异
  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 1. 执行prepatch和update两个钩子函数,两个钩子的差别是无论新旧节点是否相同,都会执行prepatch,但之后新旧节点不相同,才会执行update
    // 获取用户传入的prepatch钩子函数
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    // 老节点的dom属性赋值给新节点的dom属性,并存一个常量elm
    const elm = vnode.elm = oldVnode.elm!
    // 获取新节点和老节点中的子节点
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    // 判断两个节点的内存地址是否相同,如果相同表示么有变化直接返回
    if (oldVnode === vnode) return
    // 判断节点的data属性是不是为空
    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)
    }
    // 2. 对比啷个vnode
    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)
        // 如果老节点有text属性,新节点没有text属性,那么把老节点的文本清空
      } 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!)
    }
    // 3. 触发用户传入的postpatch钩子函数
    hook?.postpatch?.(oldVnode, vnode)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# updateChildren

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

  /**
   * updateChildren
   * 整体分析:
   * diff算法的核心,内部对比新旧节点的children,更新DOM
   * @param parentElm 父节点
   * @param oldCh 老节点数组
   * @param newCh 新节点数组
   * @param insertedVnodeQueue 调用addVnodes需要传的参数
   */
  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    // 1. 定义了很多变量
    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: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any
    // 2. 循环,在循环中比较新旧两个元素
    // 当老节点的开始索引小于等于老节点的结束索引 && 新节点开始索引小于等于新节点的结束索引 开始循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 先判断新旧开始结束节点是否为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]

      // 开始比较开始和结束节点的四种情况
      // 先比较旧开始节点和新开始节点是否相同,判断key和sel是否相同
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 如果是相同关节点,调用patchVnode判断新老节点的差异然后更新到DOM
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        // 判断完之后新老节点的开始索引++,同时到下一个节点,进入下一次循环
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // 如果两者开始节点不是相同节点,就判断其结束节点是不是相同节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 如果两个结束节点相同,就执行PatchVnode比较两个节点差异并更新DOM
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        // 将两个结束节点的索引--,同时到前一个节点,进入下一次循环
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // 下面两种情况可能是排序  
        // 如果两者的结束节点不同,就比较老的开始节点和新的结束节点是否是相同节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 如果他们是相同节点,就用patchVnode比较两个节点差异并更新DOM
        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比较两个节点差异并更新DOM
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 把就的结束节点移动到旧的开始节点之前
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        // 老节点的结束索引向前移动,新节点的开始索引向后移动
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      
      // 新旧开始结束节点比较完毕  
      // 下面要去遍历所有新节点,使用新节点的key在老节点的数组中找相同key的节点
      } else {
        // 先设置记录 key 和 index 的对象
        if (oldKeyToIdx === undefined) {
          // 如果是undefined就通过函数初始化其值(传参,老节点数组,老节点开始索引,老节点结束索引)
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // 用新节点的key作为索引去老节点中找,可能找到可能没有找到
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 新节点没有在老节点中找到对应元素,就是新的元素
        if (isUndef(idxInOld)) { // New element
          //通过createElm创建DOM对象插入到老开始节点之前
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // 在老元素中找到了对应元素,将其从数组中提取并记录到elmToMove中
          elmToMove = oldCh[idxInOld]
          // 判断老节点和新节点的选择器是否相同,如果不同说明被修改过,就创建新节点并把它插到对应的开始节点之前
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // 如果老节点和新节点的选择器相同,就调用patchVnode两个节点的差异并更新到DOM上
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            // 将移动的老节点的数组对应位置设置成undefined,因为此节点已经被移动走了
            oldCh[idxInOld] = undefined as any
            // 再把老节点移动到老开始节点之前
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        // 重新给新开始节点赋值,指向下一个新节点
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 3. 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
    // 保证至少有一个没有被遍历完,即新老节点的个数如果是相同的,就不走下面的内容了。
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      // 两者其中一个还有剩余,判断新老节点哪个先遍历完成
      if (oldStartIdx > oldEndIdx) {
        // 新节点有剩余,老节点的数组先遍历完成,说明有些内容是要新增的,使用addVnodes添加新元素
        // before是参考节点,表示将新节点插入到哪个位置中
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        // 参数是父节点,参考节点,新节点数组,添加的前后范围...
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        // 老节点有剩余,新节点的数组先遍历完成,说明有些内容是要删除的,使用removeVnodes删除元素
        // 参数是父节点,老节点数组,继续剩余的位置
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

patch() -> patchVnode() -> updateChildren()

# 模块

  • Snabbdom为了保证核心库的精简,把处理元素的属性、事件、样式等工作,放置到模块中。
  • 模块默认不会被导入,如果需要则要按需引入。
  • 模块使用查看官方文档
  • 模块的核心基于Hooks
  • 内部其他都是定义的模块(attributes:DOM元素属性,class:样式,dataset:data-属性,eventlisteners:事件,props,style:行内样式)

# Hooks

定义了参数类型,和接口,接口中定义了钩子函数需要的名称以及对应的类型。

// 定义了参数类型,确定了要传什么参数和返回值
...
// 定义了接口,钩子函数需要的名称以及对应的类型
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 -> invokeDestoryHook 中调用
  // 在删除元素之前触发,子节点的 destory 也被触发
  destroy?: DestroyHook
  // removeVNodes中调用
  // 元素被删除的时候触发
  remove?: RemoveHook
  // patch函数的最后调用
  post?: PostHook
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# module

导入了hooks,定义了模块的钩子函数,并不是所有的钩子函数。

# attributes

// 导入module
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'

export type Attrs = Record<string, string | number | boolean>
// 定义了一些常量
const xlinkNS = 'http://www.w3.org/1999/xlink'
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
const colonChar = 58
const xChar = 120

// attributes的核心函数
function updateAttrs (oldVnode: VNode, vnode: VNode): void {
  var key: string
  var elm: Element = vnode.elm as Element
  // 老节点的属性
  var oldAttrs = (oldVnode.data as VNodeData).attrs
  // 新节点的属性
  var attrs = (vnode.data as VNodeData).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) {
      // 新节点属性值是否是布尔类型的true
      if (cur === true) {
        // 如果是布尔类型就直接设置给DOM,值是空字符串
        elm.setAttribute(key, '')
        // 如果是布尔类型的false,就直接把属性移除掉
      } else if (cur === false) {
        elm.removeAttribute(key)
        // 如果不是布尔类型
      } else {
        // 判断第一个元素是不是 x 如果是就是属于svg中处理命名空间属性
        // xChar -> x
        // <svg xmlns="http://www.w3.org/2000/svg">
        // 如果不是就按照正常的设置属性值
        if (key.charCodeAt(0) !== xChar) {
          elm.setAttribute(key, cur as any)
        // 下面是判断是不是命名空间的,如果是就用  setAttributeNS 方法
        } else if (key.charCodeAt(3) === colonChar) {
          // colonChar -> :
          // Assume xml namespace
          elm.setAttributeNS(xmlNS, key, cur as any)
        } else if (key.charCodeAt(5) === colonChar) {
          // <svg xmlns:xlink="https://www.w3.org/1999/xlink">
          // Assume xlink namespace
          elm.setAttributeNS(xlinkNS, key, cur as any)
        } else {
          // 其他的就用正常属性去设置
          elm.setAttribute(key, cur as any)
        }
      }
    }
  }
  // 遍历老节点的属性,如果在新节点中没有出现就直接移除
  for (key in oldAttrs) {
    if (!(key in attrs)) {
      elm.removeAttribute(key)
    }
  }
}

// 导出 create钩子函数和update钩子函数
export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
更新时间: 2022-01-11 10:46