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: 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)
}
}
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!)
}
}
}
}
- 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元素
-
循环结束 两种情况;
-
老节点先遍历完 oldStartIndex >oldEndIndex
- 先比较开始节点 相同就复用 不同就比较结束节点 相同就移动结束节点前移 不相同就结束了
-
-
新节点先遍历完 newStartIndex >newEndIndex
先比较开始节点 相同就复用 新开始节点后移 继续比较 不同就比较结束节点
相同就移动结束节点前移 不相同就结束了
3.1 新节点剩余: 直接创建dom 移动到它的位置
3.2 老节点剩余: 直接删除
key的意义 不设置key最大程度重用dom元素 但是会出现渲染错误问题