Rumah  >  Artikel  >  hujung hadapan web  >  Analisis mendalam tentang prinsip algoritma diff dalam vue2.x

Analisis mendalam tentang prinsip algoritma diff dalam vue2.x

青灯夜游
青灯夜游ke hadapan
2022-08-15 19:54:212050semak imbas

Algoritma diff ialah algoritma cekap yang membandingkan nod pokok pada tahap yang sama, mengelakkan keperluan untuk mencari dan melintasi pokok lapisan demi lapisan. Artikel ini akan memberi anda analisis mendalam tentang prinsip algoritma diff dalam vue2.x saya harap ia akan membantu anda!

Analisis mendalam tentang prinsip algoritma diff dalam vue2.x

Saya telah membaca banyak artikel analisis kod sumber dan membaca kod sumber sekurang-kurangnya dua kali. Lagipun, saya masih mahu menulisnya sendiri sebagai rekod dan kajian untuk diri saya sendiri. Fokus pada ulasan dan ringkasan, jangan terlalu risau tentang yang lain Anda akan mendapat lebih banyak dengan menyemak proses dan ulasan dengan membandingkan ringkasan dengan kod sumber

. Kemas kini definisi kaedah

Selepas fungsi pemaparan dijana, kaedah pelekap akan dipanggil, dan pengiraan perbezaan akan dilakukan semasa pemasangan, kerana tiada nod maya yang lama nod dom sebenar akan dibandingkan dengan nod maya buat kali pertama Sebagai perbandingan, memandangkan nod maya bukan nod asli, operasi penggantian akan dilakukan buat kali pertama. (Belajar perkongsian video: tutorial video vue)

// /src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode // 当前render函数产生的虚拟节点,保存后以便下次做对比
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) //初次渲染
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
   ...
  }

Dua cabang utama algoritma diff

Ibu utama akan menjadi dua utama cawangan: Nod maya hadapan dan belakang adalah konsisten dan nod maya hadapan dan belakang tidak konsisten

// /src/core/vdom/patch.js
export function createPatchFunction (backend) {
  ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ...
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        ...// 前后虚拟节点一致的方法
      } else {
        ...// 前后虚拟节点不一致的方法
      }
  }
}

Nod maya hadapan dan belakang tidak konsisten

Ia adalah dibahagikan kepada tiga langkah: 1. Buat nod baharu, 2. Kemas kini nod pemegang tempat Induk, 3. Padamkan nod lama
Kedua-duanya berbeza apabila memasang komponen buat kali pertama, ia akan dinilai jika ia ialah dom sebenar, ia akan ditukar menjadi nod maya dan digantikan

if (isRealElement) {
  ...
  //需要diff 所以将第一次的真实节点转换成虚拟节点
  oldVnode = emptyNodeAt(oldVnode) //<div id="app"></div>
}
// 拿到父类的dom节点
const oldElm = oldVnode.elm //app
const parentElm = nodeOps.parentNode(oldElm) // body
//创建新dom节点 内部包含组件逻辑
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
//更新父的占位符节点 (组件更新相关)
if (isDef(vnode.parent)) {
  // 在生成render函数时会生成占位符节点<Dialog>提示</Dialog> => <div>提示</div> <Dialog></Dialog>就是占位符节点
  let ancestor = vnode.parent
  // 判断是否可挂载
  const patchable = isPatchable(vnode)
  while (ancestor) {
    for (let i = 0; i < cbs.destroy.length; ++i) {
      cbs.destroy[i](ancestor)
    }
    //更新父占位符的element
    ancestor.elm = vnode.elm
    if (patchable) {
      ...
    } else {
      registerRef(ancestor)
    }
    ancestor = ancestor.parent
  }
}
// 删除旧节点
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

Nod maya hadapan dan belakang adalah konsisten

  • Tentukan dahulu sama ada nod baharu ialah teks, jika ya, tetapkan teks terus, jika tidak, teruskan tentukan
  • nod baharu dan lama Kedua-duanya mempunyai anak, perbandingan mendalam (titik utama)
  • Nod baharu nod mempunyai anak, nod lama tidak, tambah nod baru dalam gelung
  • Nod baru tidak mempunyai anak, nod lama mempunyai anak, padamkan nod lama secara langsung
function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
    const elm = vnode.elm = oldVnode.elm

    let i
    const data = vnode.data 
    // 是组件vnode,在组件更新会调用组件的prepatch方法
    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)
    }
    // 是否是text
    if (isUndef(vnode.text)) {
      // 新旧节点都有children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      // 新有 老没有 children 循环创建新节点
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      // 新没有 老有 children 直接删除老节点
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
      // 新老都没有 children 老的是文本 就置为空
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, &#39;&#39;)
      }
    // 是text 直接设置文本
    } 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)
    }
  }

Perbandingan status kanak-kanak bagi kedua-dua nod lama dan baharu

// /src/core/vdom/patch.js
 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]
      } else if (isUndef(oldEndVnode)) {// 是否定义老节点结束元素
        oldEndVnode = oldCh[--oldEndIdx]
        // 头(旧节点开始元素)头(新节点开始元素)对比 例如四个li,末尾新增一个li,这种情况头头对比性能高
      } else if (sameVnode(oldStartVnode, newStartVnode)) { // sameVnode判断key和tag是否相同
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾对比 例如四个li,头部新增一个li,这种情况尾尾对比性能高
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {// 头尾对比 节点反转优化 reverse
        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)) { // 尾头对比
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else { // 乱序对比(核心diff,其他方式为优化)
        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, 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 {
            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)
    }
  }

Nota

  • kunci tidak boleh digunakan dalam beberapa kes Indeks, kerana indeks sebelum dan selepas perubahan adalah sama Apabila menambah elemen di kepala, jika anda menggunakan indeks sebagai kunci, ralat kemas kini akan berlaku sebagai menambah elemen pada akhir (kerana kunci sebelum dan lagipun 0)
  • Dalam pelbagai situasi perbandingan, selagi kedua-duanya didapati sama, kanak-kanak akan dibandingkan secara rekursif
  • Dalam perbandingan tidak teratur, kuncinya memainkan peranan yang besar. Tanpa kunci, ralat kemas kini akan berlaku dan kesan penggunaan semula tidak dapat dicapai
  • Perbandingan beza ialahkedalaman dahulu, dan perbandingan lapisan yang sama

Ringkasan

Apabila dipasang, templat akan dikemas kini selepas melalui algoritma diff Buat pertama kalinya, nod dom sebenar akan dibandingkan dengan nod maya yang dihasilkan, dan nod maya yang dihasilkan. akan disimpan untuk kemas kini dan perbandingan seterusnya. Algoritma diff hanya perlu dibahagikan kepada dua cabang, nod maya hadapan dan belakang adalah konsisten dan nod maya hadapan dan belakang tidak konsisten. Apabila nod maya hadapan dan belakang tidak konsisten, nod baharu akan dibuat, pemegang tempat induk akan dikemas kini dan nod lama akan dipadamkan. Jika nod lama ialah nod sebenar, tukarkannya kepada nod maya, dapatkan nod induk nod lama dan gantikan nod lama. Apabila nod maya hadapan dan belakang adalah konsisten, ia akan terlebih dahulu menentukan sama ada nod baharu ialah teks, dan jika nilai ditambah secara langsung, jika tidak bandingkan atribut dahulu, dan kemudian tentukan sama ada nod baharu mempunyai anak dan nod lama mempunyai tiada anak, maka anak nod baru akan ditambah secara langsung Jika nod baru mempunyai anak, maka nod baru akan ditambah secara langsung Jika nod lama dan baru mempunyai anak, penunjuk berganda akan digunakan untuk membandingkan lapisan yang sama, melalui perbandingan kepala-ke-kepala, perbandingan ekor-ke-ekor, perbandingan kepala-ke-ekor, perbandingan ekor-ke-kepala, dan perbandingan di luar susunan secara berterusan menilai dan mengemas kininya untuk memaksimumkan penggunaan nod lama yang berlebihan, ia akan ditambah, dan nod lama yang berlebihan akan dipadamkan dikembalikan dan disimpan sebagai nod lama yang akan datang untuk dibandingkan.

  • Perbandingan head-to-head
    Jika elemen permulaan baharu dan lama adalah vnod yang sama, panggil kaedah patchVnode secara rekursif untuk perbandingan mendalam, dan kemudian alihkan indeks ke elemen seterusnya
  • Perbandingan ekor-ke-ekor
    Jika elemen baharu dan lama adalah Jika elemen akhir adalah vnod yang sama, panggil kaedah patchVnode secara rekursif untuk perbandingan mendalam, dan kemudian alihkan indeks ke elemen sebelumnya
  • Bandingkan kepala dan ekor
    Bandingkan elemen permulaan nod lama dengan elemen ekor nod lama Jika ia adalah sama, ulangi Panggil kaedah patchVnode untuk perbandingan mendalam, kemudian gerakkan yang lama elemen nod ke penghujung, alihkan penuding kepala nod lama ke yang seterusnya, dan alihkan penuding ekor nod baharu ke yang sebelumnya. Contohnya, lama: A, B, C, baharu: C, B, A. Untuk perbandingan pertama, alihkan A lama ke belakang C
  • Bandingkan ekor dan kepala
    Gerakkan ekor elemen nod lama ke elemen permulaan nod lama Untuk perbandingan, panggil kaedah patchVnode secara rekursif untuk perbandingan mendalam Kemudian gerakkan elemen nod lama ke hadapan, alihkan penuding ekor nod lama ke yang sebelumnya. dan alihkan penuding kepala nod baharu ke nod seterusnya. Contohnya, lama: A, B, C, baharu: C, B, A, D. Untuk perbandingan pertama, alihkan C lama ke hadapan A
  • Perbandingan luar pesanan
    Sebelum perbandingan, kunci dan yang sepadan Indeks nod lama akan dijana ke dalam jadual pemetaan. Semasa perbandingan di luar pesanan, kunci semasa akan digunakan untuk mencari kunci nod lama Jika ia boleh digunakan semula, nod akan dialihkan ke permulaan nod lama dan anak-anak akan dibandingkan secara rekursif ia tidak boleh digunakan semula, yang baru akan dibuat dan dimasukkan ke dalam yang lama pada permulaan nod. Kemudian alihkan indeks baharu ke elemen seterusnya

(Perkongsian video pembelajaran: pembangunan bahagian hadapan web, Video pengaturcaraan asas)

Atas ialah kandungan terperinci Analisis mendalam tentang prinsip algoritma diff dalam vue2.x. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:juejin.cn. Jika ada pelanggaran, sila hubungi admin@php.cn Padam