vue:虛擬dom的patch

源碼目錄:src/core/vdom/patch.jsvue

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

       const canMove = !removeOnly

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 開始索引大於結束索引,進不了
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode已經被移走了。
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx] // 索引加1。是去對比下一個節點。好比以前start=a[0],那如今start=a[1],改變start的值後再去對比start這個vnode
        newStartVnode = newCh[++newStartIdx]
         
      } else if (sameVnode(oldEndVnode, newEndVnode)) { 
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// 把節點b移到樹的最右邊
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
          
      } else if (sameVnode(oldEndVnode, newStartVnode)) {   old.end.d=new.start.d
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// Vnode moved left,把d移到c的左邊。=old.start->old.end
        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)) { 
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 建立新節點,後面執行了nodeOps.insertBefore(parent, elm, ref)
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            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)
          }
        }
        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(parentElm, oldCh, oldStartIdx, oldEndIdx) // 刪除舊的c,removeNode(ch.elm)

    }
  }
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

/**
     * 比較新舊vnode節點,根據不一樣的狀態對dom作合理的更新操做(添加,移動,刪除)整個過程還會依次調用prepatch,update,postpatch等鉤子函數,在編譯階段生成的一些靜態子樹,在這個過程
     * @param oldVnode 中因爲不會改變而直接跳過比對,動態子樹在比較過程當中比較核心的部分就是當新舊vnode同時存在children,經過updateChildren方法對子節點作更新,
     * @param vnode
     * @param insertedVnodeQueue
     * @param removeOnly
     */
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }

    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
    }

      // 用於靜態樹的重用元素。
        // 注意,若是vnode是克隆的,咱們只作這個。
        // 若是新節點不是克隆的,則表示呈現函數。
        // 由熱重加載api從新設置,咱們須要進行適當的從新渲染。
    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 (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, 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)
    }
  }

function insertBefore (parentNode, newNode, referenceNode) {
  parentNode.insertBefore(newNode, referenceNode);
}

/**
     *
     * @param vnode根據vnode的數據結構建立真實的dom節點,若是vnode有children則會遍歷這些子節點,遞歸調用createElm方法,
     * @param insertedVnodeQueue記錄子節點建立順序的隊列,每建立一個dom元素就會往隊列中插入當前的vnode,當整個vnode對象所有轉換成爲真實的dom 樹時,會依次調用這個隊列中vnode hook的insert方法
     * @param parentElm
     * @param refElm
     * @param nested
     */

     let inPre = 0
  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    vnode.isRootInsert = !nested // 過渡進入檢查
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          inPre++
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(
            config.ignoredElements.length &&
            config.ignoredElements.some(ignore => {
              return isRegExp(ignore)
                ? ignore.test(tag)
                : ignore === tag
            })
          ) &&
          config.isUnknownElement(tag)
        ) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        inPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (ref.parentNode === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

function removeVnodes (parentElm, 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)
        }
      }
    }
  }

updateChildren方法主要經過while循環去對比2棵樹的子節點來更新dom,經過對比新的來改變舊的,以達到新舊統一的目的。node

經過一個例子來模擬一下:
假設有新舊2棵樹,樹中的子節點分別爲a,b,c,d等表示,不一樣的代號表明不一樣的vnode,如:api

clipboard.png

在設置好狀態後,咱們開始第一遍比較,此時oldStartVnode=a,newStartVnode=a;命中了sameVnode(oldStartVnode,newStartVnode)邏輯,則直接調用patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)方法更新節點a,接着把oldStartIdxnewStartIdx索引分別+1,如圖:數據結構

clipboard.png

更新完節點a後,咱們開始第2遍比較,此時oldStartVnode=b,newEndVnode=b;命中了sameVnode(oldStartVnode,newEndVnode)邏輯,則調用patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)方法更新節點b,接着調用canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)),把節點b移到樹的最右邊,最後把oldStartIdx索引+1,newEndIdx索引-1,如圖:app

clipboard.png

更新完節點b後,咱們開始第三遍比較,此時oldEndVnode=d,newStartVnode=d;命中了sameVnode(oldEndVnode, newStartVnode)邏輯,則調用patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)方法更新節點d,接着調用canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm),把d移到c的左邊。最後把oldEndIdx索引-1,newStartIdx索引+1,如圖:dom

clipboard.png

更新完d後,咱們開始第4遍比較,此時newStartVnode=e,節點e在舊樹裏是沒有的,所以應該被做爲一個新的元素插入,調用createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm),後面執行了nodeOps.insertBefore(parent, elm, ref)方法把e插入到c以前,接着把newStartIdx索引+1,如圖:async

clipboard.png

插入節點e後,咱們能夠看到newStartIdx已經大於newEndIdx了,while循環已經完畢。接着調用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) 刪除舊的c,最終如圖:ide

clipboard.png

updateChildren經過以上幾步操做完成了舊樹子節點的更新,實際上只用了比較小的dom操做,在性能上有所提高,而且當子節點越複雜,這種提高效果越明顯。vnode經過patch方法生成dom後,會調用mounted hook,至此,整個vue實例就建立完成了,當這個vue實例的watcher觀察到數據變化時,會兩次調用render方法生成新的vnode,接着調用patch方法對比新舊vnode來更新dom.函數

相關文章
相關標籤/搜索