虚拟dom
- 虚拟dom解决了什么问题
- 首先是正常的一个真实dom拥有的属性非常多,还拥有很多dom操作的方法
- 其次数据更新的时候如果整个画面重新渲染会带来很大的性能开销,非常慢,而且很多没变化的部分都属于无用功,还不能保存数据更新前的状态
- 用新数据生成的虚拟dom跟上次旧的虚拟dom做对比,只更新发生变化的部分
- diff算法也是消耗性能的,所以如果我们知道要修改那个dom,直接手动操作应该是最快的,这样做是为了让我们更关注数据的变化,而不需要关心dom操作
- Vue中虚拟dom分类:
注释节点
文本节点
克隆节点(代表本节点是克隆来的)
元素节点
组件节点
函数式组件节点
- VNode作用,就是将template编译成VNode缓存下来,数据变化生成的VNode与之前的VNode树作对比,将有差异的渲染成真实的Dom插入到视图中,最终一次性视图更新
diff思路
- 思考:有一组新节点一组旧节点,这个时候数组跟数组间for循环对比时间复杂度是n^2,假如每个节点又有子节点这个复杂度就非常大了,因此不应该做完全对比
- 只修改变动的地方不做整个布局的调整
- 只做同层比较,不会跨级比较,算法是一个O(n)的算法
- 不值得比较的节点直接用新的替换旧的,如果两个节点一样才会去比较其子节点,假如第一层不一样就用新的替换旧的(注意diff算法从来不是最优解,只是一个时间跟节点利用率的平衡方案)
以下代码均在src/core/vdom/patch.js中
patch(oldVnode, vnode) 函数
- 根据界面生成Vnode,然后用这个新的Vnode对比旧的Vnode,用新的Vnode去更新真实的dom树
- 根据Vnode递归创建真实dom节点,从上到下,先创建子节点后插到父节点上,vnode.elm为真实dom
- 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
- 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
- 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode
updateChildren子节点算法
- 新前跟旧前作对比,如果节点相同就做patchVnode的操作
- 新后跟旧后作对比,如果节点相同就做patchVnode的操作
-
旧前跟新后做对比,如果相同就patchVnode这两个节点,然后把真实dom旧前节点移动到oldChilren中所有未处理节点之后,这里做的是真实dom操作
-
旧后跟先前做对比,如果相同就patchVnode这两个节点,然后把旧后节点移动到oldChilren中所有未处理节点之前
- 假如不属于以上任何一种,就在旧节点中查找当前节点,找不到就创建新节点,找到了假如相同就更新节点,相同key不同元素就当初新节点处理
- 如果oldStartIdx > oldEndIdx,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
- 如果newStartIdx > newEndIdx,把[oldStartIdx, oldEndIdx]之间的所有节点都删除
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
debugger
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)) {
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)
}
}
patchVnode
功能是根据根据新的Vnode去更新旧的VNode,把dom属性,class事件啥的都一一同步,让旧的节点跟新的节点一样
由此编码上的注意点
- 尽量不要跨层级的修改dom
- 设置key可以最大化的利用节点,同一层不要设置两个key相同
- 不同的节点不要设置相同的key
- 不要盲目相信diff的效率,在必要时可以手工优化