一、什么是虚拟dom?
- 虚拟 DOM(Virtual DOM) ,是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫虚拟 DOM
// 真实DOM
<ul id='ulist'>
<li id='first'>这是第一个List元素</li>
<li id='second'>这是第二个List元素</li>
</ul>
// 虚拟DOM
var element = {
element: 'ul',
props: {
id:"ulist"
},
children: [
{ element: 'li', props: { id:"first" }, children: ['这是第一个List元素'] },
{ element: 'li', props: { id:"second" }, children: ['这是第二个List元素'] }
]
}
二、为什么要使用虚拟dom
-
手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作的复杂会提升
-
为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
-
为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是虚拟 DOM 出现了
-
虚拟 DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM, 虚拟dom 内部将弄清楚如何有效(diff)的更新 DOM
-
虚拟 DOM 可以维护程序的状态,跟踪上一次的状态,这样可以更好的维护视图和状态的关系,在复杂视图情况下提升渲染性能
-
虚拟 DOM 除了可以渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等
三、虚拟DOM库 Snabbdom
- Vue 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
- 大约 200 SLOC(single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的虚拟 DOM 之一
四、Snabbdom 基本使用
- 初始化项目,安装打包工具 parcel
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json
npm init -y
- 配置 scripts,配置启动项目命令,和打包项目命令
// --open 指的是自动打开浏览器
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
}
- 创建项目目录结构
│ index.html
│ package.json
└─src
01-basicusage.js
- 安装 Snabbdom
npm install snabbdom@2.1.0
- 导入 Snabbdom 的两个核心函数
- init() 是一个高阶函数,返回 patch()
- h() 返回虚拟节点 VNode
- package.json 中的 exports 字段主要是用来做路径的映射,方便设置子路径
- 因为 parcel 和 webpakc5 之前都不支持 package.json 中的 exports 字段,所以这里要补全路径
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
- Snabbdom 案例一
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// h() 函数主要是用来生成 vnode
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,(也可以是真实 DOM 元素,patch 会把他转成 VNode)
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'Hello Snabbdom')
// patch 的作用是对比新旧两个 vnode,将差异更新到真实 DOM 元素上
patch(oldVnode, vnode)
- Snabbdom 案例二
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// 第二个参数:如果是数组就是标签中的子元素
let vnode = h('div#container', [
h('h1', 'Hello Snabbdom'),
h('p', '这是一个p')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// 两秒之后更改 div 中的内容
vnode = h('div#container', [
h('h1', 'Hello World'),
h('p', 'Hello P')
])
patch(oldVnode, vnode)
// 两秒之后删除 div 中的内容
patch(oldVnode, h('!'))
}, 2000);
五、Snabbdom 模块
-
Snabbdom 的核心库是不能处理 DOM 元素的属性、样式、事件......但是我们可以通过注册 Snabbdom 默认提供的模块来实现
-
Snabbdom 中的模块是可以用来拓展 Snabbdom 的功能
-
Snabbdom 中的模块的实现是通过注册全局的狗子函数来实现的
-
Snabbdom 官方提供的常用模块:
-
attributes
- 设置 DOM 元素的属性,使用
setAttribute() - 处理布尔类型的属性
- 设置 DOM 元素的属性,使用
-
props
- 和
attributes模块相似,设置 DOM 元素的属性element[attr] = value - 不处理布尔类型的属性
- 和
-
class
- 切换类样式
- 注意:给元素设置类样式是通过
sel选择器
-
dataset
- 设置
data-*的自定义属性
- 设置
-
eventlisteners
- 注册和移除事件
-
style
- 设置行内样式,支持动画
- delayed/remove/destroy
-
-
Snabbdom 模块使用步骤:
- 导入需要的模块
- init() 中注册模块
- 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
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 patch = init([
styleModule,
eventListenersModule
])
// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
h('p', { on: { click: eventHandler } }, 'Hello P')
])
function eventHandler() {
console.log('别点我,疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
六、Snabbdom 源码解析
主要分析 Snabbdom 的核心源码
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- init() 设置模块,创建 patch()
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM 树上
6-1 h()
-
h()函数作用主要是用来创建 vnode 对象,和 Vue 中的h()函数类似
-
Snabbdom 中的h()函数是用ts写的,用了函数重载的概念
-
函数重载
- 参数个数或类型不同的函数
- JavaScript 中没有重载的概念
- TypeScript 中有重载,不过重载的实现还是通过代码调整参数 重载的示意 当add传两个参数时候,就会调用第一个方法,当给add传三个参数的时候会调用第二个方法,这就是函数重载
function add (a: number, b: number) {
console.log(a + b)
}
function add (a: number, b: number, c: number) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
h()函数源码
// h 函数的重载
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
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) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) {
data = b
}
if (is.array(c)) {
// 如果 c 是数组
children = c
} else if (is.primitive(c)) {
// 如果 c 是字符串或者数字
text = c
} else if (c && c.sel) {
// 如果 c 是 VNode
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
// 如果 b 是字符串或者数字
text = b
} else if (b && b.sel) {
// 如果 b 是 VNode
children = [b]
} else { data = b }
}
if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number,创建文本节点
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] === '#')
) {
// 如果是 svg,添加命名空间
addNS(data, children, sel)
}
// 返回 VNode
return vnode(sel, data, children, text, undefined)
};
6-2 VNode
VNode 就是虚拟节点,是一个用来描述 DOM 元素的 JavaScript 对象
export interface VNode {
// 选择器
sel: string | undefined;
// 节点数据:属性/样式/事件等
data: VNodeData | undefined;
// 子节点,和 text 只能互斥
children: Array<VNode | string> | undefined;
// 记录 vnode 对应的真实 DOM
elm: Node | undefined;
// 节点中的内容,和 children 只能互斥
text: string | undefined;
// 优化用
key: Key | undefined;
}
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 }
}
6-3 patch 整体过程分析
-
patch(oldVnode, newVnode)
-
patch 又俗称打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
-
对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
-
如果不是相同节点,删除之前的内容,重新渲染
-
如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
-
如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
-
diff 过程只进行同层级比较
6-4 init()
-
init(modules, domApi) 主要作用是返回 patch() 函数,他是一个高阶函数
-
为什么要使用高阶函数?
- 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
- 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
-
init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中
init() 函数源码
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}
// 初始化 api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
// 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
// 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ... ]
for (i = 0; i < hooks.length; ++i) {
// cbs['create'] = []
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
// const hook = modules[0]['create']
const hook = modules[j][hooks[i]]
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook)
}
}
}
……
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
……
}
}
6-5 patch()
-
patch()功能是将传入新旧 VNode,对比差异,把差异渲染到 DOM,返回新的 VNode,作为下一次 patch() 的 oldVnode
-
patch()执行过程
-
首先执行模块中的钩子函数
-
如果 oldVnode 和 vnode 相同(key 和 sel 相同),调用 patchVnode(),找节点的差异并更新 DOM,
-
如果 oldVnode 是 DOM 元素(首次渲染),把 DOM 元素转换成 oldVnode,然后调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm,把刚创建的 DOM 元素插入到 parent 中,移除老节点,触发用户设置的 create 钩子函数
-
patch() 源码
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
// 保存新插入节点的队列,为了触发钩子函数
const insertedVnodeQueue: VNodeQueue = []
// 执行模块的 pre 钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
if (!isVnode(oldVnode)) {
// 把 DOM 元素转换成空的 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 如果新旧节点是相同节点(key 和 sel 相同)
if (sameVnode(oldVnode, vnode)) {
// 找节点的差异并更新 DOM
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// 如果新旧节点不同,vnode 创建对应的 DOM
// 获取当前的 DOM 元素
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
// 触发 init/create 钩子函数,创建 DOM
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 移除老节点
removeVnodes(parent, [oldVnode], 0, 0)
}
}
// 执行用户设置的 insert 钩子函数
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
// 执行模块的 post 钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}
6-6 createElm()
-
createElm()作用是把 vnode 转化为真实 dom 元素,并且把 dom 元素存储在 vnode 对象的 elm 属性中,然后返回创建好的真实 dom ,这里并没有把创建的 dom 元素挂载到 dom 树上
-
createElm()执行过程:
- 首先触发用户设置的 init 钩子函数
- 如果选择器是!,创建注释节点
- 如果选择器为空,创建文本节点
- 如果选择器不为空,解析选择器,设置标签的 id 和 class 属性,执行模块的 create 钩子函数
- 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
- 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
- 执行用户设置的 create 钩子函数
- 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
createElm() 源码
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
// 执行用户设置的 init 钩子函数
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) {
// 如果选择器不为空
// 解析选择器
// Parse selector
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, ' '))
// 执行模块的 create 钩子函数
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 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)) {
// 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
api.appendChild(elm, api.createTextNode(vnode.text))
}
const hook = vnode.data!.hook
if (isDef(hook)) {
// 执行用户传入的钩子 create
hook.create?.(emptyNode, vnode)
if (hook.insert) {
// 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
insertedVnodeQueue.push(vnode)
}
}
} else {
// 如果选择器为空,创建文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
// 返回新创建的 DOM
return vnode.elm
}
6-7 patchVnode()
-
patchVnode() 是 patch() 中调用的方法,主要对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
-
patchVnode() 执行过程
-
首先执行用户设置的 prepatch 钩子函数
-
执行 create 钩子函数,首先执行模块的 create** 钩子函数,然后再执行用户设置的 create 钩子函数
-
如果 vnode.text 未定义,就判断是否 oldVnode.children 和 vnode.children 是否都有值,然后调用 updateChildren(),使用 diff 算法对比子节点,更新子节点
-
如果 vnode.children 有值,oldVnode.children 无值,则清空 DOM 元素,调用 addVnodes(),批量添加子节点
-
如果 oldVnode.children 有值,vnode.children 无值,就调用 removeVnodes(),批量移除子节点
-
如果 oldVnode.text 有值,清空 DOM 元素的内容
-
如果设置了 vnode.text 并且和 oldVnode.text 不等,如果老节点有子节点,全部移除,设置 DOM 元素的 textContent 为 vnode.text
-
最后执行用户设置的 postpatch 钩子函数
-
patchVnode() 源码
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
// 首先执行用户设置的 prepatch 钩子函数
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// 如果新老 vnode 相同返回
if (oldVnode === vnode) return
if (vnode.data !== undefined) {
// 执行模块的 update 钩子函数
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 执行用户设置的 update 钩子函数
vnode.data.hook?.update?.(oldVnode, vnode)
}
// 如果 vnode.text 未定义
if (isUndef(vnode.text)) {
// 如果新老节点都有 children
if (isDef(oldCh) && isDef(ch)) {
// 调用 updateChildren 对比子节点,更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
// 如果新节点有 children,老节点没有 children
// 如果老节点有text,清空dom 元素的内容
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 批量添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果老节点有children,新节点没有children
// 批量移除子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老节点有 text,清空 DOM 元素
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果没有设置 vnode.text
if (isDef(oldCh)) {
// 如果老节点有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 设置 DOM 元素的 textContent 为 vnode.text
api.setTextContent(elm, vnode.text!)
}
// 最后执行用户设置的 postpatch 钩子函数
hook?.postpatch?.(oldVnode, vnode)
}
6-8 updateChildren()
-
updateChildren() 是 diff 算法的核心,主要是用来对比新旧节点的 children,更新 DOM
-
updateChildren() 执行过程:
要对比两棵树的差异,如果取第一棵树的每一个节点依次和第二课树的每一个节点比较,这样的时间复杂度为 O(n^3),而且在 DOM 操作的时候很少很少会把一个父节点移动/更新到某一个子节点,因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)
在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
在对开始和结束节点比较的时候,总共有四种情况
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
情况1、2:
oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同),就调用 patchVnode() 对比和更新节点,把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
情况3:
oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
就调用 patchVnode() 对比和更新节点,把 oldStartVnode 对应的 DOM 元素,移动到右边 - 更新索引
情况4:
oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
调用 patchVnode() 对比和更新节点, 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新索引
-
如果不是以上四种情况,就遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点,如果没有找到,说明 newStartNode 是新节点,创建新节点对应的 DOM 元素,插入到 DOM 树中,如果找到了,判断新节点和找到的老节点的 sel 选择器是否相同,如果不相同,说明节点被修改了,重新创建对应的 DOM 元素,插入到 DOM 树中,如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
-
循环结束之后,如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
-
循环结束之后,如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
updateChildren() 源码
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)) {
// 1. 比较老开始节点和新的开始节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 2. 比较老结束节点和新的结束节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 3. 比较老开始节点和新的结束节点
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
// 4. 比较老结束节点和新的开始节点
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 开始节点和结束节点都不相同
// 使用 newStartNode 的 key 再老节点数组中找相同节点
// 先设置记录 key 和 index 的对象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 如果是新的vnode
if (isUndef(idxInOld)) { // New element
// 如果没找到,newStartNode 是新节点
// 创建元素插入 DOM 树
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新旧节点的选择器不同
// 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果相同,patchVnode()
// 把 elmToMove 对应的 DOM 元素,移动到左边
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 重新给 newStartVnode 赋值,指向下一个新节点
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)
}
}
}