用过vue、React的同学一定都听过Virtual DOM(虚拟dom)及diff算法,它们给人的感觉就是深奥难懂。但大厂面试vdom(以下Virtual DOM都简称为vdom)、diff算法是绕不过去的一道坎。今天为大家揭开vdom、diff算法的神秘面纱。。。
什么是vdom
vdom是一种使用js对象来描述真实DOM的技术,通过这种技术,我们能精确知道哪些真实DOM改变了,从而尽量减少DOM操作的性能开销。
snabbdom.js
vdom是通过snabbdom.js库实现的,大概过程有以下三步:
- compile(把真实DOM编译成Vnode)
- diff(利用diff算法,比较oldVnode和newVnode之间有什么变化)
- patch(把这些变化用打补丁的方式更新到真实dom上去)
接下来为大家详细介绍snabbdom的两个核心函数h()、patch()
snabbdom - h 函数
h( ) 函数主要根据传进来的参数,返回一个 vnode(虚拟节点) 对象
h(‘<标签名>’, {…属性…}, ‘值’),如果值为子元素,则可以在h()函数中嵌套h(‘<标签名>’, {…属性…}, […子元素…])。
snabbdom - patch 函数
patch()函数的两种使用
- patch(container, vnode) //将虚拟dom渲染成真实的dom
简易的实现原理
- patch(vnode, newVnode) //利用diff算法比较新旧vnode之间的差异
简易的实现原理
diff算法
什么是diff算法?
diff算法很早就存在了,一开始diff算法是用来计算出两个文本的差异。所以大家一定要明确,diff算法并不是react或者vue原创的,它们只是用diff算法来比较两个vnode的差异,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。
vdom中是在patch(vnode, newVnode)比较新旧函数时会用到diff
以下是patch()函数的核心代码分析
function patch (oldVnode, vnode) {
......
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
.......
return vnode
}
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
这个函数做了以下事情:
- 找到对应的真实dom,称为el
- 判断Vnode和oldVnode是否完全相同,如果是,那么直接return
- 如果他们都有文本节点并且不相等,则更新el的文本节点
- 如果oldVnode有子节点而Vnode没有,则删除el的子节点
- 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点
updateChildren 函数比较复杂,感兴趣的同学可以了解下。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
// oldVnode起始位置小于结束位置并且newVnode起始位置小于结束位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// isUndef 用来判断对象是否等于undefined或者为空,是的话返回true
if (isUndef(oldStartVnode)) {
// oldVnode 起始位置oldS++
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// oldVnode 结束位置oldE--
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// oldS和newS相同,不变化,进行patch,oldS++,newS++
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// oldE和newE相同,不变化,进行patch,oldE--,newE--
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// oldS和newE相同,oldS移动到oldE之后,进行patch,oldS++,newE--
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
// oldE和newS相同,oldE移动到oldS之前,进行patch,oldE--,newS++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 全都不相同情况下
// 获取oldVnode->index的key
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
// oldVnode->index为undefined或null,说明没有该元素,创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
// 获取oldVnode
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
// 创建的Vnode和newS相同,插入到oldS之前,进行patch
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// 相同的key但是不一样的element. 被视为新的element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 当oldS>oldE时,将newS至newE间的全部插入
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 当newS>newE,将oldS至oldE间的全部删除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
都看到这了,就点个赞、点个关注吧,有疑问处可留言
读书不觉已春深,一寸光阴一寸金