snabbdom虚拟dom源码浅解析(分析diff算法)

186 阅读6分钟

1.snabbdom 开源库

是一个虚拟dom开源库 vue里的虚拟dom 借鉴的snabbdom;

通过模块可扩展;源码使用tyescript开发;最快的virtualDOM

1.2.虚拟DOM: 用普通的js对象来描述真实DOM

vue的MVVM机制 就是使用虚拟dom 解决视图和状态同步的问题

虚拟DOM可以维护程序的状态 跟踪上一次状态 通过比较前后两次状态差异更新视图

优点: 维护视图和状态的关系;复杂视图情况下提升渲染性能;跨平台

1.3.snabbdom的大致流程:

1.init()设置模块 创建patch()函数

2.使用h()函数创建js对象(vnode)描述真实dom

3.patch()比较新旧两个vnode

4.把变化的内容更新到真实DOM树上

2.下载snabbdom源码 v2.1.0

git clone -b v2.1.0 --depth=1  httsp://github.com/snabbdom/snabbdom.git 
​

snabbdom有几个重要的函数 :

1.h函数;

2.vnode函数;

3.init函数;

4.patch函数;

5.patchVnode函数;

6.createElm函数

6.updateChildren函数;

我们逐步分析: snabbdom采用typescript语言编写;

2.1h函数

h函数在h.ts文件夹里

根据函数参数的不同用到了函数重载

根据函数参数个数:

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;

//h函数实现 根据参数个数和类型 实现 前面四个是重载  这个是h函数的实现 根据函数内部判断参数的个数和类型  
export function h (sel: any, b?: any, c?: any): VNode {
​
 var data: VNodeData = {}
​
 var children: anyvar text: anyvar i: numberif (c !== undefined) {
​
  if (b !== null) {
​
   data = b
​
  }
​
  if (is.array(c)) {
​
   children = c
​
  } else if (is.primitive(c)) {
​
   text = c
​
  } else if (c && c.sel) {
​
   children = [c]
​
  }
​
 } else if (b !== undefined && b !== null) {
​
  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) {
​
  for (i = 0; i < children.length; ++i) {
​
   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] === '#')
​
 ) {
​
  addNS(data, children, sel)
​
 }
​
 return vnode(sel, data, children, text, undefined)
​
};

h函数参数有三个:

sel:选择器

data:模板数据

children:子元素 数组

返回一个vnode对象

h(sel:any,b?:any,c?:any):Vnode)(sel,data,children,text,undefined)

2.2 vnode函数

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

vnode参数:

sel: string | undefined

data: VNodeData | undefined

children: Array<VNode | string> | undefined 和text互斥

text: string | undefined

elm: Node | undefined 当前vnode对象转换之后的元素

key: Key | undefined

VNodeData 约束vnodeData类型

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
}

2.3 init函数

init函数返回patch函数: 这是高阶函数 如果一个函数返回值是另一个函数 就是高阶函数

function init (modules:Array<Partial>,domApi?:DOMAPI){}

init函数 两个参数 一个是用到的模块数组 一个是domapi domapi是把元素转换成其他平台下的元素 初始化浏览器下的dom元素

功能:创建patch函数 :对比两个vnode 更新vnode

init函数处理前面的参数 patch只专注于新旧vnode参数的处理;

返回 patch(oldVnode:Vnode |Element ,vnode:VNode):VNode{}

2.4patch函数

patch(oldVnode:Vnode |Element ,vnode:VNode):VNode{}

参数是 旧vnode和新vnode 返回一个vnode对象 当做下一次更新的旧vnode;

patch流程:

1.定义变量: insertedVnodeQueue是一个常量 新插入节点队列

2..触发钩子函数

3.判断第一个参数 是否是vnode对象 如果是真实dom对象 转换成vnode对象

4.判断是否是相同节点:sameVnode函数:key和sel都相同

4.1是的话:

调用patchVnode(oldVnode,vnode,insertedVnodeQueue)

patchVnode 真正的处理两个节点之前差异的函数

4.2不是相同节点:

创建新的vnode元素,并把创建的元素插入到dom树上,并把dom树 上的老节点 移除

createElm(vnode, insertedVnodeQueue) 把虚拟dom生成dom树 只是创建dom元素 不挂载

4.2.1判断是否有父节点 :

有父节点就调用insertBefore函数 是挂载到dom树上

5.触发对应的钩子函数 : 之前保存在insertedVnodeQueue 队列中的 有勾子函数 就insert函数调用

insertedVnodeQueue 队列元素 是在createElm 中触发的

insert 函数 用户添加的

6.返回vnode对象

function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
​
    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode)
    }
​
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
​
      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
  }

3.createElm函数

1.执行用户设置的int函数: const init = data.hook?.init

2.把vnode转换成真实dom

3.返回创建的dom元素vnode.elm

function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    let data = vnode.data
    if (data !== undefined) {
      const init = data.hook?.init
      if (isDef(init)) {
        init(vnode)
        data = vnode.data
      }
    }
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '!') { //是注释节点
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      vnode.elm = api.createComment(vnode.text!)
    } else if (sel !== undefined) { //是dom元素内容
      // Parse selector 解析选择器;    h('h1#app.class', 'Top 10 movies')
      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, ' '))
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      if (is.array(children)) {//有子节点 创建子vnode 对应的dom元素 并追加到dom树上
        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)) {
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      //调用用户传进来的钩子函数  把具有钩子函数的cnode 存储到队列中 等待会插入的时候再执行钩子函数
      const hook = vnode.data!.hook
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode)
        if (hook.insert) {
          insertedVnodeQueue.push(vnode)
        }
      }
    } else { //是文本节点
      vnode.elm = api.createTextNode(vnode.text!)
    }
    return vnode.elm
  }

4.removeVnodes 函数 addVnodes函数

removeVnodes(parent, [oldVnode], 0, 0)

oldVnode对应的要删除的节点 开始索引 结束索引

 function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number  //防止重复删除
      let rm: () => void
      const ch = vnodes[startIdx]
      if (ch != null) {//节点不为空 
        if (isDef(ch.sel)) { //元素节点
          invokeDestroyHook(ch)
          listeners = cbs.remove.length + 1
          rm = createRmCb(ch.elm!, listeners)
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) { //remove钩子函数 
            removeHook(ch, rm)
          } else {
            rm() //真正删除的函数
          }
        } else { // Text node 文本节点
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }
  1. invokeDestroyHook
  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode) //是否有钩子函数
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) //依次循环调用执行这个销毁函数
      if (vnode.children !== undefined) {//如果有子节点  继续循环调用 递归触发invokeDestroyHook
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j]
          if (child != null && typeof child !== 'string') {
            invokeDestroyHook(child) //这个函数 是在删除dom之前执行的
          }
        }
      }
    }
  }

7.createRmCb

  function createRmCb (childElm: Node, listeners: number) {
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm) as Node
        api.removeChild(parent, childElm)
      }
    }
  }

8addVnodes函数**

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }

9.patchVnode函数

 function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      vnode.data.hook?.update?.(oldVnode, 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)
      } 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!)
    }
    hook?.postpatch?.(oldVnode, vnode)
  }

判断新旧节点和文本属性的差异,更新真实的dom

触发postpatch钩子函数

undateChildren 函数具体

1.定义内部使用变量

2,同级别节点比较

3.循环结束后收尾工作

  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    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
​
    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 as string]
        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 as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        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)
      }
    }
  }

diff算法

上面的代码含义 用到了diff算法:

双边比较:

Virtual DOM 只会对同一个层级的元素进行对比 在dom操作上很少有跨层级的操作

patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)

有以下四种情况:

新dom (vnode) 旧dom(oldVnode)

有子节点 有子节点 子节点是text文本节点 或者 element元素节点

有子节点 无子节点 旧dom无子节点 新dom有子节点;直接创建子节点 插入到该有的位置

无子节点 有子节点 旧dom有子节点 新dom无子节点 删除旧dom的子节点

无子节点 无子节点

重要的是 新旧dom都有子节点: 都是元素节点:

dom操作基本上都是同级别的改变 没有跨级别

oldStartIndex oldEndIndex

newStartIndex newEndIndex

一共四种比较方法:

oldStartIndex和newStartIndex

oldStartIndex和newEndIndex

oldEndIndex 和newStartIndex

oldEndIndex 和newEndIndex

当有一个相等下 就会继续移动向下一个节点

例如:

旧节点有五个:1 2 3 4 5

oldStartIndex等于1 时起始点是第一个节点1

oldEndIndex等于1时 起始点是第五个节点5

新节点有五个:1 2 3 4 5

newStartIndex等于1时 起始点是第一个节点1

newEndIndex等于1时 起始点是第五个节点5

开始比较:

1.第一个比较方法:oldStartIndex newStartIndex

oldStartIndex=1 和 newStartIndex=1 比较 key 和sel 假如相等:这一个节点就不用更新新的dom 可复用

然后就变成了:oldStartIndex=2 起始点向下一个节点移动是节点为2 newStartIndex=2 节点为2 会继续比较

2.第二个比较方法:oldEndIndex newEndIndex

oldEndIndex等于1时 起始点是第五个节点5 比较 newEndIndex等于1时 起始点是第五个节点5 假如相等:这一个节点就不用更新新的dom 可复用

然后就变成了:oldEndIndex=2 起始点向下一个节点移动是节点为4 newEndIndex=2 节点为4

1.这四种比较方法 成立一种 sameVnode的条件 就把他move到他的位置

2.当四种方法不成立的时候 新旧的key都不相同 就先遍历新开始节点 在旧节点中查找具有相同key值的节点

2.1 如果找不到 就说明新的开始节点是新的节点 就要创建新dom元素 插入最前面

2.2 如果找到了 一样key值的 判断sel是否相同

2.2.1 sel不相同 也是新的节点 也要创建 插入最前面

2.2.2 sel相同 移动到最前面 不创建dom元素

  1. 循环结束 两种情况;

    1. 老节点先遍历完 oldStartIndex >oldEndIndex

      1. 先比较开始节点 相同就复用 不同就比较结束节点 相同就移动结束节点前移 不相同就结束了
  2. 新节点先遍历完 newStartIndex >newEndIndex

先比较开始节点 相同就复用 新开始节点后移 继续比较 不同就比较结束节点

相同就移动结束节点前移 不相同就结束了

3.1 新节点剩余: 直接创建dom 移动到它的位置

3.2 老节点剩余: 直接删除

key的意义 不设置key最大程度重用dom元素 但是会出现渲染错误问题