patch 整体过程分析

863 阅读4分钟

「这是我参与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)
}

这个方法函数内容相对来说较短,我们就来一点点的进行分析吧!

    1. 获取元素的id const id = elm.id ? '#' + elm.id : '' 获取元素的id,如果有id存在,对其进行拼接上#,否则返回空字符串
    1. 类样式的转换 const c = elm.className ? '.' + elm.className.split(' ').join('.') : '' 如果类样式存在,会调用split方法把多个类样式进行分隔,转换成类选择器的形式,否则返回空字符串
    1. 创建VNode对象: return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm) api.tagName(elm).toLowerCase() + id + c将标签的名字、id选择器、类样式拼接在一起,作为VNode对象的sel,然后后面是对应该方法的各个传参,最后的elm为当前对应的DOM元素。

我们再继续回到代码中

判断新旧节点

 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元素进行移除

    1. elm = oldVnode.elm!获取oldVnode下的对应的DOM元素
    1. parent = api.parentNode(elm) as Node获取对应的父元素,获取父元素的目的是:等会创建完新的VNode节点对应的DOM元素后,把创建的元素挂载到父元素里面
    1. createElm(vnode, insertedVnodeQueue) 创建VNode节点对应的DOM元素
    1. if (parent !== null) 判断父元素是否为空
    1. api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)) 将创建好的DOM元素挂载到DOM树上
    1. removeVnodes(parent, [oldVnode], 0, 0)移除老节点