听说vue的virtualDOM部分的设计有借鉴snabbdom?正好现在水平也不够去阅读vue的源码,先用snabbdom来联系思维与阅读能力,为未来我阅读vue打下一点小台阶,话不多说开整
一. 全局走向分析
1.首先打开npm下载到本地的snabbdom包,分析文件目录
-src
-package
-h.ts -提供了h 函数 用于生成vnode
-hooks.ts
-htmldomapi.ts -提供了一些dom操作的封装
-init.ts -提供了init 函数 组装function module 并返回一个patch,用于将vnode 上树 domTree,同时包含了diff算法
-is.ts -提供了 数组 和 原始值判断函数
-jsx.ts
-jsx-global.ts
-thunk.ts
-tovnode.ts
-vnode.ts -ts类型的管理,定义了类型的结构
以上未标注部分均不在本次阅读的范围内,本次阅读主要针对 diff算法 与 virtualDOM的工作原理
2.简单说明virtualDOM的工作原理(Vnode:虚拟node, Node:DOM树上的node)
- 为减少DOM操作的成本,提供了经典的diff算法,精确的比较出差异,并进行DOM更新
- 1.减少成本,但是将Vnode -> Node 上树仍然需要dom操作
- 2.本质是Vnode 与 Vnode 的比较,而并非 Vnode 与 Node的比较
3.snabbdom案例分析大局
var patch = init([ // Init patch function with chosen modules
//...
])
var container = document.getElementById('container')
var vnode = h('div#container.two.classes', { props: {} ... }, [
//vnode children
])
patch(container, vnode) //上树
var newVnode = h('div#container.two.classes', { props: {} ... }, [
//vnode children
])
patch(vnode, newVnode)
二.进入正题
通过上面的例子我们就已经可以发现了,我们的关注点应该为 h 函数 和 另一个 patch 函数,那么就让我们继续往下走
1.分析 h 函数
h.ts
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) {// 三个参数的情况
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)//child为string,number
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#') //svg svg. svg#
) {
addNS(data, children, sel)//针对svg标签的操作
}
return vnode(sel, data, children, text, undefined) //返回的vnode
};
vnode.ts
export function vnode (sel: string | undefined,
//...
return { sel, data, children, text, elm, key }
}
从上面的代码我们可以发现h 函数的主要作用 就是 生成 vnode函数所返会的结构 { sel, data, children, text, elm, key }
2.patch 函数
因为patch才是整个精细比较的主体所以难度比 h 包要高一点,首先patch由 init 返回,我们就需要去锁定init的返回值
init.ts
...
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]()
if (!isVnode(oldVnode)) {//第一个参数不是virtualDOM,如:案例的container
oldVnode = emptyNodeAt(oldVnode) //调用emptyNodeAt将其转为vnode
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
createElm(vnode, insertedVnodeQueue) //新增Node
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0) //删除OldNode
}
}
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
}
}
emptyNodeAt
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)
}
初步流程图出炉
到目前为止所有的判断都是简单判断 , 新增和删除都已经使用了dom操作,而这里也说明了调用patchVnode更新的前提是
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
这里不对createElm 和 removeVnodes进行分析了,createElm 是一个运用了递归能够将所有children从vnode -> node 的函数,而removeVnodes则是可以大批量从domTree上删除dom节点的操作其中createElm包含了较多的判断可以严谨的生成TextNode,TagNode,CommentNode节点
3.进入patchVnode
在patchVnode函数中包含了updateChildren,这个操作中就包含了核心的diff算法
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)
}
去除了一些生命周期的代码,我们可以大致得出以下流程
这里作者有一个非常巧妙的方法将第一步判断节点划分为两个路线
这个方式存在于h包中,在生成vnode时候每一个vnode 的children属性和text属性不可能同时存在有效值
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) { //只有一个对象,进行包装
children = [c]
}
4.有趣的diff
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, oldEndIdx = oldCh.length - 1, oldStartVnode = oldCh[0], oldEndVnode = oldCh[oldEndIdx]
let newStartIdx = 0, newEndIdx = newCh.length - 1, newStartVnode = newCh[0], 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) { //以下为map相关
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) {// key 相同但是 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) {//一整个while循环走下来,两个列表中有一个列表有剩余
if (oldStartIdx > oldEndIdx) {// 情况一 newCh 有剩余
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)// before为null 往末尾插入
} else {// 情况一 oldCh 有剩余
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
分析得出以上结论,我们需要四个指针
四种不同的判断情况
1. sameVnode(oldStartVnode, newStartVnode)
2.sameVnode(oldEndVnode, newEndVnode)
3.sameVnode(oldStartVnode, newEndVnode)
4.sameVnode(oldStartVnode, newEndVnode)
5.map的方式:该方式基于上述四种情形均不存在命中时,使用oldChildren列表中的每一项的key形成一个map降低了时间复杂度
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
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
}
/*
map = {
key1: 1,
key2: 2
}
*/
当判断newList[newStartIdx].key 存在于 map中则进行更新,否则会直接新建
6.while循环结束判断两个children列表是否都已经将指针走完
阅读源码不是个容易的事尽管我只找了一部分需要内容进行阅读但是感觉还是有些困惑,例如updateChildren中的最前面的判空,造成左移的是那部分代码的作用,我在写完这篇文章后的想法是一些removeChild操作造成的但是也只是部分猜测,如果你想与我分享你的认识请联系我的微信,谢谢