1.patchVnode 节点比较算法
1.1执行逻辑
- 依据新老vnode的关系,以老得vnode为基础,渲染成新的vnode效果。把老的dom做操作变成新的dom
- 两个节点的子列表数据比较,规则:同层比较 ,深度优先
- 新老各有 头游标 和 尾游标 ,头对象 和 尾对象,每次移动都实时更新
let oldStartIdx = 0 //旧游标开始 往右递增
let newStartIdx = 0 //新游标开始 往右递增
let oldEndIdx = oldCh.length - 1 //旧游标结束 往左递减
let newEndIdx = newCh.length - 1 //新游标结束 往左递减
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
-
通过 左右游标往中间执行的方式判断 新旧首尾是否相同节点,
-
都没找到才用新节点循环所有老节点比较
-
最后比较结束的条件是 新的或者老得 头游标 和 尾游标 已经大于或者小于为结束
//由于是往中间靠拢,所以游标最终移动后,要么是左边移动多点,要么右边移动多点。
//当新老都存在 游标先超过另一边,则结束循环
//可以理解 oldStartIdx开始为0 ,一直递增, oldEndIdx开始为10,一直递减。
//那么迟早会发生oldStartIdx > oldEndIdx的情况,newStartIdx 和 newEndIdx 同理
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
}
- 把比较剩下的做 新增或者删除操作
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
1.2节点比较逻辑
核心比较顺序
新老各有 头游标 和 尾游标 ,头对象 和 尾对象,往中心移动,当新的和老的数组:开始游标大于结束游标结束移动。
1.2.1两头比较
1.2.2两尾比较
1.2.3头尾比较
1.2.4尾头比较
1.2.5剩下节点处理
当老的数组先结束,证明需要做新增
当新的数组先结束,证明需要做删除
1.2更新流程
注意这里做的是具体的dom操作,只是利用新旧的vnode找出更新内容,直接做dom操作。每次循环都会直接做dom,性能消化大,所以需要加上key做唯一判断提高性能。
- 属性更新
- 文本更新
- 子节点更新
1.3源码分析
//核心比较方法 单个节点
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
//调用对应的钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
//获取当前两个新旧vnode的子节点
const oldCh = oldVnode.children
const ch = vnode.children
//先更新属性
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
//判断新节点是否 没有文本(相当于处理有子节点的情况)
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {//判断新旧都有子节点,核心比较方法 updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {//老节点是文本,新节点是有子节点
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {//当前节点是文本,并且大家内容不一样,则更新
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
//比较两个虚拟节点是否一样
function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
//是否是相同的输入类型
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
//最终比较的意义是:依据新老vnode的关系,以老得vnode为基础,渲染成新的vnode效果。把老的dom做操作变成新的dom
//1.两个节点的子列表数据比较,规则:同层比较 ,深度优先
//2.新老各有 头游标 和 尾游标 ,头对象 和 尾对象,每次移动都实时更新
//3.通过 左右游标往中间执行的方式判断 新旧首尾是否相同节点
//4.都没找到才用新节点循环所有老节点比较
//5.最后比较结束的条件是 新的或者老得 头游标 和 尾游标 已经大于或者小于为结束
//6.把比较剩下的做 新增或者删除操作
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
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, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//如果两个新老节点 头相同直接patch,同时游标一起右移
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
//如果两个新老节点 尾巴相同直接patch,同时游标一起左移
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
//交叉处理,老开始和新结束patch,老的移动到最尾巴
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))//这里把老的开始dom移动到老的结束dom后边位置,实时执行的dom操作
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
//交叉处理,老结束和新开始patch,老的移动到最开始
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
//这里把老的结束dom移动到老的开始dom后边位置,实时执行的dom操作
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//如果首尾都没找到,则拿新的给每一个老的从左到右比较,找到就patch,同时按新的位置移动老节点
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
//idxInOld 老节点不为空
if (isUndef(idxInOld)) { // New element
//在老的里面没有找到,直接新增节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 找到了
vnodeToMove = oldCh[idxInOld]
//如果是相同的节点,进行patch,由于是在新虚拟dom列表从左往右比较,所以插入到排头的位置
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}
//如果不相同元素,也按新增操作
else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
//新节点不断开始有右移
newStartVnode = newCh[++newStartIdx]
}
}
//所有节点都比较完,新老的数组一定有一个先结束
//剩下的做新增或者删除动作
if (oldStartIdx > oldEndIdx) {//证明老的数组先结束,说明新的数组有剩下,批量新增
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {//证明新的数组先结束,说明老的数组有剩下,批量删除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
2.如何实现跨平台 patch
//编译入口
//src/platforms/web/runtime/index.js
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
//通过工厂返回需要的patch方法
//nodeOps和modules 是平台特有的方法和函数
//src/platforms/web/runtime/patch.js
//dom操作 ,各种节点增删改
import * as nodeOps from 'web/runtime/node-ops'
//这里使用core上面的createPatchFunction方法做工厂
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
//src/platforms/web/runtime/modules/index.js
//当前方法导出二维数组
import attrs from './attrs'
import klass from './class'
import events from './events'
import domProps from './dom-props'
import style from './style'
import transition from './transition'
export default [
attrs,
klass,
events,
domProps,
style,
transition
]
//src/platforms/web/runtime/modules/attrs.js
export default {
create: updateAttrs,
update: updateAttrs
}
//src/platforms/web/runtime/modules/class.js
export default {
create: updateClass,
update: updateClass
}
//src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
//这里接受平台特性的方法做dom操作
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
//modules定义了上面定义的二维数组
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
//cbs['create'] = [] // 保留多个方法
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
//modules[j][hooks[i] 相当于 attrs['create'] 或者 events['create']
//把所有相关的方法都保留到cbs数组里,供下次使用
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
...
}
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
//这里判断如果需要打补丁 遍历上面存好的所有更新方法 所有方法都执行
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
}