从Vue2源码看diff算法

Posted 一腔诗意醉了酒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Vue2源码看diff算法相关的知识,希望对你有一定的参考价值。

文章目录


基于Talk is cheap. Show me the code原则,本文会有大量的源码,不感兴趣的可以直接跳到答案区。

学习目标

通过看[Vue2.x](Vue_src_code/vue at master · csDeng/Vue_src_code (github.com))的源码, 学习Vnode以及diff算法,达到解决面试难题

学习过程

环境准备

  • 修改scripts命令
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
  • yarn dev进行打包(注意安装rollup

查看Vnode长什么样

    1. src\\core\\vdom\\vnode.js中的VNode类的constructor里面打印this,观察什么是Vnode,删减代码如下:
export default class VNode 
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) 
    console.log('Vnode class', this)
  

    1. 重新打包
    1. 编写测试html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <script src='../dist/vue.js'></script>
    </head>
    <body>
        <div id = 'demo'>
            <h1>虚拟dom</h1>
            <p id='p1'>foo</p>
        </div>
    
        <script>
            const app = new Vue(
                el:'#demo',
                data:
                    foo:'foo'
                ,
                mounted()
                    setTimeout(()=>
                        this.foo = 'foooooooo'
                    ,3000)
                
            )
        </script>
    </body>
    </html>
    
    1. 浏览器中打开,查看结果

    1. 打开其中一个节点看看具体结构

    1. 可以看到其实Vnode就是把我们的html标签转化成一个js对象.
    2. Vdom就是一个又一个Vnode构成的

测试Vue的批量异步更新的

  • 环境准备

注释掉前面在vnode里加的注释,重新打包,避免不必要的干扰

  • 测试html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='../dist/vue.js'></script>
</head>
<body>
    <div id = 'demo'>
        <h1>异步更新</h1>
        <p id='p1'>foo</p>
    </div>

    <script>
        const app = new Vue(
            el:'#demo',
            data:
                foo:'ready'
            ,
            mounted()
                /* 说明批量异步更新的代码*/
                setInterval(()=>
                    this.foo = Math.random()
                    console.log('1', this.foo)
                    this.foo = Math.random()
                    console.log('2',this.foo)
                    this.foo = Math.random()
                    console.log('3', this.foo)

                    // 异步行为,此时内容没变
                    console.log(p1.innerHTML)
                    this.$nextTick(()=>
                         console.log('$nextTick', p1.innerHTML)
                    )
                ,3000)
            
        )
    </script>
</body>
</html>
  • 浏览器打开观察结果

Diff算法

  • 源码位置src\\core\\vdom\\patch.js

  • 进行diff的入口源码

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

    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)
    

    // 判断是否是元素, 没有文本则是Element
    if (isUndef(vnode.text)) 

      // 都有孩子
      if (isDef(oldCh) && isDef(ch)) 
        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)
    
  

总结一下

比较两个VNode, 包括三种操作: 属性更新、文本更新、子节点更新

具体规则:

  1. 新老节点均有子节点,则对子节点进行diff操作,调用updateChildren
  2. 如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点
  3. 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点
  4. 当新老节点都无子节点的时候,只是文本替换
  • updateChildren, 进行diff的具体细节,源码如下:
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)
    

    /**
     * 两边向中间靠拢,dfs
     */
    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)) 
        // 新旧的开始节点相同,同时+1
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

       else if (sameVnode(oldEndVnode, newEndVnode)) 
        // 新旧的结束节点相同, 同时-1
        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 
        // 4种猜想都没有找到相同的,才被迫进行循环查找

        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        
        // 查找在老的数组中的索引key
        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 
            // 只有key相同,但是内容不相同
            // 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)
    
  

  • addVnodes批量增加节点的操作
  function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) 
    for (; startIdx <= endIdx; ++startIdx) 
      // 批量创建
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    
  
  • removeVnodes批量删除旧的废弃节点
  function removeVnodes (vnodes, startIdx, endIdx) 
    for (; startIdx <= endIdx; ++startIdx) 
      const ch = vnodes[startIdx]
      if (isDef(ch)) 
        if (isDef(ch.tag)) 
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
         else  // Text node
          removeNode(ch.elm)
        
      
    
  

  • 比较两个节点是否完全一致的源码
function以上是关于从Vue2源码看diff算法的主要内容,如果未能解决你的问题,请参考以下文章

Vue2.x源码系列08Diff算法原理

vue2源码简单实现stage3

你怎么理解vue中的diff算法?

vue中diff算法处理新旧节点的流程

Vue源码学习之虚拟DOM——Vue中的DOM-Diff (上)

十分详细的diff算法原理解析