「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
patch
接下来我们要学习 VNode 渲染成真实DOM的过程,但是这个过程比较复杂,也是Snabbdom的核心,那么我们这一小节先来学习 patch 整体过程分析
patch 整体过程分析
-
patch (oldVnode: VNode | Element, vnode: VNode)
-
把新节点中变得内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
-
对比新旧 VNode 是否相同节点(节点的key和sel相同)
-
如果不是相同节点,删除之前的内容,重新渲染
-
如果是相同的节点,在判断信的VNode是否有text,如果有并且和 oldVnode的 text不同,直接更新文本内容
-
如果新的 VNode有 children,判断子节点是否有变化
-
patch 对比 VNode
了解方法入参
首先我们来回顾一下 patch函数的两个参数
- oldVnode: 它既可以是普通的VNode对象,也可以是普通的DOM对象(首次渲染的时候提供)
- vnode
接下来:
函数中定义了3个变量 和一个常量 insertedVnodeQueue,
这里我们首先触发了:
cbs中的pre钩子函数,这个函数就是我们正式开始处理之前的钩子函数
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
oldVNode 类型判断和转换
接下来我们判断传入的第一个参数 oldVNode是否是VNode对象?因为第一个对象既可以是真实DOM对象,也可以是虚拟的DOM对象
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
下面我们来看一下isVnode函数如何判断是否真实的DOM
isVnode方法
function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined
}
这里的代码很简单,判断 vnode下是否有sel属性,我们认识拥有sel选择器属性的就是VNode对象
回到上面的代码中
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode) <=== 回到这里
}
当我们传入的 oldVnode 是真实DOM的时候,我们会通过 emptyNodeAt 这个函数,将真实的DOM转换成 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)
}
这个方法函数内容相对来说较短,我们就来一点点的进行分析吧!
-
- 获取元素的id
const id = elm.id ? '#' + elm.id : ''获取元素的id,如果有id存在,对其进行拼接上#,否则返回空字符串
- 获取元素的id
-
- 类样式的转换
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''如果类样式存在,会调用split方法把多个类样式进行分隔,转换成类选择器的形式,否则返回空字符串
- 类样式的转换
-
- 创建VNode对象:
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)api.tagName(elm).toLowerCase() + id + c将标签的名字、id选择器、类样式拼接在一起,作为VNode对象的sel,然后后面是对应该方法的各个传参,最后的elm为当前对应的DOM元素。
- 创建VNode对象:
我们再继续回到代码中
判断新旧节点
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)
}
}
首先我们先使用sameVnode函数判断新旧两个节点是否是同一个节点
sameVnode 比较是否相同节点函数
sameVnode 这个函数也十分的简单
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
就是判断key和sel节点是否相等
-
如果是同一个节点调用
patchVnode函数更新节点 patchVnode 函数比较复杂,因为本章字数关系,就先不说了 -
如果不是一个节点的情况
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)
}
这里会创建新的VNode对应的DOM元素,并且把新创建的DOM元素插入到DOM树上,并且把老节点对应的DOM元素进行移除
-
elm = oldVnode.elm!获取oldVnode下的对应的DOM元素
-
parent = api.parentNode(elm) as Node获取对应的父元素,获取父元素的目的是:等会创建完新的VNode节点对应的DOM元素后,把创建的元素挂载到父元素里面
-
createElm(vnode, insertedVnodeQueue)创建VNode节点对应的DOM元素
-
if (parent !== null)判断父元素是否为空
-
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))将创建好的DOM元素挂载到DOM树上
-
removeVnodes(parent, [oldVnode], 0, 0)移除老节点