snabbdom diff算法中文注释
根据 snabbdom源码总结了一下diff算法的几个方法
import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'
type NonUndefined<T> = T extends undefined ? never : T
function isUndef (s: any): boolean {
return s === undefined
}
function isDef<A> (s: A): s is NonUndefined<A> {
return s !== undefined
}
type VNodeQueue = VNode[]
const emptyNode = vnode('', {}, [], undefined, undefined)
//判断两个vnode是否相同 key,data,选择器sel 三个相同,才相同
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1.key === vnode2.key
const isSameIs = vnode1.data?.is === vnode2.data?.is
const isSameSel = vnode1.sel === vnode2.sel
return isSameSel && isSameKey && isSameIs
}
function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined
}
type KeyToIndexMap = {[key: string]: number}
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
}
type ModuleHooks = ArraysOf<Required<Module>>
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
const map: KeyToIndexMap = {}
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key
if (key !== undefined) {
map[key] = i
}
}
return map
}
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]]
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook)
}
}
}
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : ''
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
function createRmCb (childElm: Node, listeners: number) {
return function rmCb () {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node
api.removeChild(parent, childElm)
}
}
}
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) {
// 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, data)
: api.createElement(tag, data)
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)) {
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))
}
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
}
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)
}
}
}
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) {
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
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)) {
removeHook(ch, rm)
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm!)
}
}
}
}
//更新children
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)
}
//用新开始的key在老children中查找有没有这个key对应的节点
idxInOld = oldKeyToIdx[newStartVnode.key as string]
//没有找到
if (isUndef(idxInOld)) {
// 创建节点
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
//找到了
} else {
//老children中key相同的节点
elmToMove = oldCh[idxInOld]
//判断tag是否相同
if (elmToMove.sel !== newStartVnode.sel) {
//tag不同
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
//tag相同
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)
}
}
}
//参照vnode为依据更改
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
//oldVnode的children
const oldCh = oldVnode.children as VNode[]
//vnode的children
const ch = vnode.children as VNode[]
//如果oldVnode等于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)
}
//vnode没有text,可能有children可能没有
if (isUndef(vnode.text)) {
//oldVnode有children vnode也有children
if (isDef(oldCh) && isDef(ch)) {
//oldVnode childern不等于vnode的children 更新children
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
//vnode有children
} else if (isDef(ch)) {
//oldVnode有text 操作有两步1.清空text 2.添加children
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
//oldVnode有children,vnode没有
} else if (isDef(oldCh)) {
//移除oldVnode的children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
//oldVnode有text,vnode没有
} else if (isDef(oldVnode.text)) {
//清空oldVnode的text
api.setTextContent(elm, '')
}
//vnodetext oldVnode可能有text可能没有
} else if (oldVnode.text !== vnode.text) {
//oldVnode没有text有children
if (isDef(oldCh)) {
//移除oldVnode的children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
//根据vnode文本更新
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
//参数1可以是vnode也可以是html节点用oldVnode代表 参数2必须是vnode用VNode代表
//返回一个vnode
return 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]()
//判断oldVnode是不是vnode 如果不是根据element生成空白vnode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
//判断oldVnode与vnode是否相同 相同走pathVnode方法
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
//oldVnode与vnode不同 根据vnode创建节点,移除oldVnode指向的节点
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
//根据vnode创建节点
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
//移除oldVnode指向的节点
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
}
}