Virtual DOM
Virtual DOM
(虚拟DOM
),是由普通的js
对象来描述DOM
对象。
- 为什么要使用虚拟
DOM
- 真实
DOM
的创建需要花费很大代价 - 虚拟
DOM
通过比较前后两次状态差异更新真实DOM
,极大的减少了真实DOM
的创建。
- 真实
- 虚拟
DOM
的作用- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 实现跨平台
- 浏览器渲染
DOM
- 服务端渲染
SSR
(Nuxt.js/Next.js
) - 原生应用(
Weex/React Native
) - 小程序(
mpvue/uni-app
)
- 浏览器渲染
- 虚拟
DOM
库Snabbdom
Vue2.x
内部使用改造后的Snabbdom
- 源码体积小,只有
200L
- 通过模块可扩展
- 源码使用
ts
开发 - 最快的
Virtual DOM
之一
virtual-dom
:最早的虚拟DOM
实现
Snabbdom
的基本使用
-
安装:
yarn add/npm install snabbdom
-
引入:通过
import
引入相关的模块import { init } from 'snabbdom/build/package/init' import { h } from 'snabbdom/build/package/h'
官方实例写法如下:
import { init } from 'snabbdom/init' import { h } from 'snabbdom/h'
官方写法是因为使用了
webpack5
的exports
,设置了子路径映射,node12
之后才支持。
-
init
、h
、patch
函数的使用-
init
: 接受一个模块数组,并返回一个patch
函数const patch = init([])
-
h
:创建虚拟DOM
,接收三个参数,第一个是字符串类型的标签或者选择器,第二个参数是一个可选的选项对象,第三个参数是表示子元素,可以是一个字符串或一个数组,也是可选的。// 创建一个id为main的空div vnode = h('div#main') // 创建一个id为main的div,文本内容为Hello Vue vnode = h('div#main', 'Hello Vue') // 创建包含多个子元素的div vnode = h('div', [ 'Hello Vue', h('h1', 'children') ])
-
patch
:init
函数执行后的返回,接收两个参数,第一个参数是要被替换的真实DOM
或虚拟DOM
,第二个参数是新的虚拟DOM
const old = patch(container, vnode) patch(old, vnode)
-
-
模块
-
模块的作用
Snabbdom
的核心库不能处理DOM
元素的属性/样式/事件等,可以通过注册Snabbdom
默认提供的模块来实现Snabbdom
中的模块可以用来扩展Snabbdom
的功能Snabbdom
的模块是通过注册全局的钩子函数来实现,这些钩子函数在虚拟DOM
的生命周期会被执行
-
官方提供的模块
attributes
:设置DOM
对象的属性,使用DOM
的标准方法createAttribute()
实现props
:设置DOM
对象的属性,只是是通过对象.
的形式实现dataset
:处理元素的自定义属性class
:改变元素样式属性style
:设置行内样式eventlisteners
:添加事件监听。
-
模块的使用
- 导入:导入需要使用的模块
- 注册:在
init
函数中注册 - 使用:在
h
函数的第二个参数中传入选项数据来使用
import { init } from 'snabbdom/build/package/init' import { h } from 'snabbdom/build/package/h' // 1、导入模块 import { styleModule } from 'snabbdom/build/package/modules/style' import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners' // 2、注册 const pacth = init([ styleModule, eventListenersModule, ]) const container = document.getElementById('app') // 在h函数的第二个参数中传入选项数据 const vnode = h('div', [ h('h1', { style: { color: 'red' } }, 'Style Test'), h('p', { on: { click: clickHandler } }, 'Click Test') ]) function clickHandler() { console.log('click'); } pacth(container, vnode) ```
-
-
Snabbdom
源码-
h
函数 :h
函数最早见于hyperscript
,用于创建超文本。Snabbdom
在此基础上对h
函数进行改造,用于创建虚拟DOM
(vnode
)对象。h
函数内部使用重载来实现函数的多种参数情况的调用。处理函数参数,并调用vnode
函数生成vnode
对象。 -
patch
函数整体执行过程分析:patch(oldVnode, newVnode)
:把新节点中变化的内容渲染到真实DOM
,并返回新节点作为下一次处理的旧节点。- 对比新旧
VNode
,是否是相同节点,比较节点的key
和sel
是否相同 - 如果不是相同节点,删除之前的内容,重新渲染
newVnode
。 - 如果是相同的节点,
newVnode
中有text
,再判断新旧节点的text
是否相同,如果不同,直接更新文本内容。 - 如果
newVnode
中有children
,在判断子节点是否有变化。··
-
init(modules[, domApi])
:modules
表示引用的模块,domApi
接收传入的dom
操作方法对象,默认为htmlDomApi
,也可以传入其他平台处理dom
的方法对象,也是虚拟DOM
实现跨平台的基础。-
首先定义了一个接收钩子回调函数的对象,用来接收各个钩子的回调函数。
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'] let i: number let j: number const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [] }
-
对
domApi
进行处理,设置domApi
的默认值。const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
-
遍历
modules
,并将模块中的钩子函数保存到ModuleHooks
中。// 对注册的模块进行遍历,并将模块中定义的钩子函数存入到保存钩子函数的对象中。 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) } } }
-
返回
patch
函数。
-
-
patch(oldVnode, vnode)
:oldVnode
可以是真实的DOM
或者虚拟DOM
,vnode
是要替换显示的虚拟DOM
。-
定义变量,执行模块的
pre
钩子函数。let i: number, elm: Node, parent: Node const insertedVnodeQueue: VNodeQueue = [] for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
-
判断
oldVnode
是否是Vnode
节点,如果不是则将它转化为Vnode
。if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode) }
判断是否是
Vnode
只要判断对象中是否具有sel
属性。function isVnode (vnode: any): vnode is VNode { return vnode.sel !== undefined }
由真实
DOM
创建Vnode
只需要拼接选择器,然后调用vnode
函数创建Vnode
对象即可。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) }
-
判断转化后的
oldVnode
和vnode
是否是相同的Vnode
- 如果相同,则比较两个
Vnode
的差异,并将差异更新到视图。 - 如果不相同,则先根据
vnode
创建真实DOM
,然后将创建的真实DOM
通过oldVnode
的父元素添加到oldVnode
的后面,并删除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) } }
- 如果相同,则比较两个
-
遍历更新到视图的
Vnode
,并执行用户传入的insert
钩子函数(通过vnode.data.hook
指定)。for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]) }
这里的属性名后面加上
!
的写法是typescript
的语法,表示属性不是undefined
或null
。-
执行模块的
post
钩子函数。并返回vnode
对象。for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() return vnode
-
这里有两种钩子函数,一种是模块提供的
vnode
的生命周期钩子函数,以及用户传入的生命周期钩子,类似Vue
的生命周期钩子函数。vnode
生命周期钩子函数是注册模块中提供的,然后保存在cbs
对象中,包括['create', 'update', 'remove', 'destroy', 'pre', 'post']
。- 用户传入的生命周期钩子函数是创建
vnode
对象时通过第二个参数data
传入的。定义在data.hook
中,包括init
、create
、insert
、remove
等。
-
-
createElm(vnode, insertedVnodeQueue)
:根据虚拟DOM
创建真实DOM
,并将真实DOM
对象保存在vnode
对象的elm
属性中。vnode
是要创建为真实DOM
的Vnode
对象,insertedVnodeQueue
保存用户传入了insert
钩子的vnode
对象。-
首先定义变量,并执行用户传入的
init
钩子函数,这里执行用户传入的init
钩子时可能修改data
,所有需要对data
进行重新赋值。let i: any let data = vnode.data // 判断并执行init钩子 if (data !== undefined) { const init = data.hook?.init if (isDef(init)) { init(vnode) data = vnode.data//可能修改`data`,所有需要对`data`进行重新赋值 } }
-
然后要根据
vnode
对象来创建真实的DOM
节点,并保存到vnode.elm
中。 这里按节点的类型分为三种情况,注释节点、元素节点以及文本节点。-
注释节点,当
sel
属性为!
时,认为要创建注释节点,然后需要处理一下vnode.text
,没有定义时需要给一个空字符的默认值。然后通过createComment
来创建一个注解节点。if (sel === '!') { // 判断是否是注释节点 if (isUndef(vnode.text)) { // 是否有注释文本 vnode.text = '' } vnode.elm = api.createComment(vnode.text!) // 创建注释DOM }
-
文本节点,当
sel
没有定义时,即为undefined
,认为要创建一个文本节点。然后根据vnode.text
创建文本节点即可。vnode.elm = api.createTextNode(vnode.text!)
-
元素节点,除了上述两种情况外,都认为要创建元素节点。
- 首先根据
sel
选择器解析出需要创建的元素标签名,以及id
和class
。 - 再根据
data.ns
命名空间属性来判断是否要根据命名空间来创建节点,将id
和class
属性添加到节点上; - 判断是否具有子元素或文本内容,如果存在子元素节点,递归遍历并创建子元素节点添加到当前节点,如果存在文本内容,则根据文本内容创建子节点并添加到当前节点。如果存在子元素就不存在文本内容,存在文本内容就不存在子元素,这两者是互斥的。
- 最后如果用户传入了
insert
钩子函数,则将vnode
对象添加到insertedVnodeQueue
中。
if (sel !== undefined) { // 根据选择器创建真实DOM元素, div#conatienr.main.active // Parse selector // 创建DOM元素 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) // ns表示命名空间 ? api.createElementNS(i, tag) // 如果有命名空间,根据命名空间创建DOM元素 : api.createElement(tag) if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)) // 设置id属性 if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')) // 设置class样式 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode) // 执行create钩子函数 // 判断是否存在子节点,如果存在子节点,递归向本节点添加子节点创建的真实DOM 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) } } }
- 首先根据
-
-
返回
vnode.elm
。
-
-
removeVnodes (parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number)
:从父节点中删除子节点,parentElm
父元素节点,是一个真实的DOM
节点,vnodes
是要删除的子节点的数组,startIdx
是要删除的子节点的起始下标,endIdx
是要删除的子节点的结束下标。-
从
startIdx
开始遍历每个要删除的子节点,对非空节点进行处理。 -
如果子节点是文本节点,则直接删除节点即可。
-
子节点为非文本节点时,在删除每个子节点之前,需要先执行当前节点及其子节点中用户传入的
destroy
钩子函数。// 执行虚拟DOM及其子元素的destroy钩子,和cbs中的destroy钩子 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) } } } } }
-
然后执行模块的
remove
钩子函数以及用户传入的remove
钩子函数,最后再移除节点元素。这里为了防止重复删除节点,将生成的删除节点的函数rm
传入到remove
钩子函数中,在remove
钩子函数中调用rm
删除节点。这样设计的原因是在remove
钩子中可能存在异步任务(如动画),需要等待异步任务执行完毕之后才能移除节点。在rm
函数内部使用闭包的原理记录模块中remove
钩子的数量listeners
,每次调用rm
函数时,listeners
会自减,当listeners
为零时才会执行从真实DOM移除节点的操作。// 创建删除元素的方法 function createRmCb (childElm: Node, listeners: number) { return function rmCb () { if (--listeners === 0) { // 判断remove钩子是否都执行完了,防止重复删除,模块的remove钩子中可能存在异步的任务,需要等待这些任务执行完成之后才能删除元素。 const parent = api.parentNode(childElm) as Node api.removeChild(parent, childElm) // 删除元素 } } }
removeVnodes
完整代码// 从父节点移除虚拟数组中指定范围的元素对应的真实DOM 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) // 执行destroy钩子 listeners = cbs.remove.length + 1 // 记录remove钩子个数 rm = createRmCb(ch.elm!, listeners) for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm) // 执行虚拟DOM remove钩子函数 // 用户传入的 remove 钩子 const removeHook = ch?.data?.hook?.remove if (isDef(removeHook)) { removeHook(ch, rm) // 这里为什么不执行子节点用户传入的remove的钩子呢 } else { rm() } } else { // Text node api.removeChild(parentElm, ch.elm!) } } } }
-
-
patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)
:对比oldNode
和vnode
,并更新差异部分视图。-
首先执行用户传入的
prepatch
钩子函数。判断两个节点是否相同,如果不相同则执行模块和用户传入的update
钩子函数。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 // 执行模块及用户传入的update 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
中是否具有文本内容来进行判断,文本内容和子元素节点时互斥的。-
如果
vnode
中没有文本内容,再根据是否具有子元素节点来进行处理。结合oldVnode
的子节点和文本内容是否存在有以下几种情况。vnode
存在子元素节点,oldVnode
也存在子元素节点,需要对比二者的子元素节点,更新差异部分。vnode
存在子元素节点,oldVnode
存在文本内容,设置文本内容为空,并添加vnode
的子元素节点。vnode
不存在子元素节点,oldVnode
存在子元素节点,删除oldVnode
的子元素节点。vnode
不存在子元素节点,oldVnode
存在文本内容,将文本内容设置为空。
// 更新元素子节点 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)// 将新节点中的子节点添加到真实DOM中。 } else if (isDef(oldCh)) { // 如果旧节点中存在子节点,移除旧节点的子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 如果旧节点中存在文本节点,将文本内容设置为空 api.setTextContent(elm, '') } }
-
如果
vnode
中有文本内容,则判断一下oldVnode
中是否有子元素节点,如果有的话需要移除oldVnode
中的子元素节点,来触发节点移除相关的钩子函数。然后再设置文本内容为vnode
的文本内容。else if (oldVnode.text !== vnode.text) {// 新旧节点文本内容不同, if (isDef(oldCh)) { // 如果旧节点中存在子元素节点,移除子元素节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } api.setTextContent(elm, vnode.text!) // 设置文本内容为新节点的文本内容。 }
-
-
最后执行用户传入的
postpatch
钩子。
-
-
function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue)
:更新子元素的差异部分。函数函数内部定义了四个指针变量,分别是指向oldCh
开始位置的oldStartIdx
以及结束位置的oldEndIdx
,和指向newCh
开始位置的newStartIdx
以及结束位置的newEndIdx
。然后通过判断指针指向的节点是否相同来进行相应的操作,有以下几种判断情况:oldStartIdx
和newStartIdx
指向的节点是相同节点,更新节点,oldStartIdx
和newStartIdx
向后移动一位,再次进行判断。oldEndIdx
和newEndIdx
指向的节点是相同节点,更新节点,oldEndIdx
和newEndIdx
向前移动一位,再次进行判断。oldStartIdx
和newEndIdx
指向的节点是相同节点,更新节点,将oldStartIdx
位置的节点移动到旧子节点的最后位置,oldStartIdx
向后移动一位,newEndIdx
向前移动一位。然后再次重复上面的判断过程。oldEndIdx
和newStartIdx
指向的节点是相同节点,更新节点,将oldEndIdx
位置的节点移动到旧子节点的最前面。oldEndIdx
向前移动一位,newStartIdx
向后移动一位。- 如果上面的情况都不满足,则根据
key
来查找旧子节点的[oldStartIdx, oldEndIdx]
范围内中有没有相同的key
。- 如果能找到相同的
key
, 则根据sel
属性来判断是否是同一节点,如果是同一节点,则更新节点,并将更新后的节点移动到newStartIdx
之前的位置。 需要注意的是,更新节点只会更新子节点或文本内容,不会更新节点的状态。 如果不是同一节点,则直接在newStartIdx
之前插入新节点。 - 如果找不到到相同的
key
,表示是一个新增的节点,则直接在newStartIdx
之前插入新节点。 - 最后经过上述处理之后,
newStartIdx
向后移动一位。
- 如果能找到相同的
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { // 将oldStartIdx定位到第一个非空子节点 oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left } else if (oldEndVnode == null) {// 将oldEndIdx定位到最后一个非空子节点 oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode == null) {// 将newStartIdx定位到第一个非空子节点 newStartVnode = newCh[++newStartIdx] } else if (newEndVnode == null) {// 将newEndIdx定位到最后一个非空子节点 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)) { // 旧子节点的开始节点和新子节点的结束节点相同, patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 在旧子节点的基础上,更新开始节点的内容, // 将旧子节点的开始节点移动到旧子节点的结束节点之后 api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!)) oldStartVnode = oldCh[++oldStartIdx] // 旧子节点开始指针向后移 newEndVnode = newCh[--newEndIdx] // 新子节点结束指针向前移 } else if (sameVnode(oldEndVnode, newStartVnode)) { // 旧子节点的结束节点和新子节点的开始节点相同, patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 在旧子节点的基础上,更新开始节点的内容, // 将旧子节点的结束节点移动到旧子节点的开始节点之前 api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx]// 旧子节点结束指针向前移 newStartVnode = newCh[++newStartIdx]// 新子节点开始指针向后移 } else { if (oldKeyToIdx === undefined) { // 创建一个旧子节点的key和index映射关系的对象。 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } idxInOld = oldKeyToIdx[newStartVnode.key as string] // 在旧子节点中定位新子节点开始节点的位置 if (isUndef(idxInOld)) { // 新子节点开始节点是一个新增节点 // 创建节点,并将节点插入到旧子节点开始节点之前 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]// 新子节点开始指针向后移 } }
循环进行上述判断,当
oldCh
遍历完成(oldStartIdx > oldEndIdx)
,或newCh
遍历完成(newStartIdx > newEndIdx)
时标志循环结束。这里又分为3
中情况:oldStartIdx > oldEndIdx
和newStartIdx > newEndIdx
都成立,表示oldCh
和newCh
都遍历完成,所以不需要进行其他处理。- 只有
oldStartIdx > oldEndIdx
成立,表示newCh
还没有处理完,[newStartIdx , newEndIdx]
范围内节点需要添加到对应的位置,即前一个节点之前。 - 只有
newStartIdx > newEndIdx
成立,表示表示oldCh
还没有处理完,需要移除[newStartIdx , newEndIdx]
范围内节点。
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) } }
-