虚拟dom
简介
- 虚拟dom就是用js对象来描述一个dom节点
- vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的操作中难免要操作dom,而操作真是dom又是非常耗费性能的,因为浏览器把dom设计的非常复杂
- 所以我们要尽量少的操作dom,不要盲目的去更新视图,而是通过对比数据变化前后的状态,计算出视图中那些地方需要更新,只更新需要更新的地方
- 通过js模拟一个dom节点,称为虚拟dom节点·,当数据发生变化时,对比变化前后的虚拟dom节点,通过diff算法计算出需要更新的地方,然后再去更新视图
接下来,我们就来看一下vue中如何实现虚拟dom
VNode类
虚拟dom就是通过这个类实例出来的,也就是说,我们可以通过这个类实例化不同的虚拟dom节点
虚拟dom节点其实就是一个对象,而这个类的作用就是在这个对象上添加各种属性,其源码如下:
export default class VNode {
tag?: string
data: VNodeData | undefined
children?: Array<VNode> | null
text?: string
elm: Node | undefined
ns?: string
context?: Component // rendered in this component's scope
key: string | number | undefined
componentOptions?: VNodeComponentOptions
componentInstance?: Component // component instance
parent: VNode | undefined | null // component placeholder node
// strictly internal
raw: boolean // contains raw HTML? (server only)
isStatic: boolean // hoisted static node
isRootInsert: boolean // necessary for enter transition check
isComment: boolean // empty comment placeholder?
isCloned: boolean // is a cloned node?
isOnce: boolean // is a v-once node?
asyncFactory?: Function // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext?: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions?: ComponentOptions | null // for SSR caching
devtoolsMeta?: Object | null // used to store functional render context for devtools
fnScopeId?: string | null // functional scope id support
constructor(
tag?: string, // 当前节点的标签名
data?: VNodeData, // 当前节点对应的对象,包含了一些具体的一些数据对象
children?: Array<VNode> | null, // 当前节点的子节点组成的一个数组
text?: string, // 当前节点的文本
elm?: Node, // 当前虚拟节点对应的真实dom节点
context?: Component, // 当前组件节点对应的vue实例
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key // 节点的key属性,被当做节点的标志,后续diff算法就是通过这个属性判断是否是同一个节点
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false // /*是否为注释节点*/
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance
}
}
vnode类包含了虚拟dom需要的一些类属性,一下几个属性是比较重要的:
- tag:属性的标签名
- data:节点包含的信息,比如class类名等
- children:节点包含的子节电,是一个数组
- text:节点包含的文本属性
- elm:虚拟节点对应的真实don节点
VNode类型
不同的真实dom对应不同的虚拟dom类型,并拥有不同的属性,其中,就有一下几种类型:
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件节点
- 克隆节点
VNode的作用
我们在视图渲染之前,把写好的template
模板先编译成VNode
并缓存下来,等到数据发生变化页面需要重新渲染的时候,我们把数据发生变化后生成的VNode
与前一次缓存下来的VNode
进行对比,找出差异,然后有差异的VNode
对应的真实DOM
节点就是需要重新渲染的节点,最后根据有差异的VNode
创建出真实的DOM
节点再插入到视图中,最终完成一次视图更新。
vue中的diff算法
这个过程就是通过新旧vnode对比,找出差异所在
整体过程:
- 首先,通过pathc方法比对,判断是否是同一个节点,如果不是同一个节点,就删除旧的节点,插入新的节点。
- 如果新旧节点是同一个节点,就调用patchVnode方法,比对同一个节点
- 在patchVnode中,判断新旧节点是否有children属性,没有的话过程就比较简单,有的话就要调用updateChildren方法
- 在updateChildren方法中会进行diff算法的比对
接下来就来看看各个过程的实现
patch
path方法以新的vnode为基准,改造就的vnode节点使之和新的vnode节点一样,主要做一下内容:
- 创建节点:新的
VNode
中有而旧的oldVNode
中没有,就在旧的oldVNode
中创建。 - 删除节点:新的
VNode
中没有而旧的oldVNode
中有,就从旧的oldVNode
中删除。 - 更新节点:新的
VNode
和旧的oldVNode
中都有,就以新的VNode
为准,更新旧的oldVNode
。
具体过程:
-
先判断老节点是否是一个虚拟节点,如果不是的话,先要转化为虚拟节点,才能进行比对
-
判断新老节点是否是同一个节点,判断的依据是新老节点的tag属性和key属性是否相同,如果相同就是同一个节点,不同则不是同一个节点
-
如果是同一个节点,就要执行patchVnode方法进行同一个节点的比对
-
如果不是同一个节点的话,就需要插入新的节点,并删除旧的节点
-
简易代码实现
export default function patch(oldVnode, newVnode) { // 判断传入的第一个参数,是DOM节点还是虚拟节点? if (oldVnode.sel == '' || oldVnode.sel == undefined) { // 传入的第一个参数是DOM节点,此时要包装为虚拟节点 oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } // 判断oldVnode和newVnode是不是同一个节点 if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) { console.log('是同一个节点'); patchVnode(oldVnode, newVnode); } else { console.log('不是同一个节点,暴力插入新的,删除旧的'); let newVnodeElm = createElement(newVnode); // 插入到老节点之前 if (oldVnode.elm.parentNode && newVnodeElm) { oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); } // 删除老节点 oldVnode.elm.parentNode.removeChild(oldVnode.elm); } };
在这里,有个createElement方法,是用来创建新节点的,要注意一点的是,如果新节点有children属性的话,还需要依次创建子节点,并插入到新节点中,具体代码如下:
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
// console.log('目的是把虚拟节点', vnode, '真正变为DOM');
// 创建一个DOM节点,这个节点现在还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是有文本??
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 它内部是文字
domNode.innerText = vnode.text;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内部是子节点,就要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
// 得到当前这个children
let ch = vnode.children[i];
// 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
let chDOM = createElement(ch);
// 上树
domNode.appendChild(chDOM);
}
}
// 补充elm属性
vnode.elm = domNode;
// 返回elm,elm属性是一个纯DOM对象
return vnode.elm;
};
patchVnode
当新旧节点是同一个节点的时候,就需要调用这个方法比对同一个节点
静态节点
静态节点就是只包含纯文字的节点,比如<p>123456</p>
,这个节点没有包含任何可变的变量,也就是说,不管数据怎么变化,这个节点只要渲染后,以后就永远不会改变了,任何数据变化都与它无关。所以pathVnode方法在碰到静态节点的时候,会直接跳过,无需处理
patchVnode在比对新旧同一个节点的时候,会经过以下过程:
-
如果新旧节点是同一个节点的话,直接返回
-
如果vnode和oldvnode均为静态节点的话,直接返回,因为此时新旧节点是同一个不会改变的节点!
-
如果vnode是文本节点,则判断oldvnode节点的text属性是否相等,如果不相等的话,则直接将oldvnode的内容更改为vnode的文本,这个时候就不需要知道oldvnode里面包含了什么了
-
如果vnode是元素节点
-
vnode包含子节点
- 如果oldvnode也包含子节点,就需要调用updateChildren方法进行diff算法的比对了
- 如果oldVnode不包含子节点,则直接清空oldvnode节点的内容,再将vnode的子节点添加在oldvnode对应的真实dom节点上
-
vnode不包含子节点
如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧节点之前里面都有啥,直接清空即可。
-
-
简易版代码如下:
// 对比同一个虚拟节点 export default function patchVnode(oldVnode, newVnode) { // 判断新旧vnode是否是同一个对象 if (oldVnode === newVnode) return; // 判断新vnode有没有text属性 if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) { // 新vnode有text属性 console.log('新vnode有text属性'); if (newVnode.text != oldVnode.text) { // 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。 oldVnode.elm.innerText = newVnode.text; } } else { // 新vnode没有text属性,有children console.log('新vnode没有text属性'); // 判断老的有没有children if (oldVnode.children != undefined && oldVnode.children.length > 0) { // 老的有children,新的也有children,此时就是最复杂的情况。 updateChildren(oldVnode.elm, oldVnode.children, newVnode.children); } else { // 老的没有children,新的有children // 清空老的节点的内容 oldVnode.elm.innerHTML = ''; // 遍历新的vnode的子节点,创建DOM,上树 for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]); oldVnode.elm.appendChild(dom); } } } }
对应源码:
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = (vnode.elm = oldVnode.elm)
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, 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)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (__DEV__) {
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)
}
}
updateChildren
在这个方法中,需要比对新旧节点的子节点,如果直接双层for循环判断新节点每一个子节点是否存在于旧节点的子节点中,效率就太低了,所以在vue中借鉴了diff算法进行比对,通过比较在特殊位置的新旧节点的子节点,如下:
- 先把
newChildren
数组里的所有未处理子节点的第一个子节点和oldChildren
数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作; - 如果不同,再把
newChildren
数组里所有未处理子节点的最后一个子节点和oldChildren
数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作; - 如果不同,再把
newChildren
数组里所有未处理子节点的最后一个子节点和oldChildren
数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren
数组里的该节点移动到与newChildren
数组里节点相同的位置; - 如果不同,再把
newChildren
数组里所有未处理子节点的第一个子节点和oldChildren
数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren
数组里的该节点移动到与newChildren
数组里节点相同的位置; - 最后四种情况都试完如果还不同,那就按照之前循环的方式来查找节点。
为了达到上面所说的比对过程,需要定义四个指针分别指向newChildren和oldChildren的首尾位置,并通过上面四个规则去移动指针
newChildren
数组里的所有未处理子节点的第一个子节点称为:新前;newChildren
数组里的所有未处理子节点的最后一个子节点称为:新后;oldChildren
数组里的所有未处理子节点的第一个子节点称为:旧前;oldChildren
数组里的所有未处理子节点的最后一个子节点称为:旧后;
上面的比对规则,可以归纳为以下四种比较,从上往下如果命中哪种规则后,对应的指针就会移动位置
- 新前与旧前
- 新后与旧后
- 新后与旧前
- 新前与旧后
接下来,就详细介绍比对的过程
-
首先,定义四个指针指向新老children首尾位置,并定义四个变量存放对应位置的节点
-
首先比对"新前与旧前"和"新后与旧后",如果命中了,对应指针变换位置,注意,命中之后,要调用patchVnode方法比对同一个节点
else if (checkSameVnode(oldStartVnode, newStartVnode)) { // 新前和旧前 console.log('新前和旧前命中'); patchVnode(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (checkSameVnode(oldEndVnode, newEndVnode)) { // 新后和旧后 console.log('新后和旧后命中'); patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; }
-
如果前两种都没有命中的话,就要判断"新后与旧前","新前与旧后"了,如果这两种命中的话,就证明元素的位置发生了变化,需要移动对应oldchildren节点位置,具体移动位置如下:
- 如果命中"新后与旧前"的话,证明元素得往后移动了,注意,移动的时候要以未处理节点为基准,所以要把节点移动到旧后对应节点(此时还未处理该节点)的后面
- 如果命中"新前与旧后"的话,证明元素被提前了,此时需要将元素移动到新后对应节点的前面
else if (checkSameVnode(oldStartVnode, newEndVnode)) { // 新后和旧前 console.log('新后和旧前命中'); patchVnode(oldStartVnode, newEndVnode); // 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面 // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动 parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(oldEndVnode, newStartVnode)) { // 新前和旧后 console.log('新前和旧后命中'); patchVnode(oldEndVnode, newStartVnode); // 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面 parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动 oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; }
-
如果四种规则都没有命中的话,就要遍历oldchildren,判断新前对应的节点是否存在于oldchildren中,这里可以利用哈希表,就不用每次都遍历一遍了
- 如果找到的话,就插入到旧前对应节点前面,同时指针移动位置,注意此时需要将oldchildren对应子节点的元素设置为undefined,表示该节点已经处理过了
- 如果没有找到的话,创建节点并插入
else { // 四种命中都没有命中 // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。 if (!keyMap) { keyMap = {}; // 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象 for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key; if (key != undefined) { keyMap[key] = i; } } } console.log(keyMap); // 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号 const idxInOld = keyMap[newStartVnode.key]; console.log(idxInOld); if (idxInOld == undefined) { // 判断,如果idxInOld是undefined表示它是全新的项 // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点 parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm); } else { // 如果不是undefined,不是全新的项,而是要移动 const elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode); // 把这项设置为undefined,表示我已经处理完这项了 oldCh[idxInOld] = undefined; // 移动,调用insertBefore也可以实现移动。 parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); } // 指针下移,只移动新的头 newStartVnode = newCh[++newStartIdx]; }
-
基于上一种没有命中规则的情况可能出现oldchildren元素为undefined,所以需要首先就判断元素是否为undefined,是的话直接移动指针位置
-
循环执行上面的操作,直到新前位置大于旧前或者旧前位置大于旧后位置,表示某一个数组已经全部遍历一遍了
-
接着判断是由于哪个条件不成立退出循环了,也就是哪一个数组还有元素还没有遍历,如果oldchildren还有元素的话,证明这些元素是新的vnode中没有的子节点,所以需要依次删除;如果newchildren还有元素,则证明新的vnode增加了新的元素,也需要依次插入对应位置,此时要插入旧前对应节点的前面
注意,这里要插入到旧前节点的前面,原因是:此时旧后在旧前的前面(退出循环的条件),也就是包括旧后与旧前这些元素的位置都确定了,所以插入只能在旧前与旧后之间,因为是从新前位置开始遍历还没有插入的子节点,如果一次从旧后节点插入的话,新插入元素位置会发生变化(逆序),而依次插入到旧前的前面就不会
// 继续看看有没有剩余的。循环结束了start还是比old小 if (newStartIdx <= newEndIdx) { console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前'); // 遍历新的newCh,添加到老的没有处理的之前 for (let i = newStartIdx; i <= newEndIdx; i++) { // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。 // newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); } } else if (oldStartIdx <= oldEndIdx) { console.log('old还有剩余节点没有处理,要删除项'); // 批量删除oldStart和oldEnd指针之间的项 for (let i = oldStartIdx; i <= oldEndIdx; i++) { if (oldCh[i]) { parentElm.removeChild(oldCh[i].elm); } } }
-
对应源码如下:
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 (__DEV__) {
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)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
vnodeToMove = oldCh[idxInOld]
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)
}
}
自此,虚拟dom与diff算法就写完了,还有很多不足的地方,后面会继续完善!!
参考链接:
https://vue-js.com/learn-vue/virtualDOM/
https://www.bilibili.com/video/BV1iX4y1K72v?spm_id_from=333.1007.top_right_bar_window_view_later.content.click&vd_source=8bcb7d684da1204ddf081fb53feb2a84