Vue源码阅读之diff算法

虚拟dom

  1. 虚拟dom解决了什么问题
  • 首先是正常的一个真实dom拥有的属性非常多,还拥有很多dom操作的方法
  • 其次数据更新的时候如果整个画面重新渲染会带来很大的性能开销,非常慢,而且很多没变化的部分都属于无用功,还不能保存数据更新前的状态
  • 用新数据生成的虚拟dom跟上次旧的虚拟dom做对比,只更新发生变化的部分
  • diff算法也是消耗性能的,所以如果我们知道要修改那个dom,直接手动操作应该是最快的,这样做是为了让我们更关注数据的变化,而不需要关心dom操作
  1. Vue中虚拟dom分类:
注释节点 文本节点 克隆节点(代表本节点是克隆来的) 元素节点 组件节点 函数式组件节点

  1. VNode作用,就是将template编译成VNode缓存下来,数据变化生成的VNode与之前的VNode树作对比,将有差异的渲染成真实的Dom插入到视图中,最终一次性视图更新
diff思路
  • 思考:有一组新节点一组旧节点,这个时候数组跟数组间for循环对比时间复杂度是n^2,假如每个节点又有子节点这个复杂度就非常大了,因此不应该做完全对比
  1. 只修改变动的地方不做整个布局的调整
  2. 只做同层比较,不会跨级比较,算法是一个O(n)的算法
  3. 不值得比较的节点直接用新的替换旧的,如果两个节点一样才会去比较其子节点,假如第一层不一样就用新的替换旧的(注意diff算法从来不是最优解,只是一个时间跟节点利用率的平衡方案)
以下代码均在src/core/vdom/patch.js中
patch(oldVnode, vnode) 函数
  1. 根据界面生成Vnode,然后用这个新的Vnode对比旧的Vnode,用新的Vnode去更新真实的dom树
  2. 根据Vnode递归创建真实dom节点,从上到下,先创建子节点后插到父节点上,vnode.elm为真实dom
  3. 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  4. 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  5. 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode
updateChildren子节点算法
Vue源码阅读之diff算法
文章图片
image
  1. 新前跟旧前作对比,如果节点相同就做patchVnode的操作
  2. 新后跟旧后作对比,如果节点相同就做patchVnode的操作
  3. 旧前跟新后做对比,如果相同就patchVnode这两个节点,然后把真实dom旧前节点移动到oldChilren中所有未处理节点之后,这里做的是真实dom操作

    Vue源码阅读之diff算法
    文章图片
    image
  4. 旧后跟先前做对比,如果相同就patchVnode这两个节点,然后把旧后节点移动到oldChilren中所有未处理节点之前

    Vue源码阅读之diff算法
    文章图片
    image
  5. 假如不属于以上任何一种,就在旧节点中查找当前节点,找不到就创建新节点,找到了假如相同就更新节点,相同key不同元素就当初新节点处理
  6. 如果oldStartIdx > oldEndIdx,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
  7. 如果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 // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnlyif (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
【Vue源码阅读之diff算法】功能是根据根据新的Vnode去更新旧的VNode,把dom属性,class事件啥的都一一同步,让旧的节点跟新的节点一样
由此编码上的注意点
  • 尽量不要跨层级的修改dom
  • 设置key可以最大化的利用节点,同一层不要设置两个key相同
  • 不同的节点不要设置相同的key
  • 不要盲目相信diff的效率,在必要时可以手工优化

    推荐阅读