ホームページ > ウェブフロントエンド > Vue.js > vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

青灯夜游
リリース: 2022-11-10 20:55:43
転載
1479 人が閲覧しました

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

vue2 diff プロセス

  • 比較方法: 同じレベルの比較、クロスレベル比較なし
#次のソース コードは

vue/patch.ts からのもので、いくつかの抜粋があり、関連する機能へのリンクが添付されます。 [関連する推奨事項: vuejs ビデオ チュートリアル Web フロントエンド開発 ]

パッチ関数

  • diff プロセスでは、patch 関数を呼び出し、古いノードと新しいノードを比較し、比較しながら実際の DOM にパッチを適用します。 関数: ソース コード アドレス:
  • パッチ関数
  • isUndef() 関数isDef() 関数emptyNodeAt function
      return function patch(oldVnode, vnode, hydrating, removeOnly) {
          if (isUndef(vnode)) {  //新的节点不存在
              if (isDef(oldVnode)) //旧的节点存在
              invokeDestroyHook(oldVnode)   //销毁旧节点
              return
           }
             .........
          //isRealElement就是为处理初始化定义的,组件初始化的时候,没有oldVnode,那么Vue会传入一个真实dom
          if (!isRealElement && sameVnode(oldVnode, vnode)) { -----判断是否值得去比较
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) ---打补丁,后面会详细讲
          } else {
            ......
             if (isRealElement) 
             ......
              oldVnode = emptyNodeAt(oldVnode) //转化为Vnode,并赋值给oldNode
            }
            // replacing existing element
            const oldElm = oldVnode.elm      ----找到oldVnode对应的真实节点
            const parentElm = nodeOps.parentNode(oldElm)  ------找到它的父节点
            createElm(.....) --------创建新节点
            ....递归地去更新节点
        return vnode.elm
      }
    ログイン後にコピー

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

sameNode function

    sameNode
  • がその中に表示されますパッチを提供する価値があるかどうかを判断するには、パッチを提供する価値があるかどうかを判断し、価値がない場合は、上記の手順に従って置き換えてください。この関数のソース コード アドレスを見つけてみましょう:
  • sameNode function
  • function sameVnode(a, b) {
      return (
            a.key === b.key &&  ----------------------key值相等, 这就是为什么我们推荐要加上key,可以让判断更准确
        a.asyncFactory === b.asyncFactory && 
        ((a.tag === b.tag && ---------------------标签相等
          a.isComment === b.isComment && ---------是否为注释节点
          isDef(a.data) === isDef(b.data) &&  ----比较data是否都不为空
          sameInputType(a, b)) ||  ---------------当标签为input的时候,需要比较type属性
          (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
      )
    }
    ログイン後にコピー
彼にパッチを適用する価値がある場合は、
    patchVNode
  • 関数を入力してください
patchVNode

ソース コード アドレス:
    patchVNode 関数
  • この関数は少し長いため短縮されました
  •   function patchVnode(...
      ) {
        if (oldVnode === vnode) {  //两个节点一致,啥也不用管,直接返回
          return
        }
        ....
        if (
        //新旧节点都是静态节点,且key值相等,则明整个组件没有任何变化,还在之前的实例,赋值一下后直接返回
          isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
        const oldCh = oldVnode.children  //获取旧节点孩子
        const ch = vnode.children //获取新节点孩子
        if (isUndef(vnode.text)) { //新节点没有文本
          if (isDef(oldCh) && isDef(ch)) {  //旧节点孩子和新节点孩子都不为空
            if (oldCh !== ch) //旧节点孩子不等于新节点孩子
              updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) //重点----比较双方的孩子进行diff算法 
          } else if (isDef(ch)) {  //新节点孩子不为空,旧节点孩子为空
             ....
            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) //设置为新节点的文本
        }
      }
    ログイン後にコピー
次の関数がありますここでは多くの判断が行われるため、フローチャートも追加しました

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

updateChildren (diff アルゴリズムの実施形態)

ソース コード アドレス:
    updateChildren 関数
  • ここでは 4 段階のプロセスが使用されています。コードを分割しました。
初期化
採用された 4 つの手順 ポインタはそれぞれ 4 つのノードを指します
    • oldStartIdx
    • newStartIdx は古いノード ヘッドを指します。新しいノードの先頭、初期値は 0
    • oldEndIdx
    • newEndIdx は古いノードの末尾、新しいノードの末尾、初期値を指しますは長さ 1
    • ##
          let oldStartIdx = 0 //旧头指针
          let newStartIdx = 0 //新头指针
          let oldEndIdx = oldCh.length - 1  //旧尾指针
          let newEndIdx = newCh.length - 1 //新尾指针
          let oldStartVnode = oldCh[0] //旧头结点
          let oldEndVnode = oldCh[oldEndIdx] //旧尾结点
          let newStartVnode = newCh[0] //新头结点
          let newEndVnode = newCh[newEndIdx]  //新尾结点
      ログイン後にコピー

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)4 つの比較 - ループ内

#Oldヘッドと新しいヘッド
    古いテールと新しいテール
  • 古いヘッドと新しいテール
  • 古いテールと新しいヘッド
  • 注: ここで 1 つ打てる限り
再起動
、どれも打てない場合は次のリンクに移動してください。 、 1 つずつ判断し続けるのではなく

  function updateChildren(){
  ·....
   //好戏从这里开始看
   //只要满足 旧头指针<=旧尾指针 同时  新头指针<= 新尾指针 -- 也可以理解为不能交叉
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    //这里进行一个矫正,是应该在循环的过程中,如果进入key表查询的话复用后会将旧节点置空(后面会说),所以这里会对其进行一个处理
      if (isUndef(oldStartVnode)) {  //旧头结点为空
        oldStartVnode = oldCh[++oldStartIdx] // 往右边走
      } else if (isUndef(oldEndVnode)) {  //旧尾结点为空
        oldEndVnode = oldCh[--oldEndIdx] //往左边走
    //step1
      } else if (sameVnode(oldStartVnode, newStartVnode)) {  //比较旧头和新头,判断是否值得打补丁
        patchVnode(...) //打补丁
        oldStartVnode = oldCh[++oldStartIdx]  //齐头并进向右走
        newStartVnode = newCh[++newStartIdx]  //齐头并进向右走
    //step2
      } else if (sameVnode(oldEndVnode, newEndVnode)) {  //比较旧尾和新尾, 判断是否值得打补丁
        patchVnode(...) //打补丁
        oldEndVnode = oldCh[--oldEndIdx]  //齐头并进向左走
        newEndVnode = newCh[--newEndIdx]  //齐头并进向左走
   //step3
      } else if (sameVnode(oldStartVnode, newEndVnode)) { //比较旧头和新尾,判断是否值得打补丁
        patchVnode(...) //打补丁
        //补完移动节点
        canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]  //旧头向右走
        newEndVnode = newCh[--newEndIdx] //新尾向左走
    //step4
      } else if (sameVnode(oldEndVnode, newStartVnode)) {  //比较旧尾和新头,判断是否值得打补丁
        patchVnode(...) //打补丁
        //补完移动节点
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]  //旧尾向左走
        newStartVnode = newCh[++newStartIdx] //新头向右走
      }
ログイン後にコピー
vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)上記のランダムな例を取り上げて練習してください

ステップ 1、ステップ 2

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)step3、

step4 (ヒット)
##
  • 在step4进行处理,移动节点到正确位置(插在旧头的前面)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)
  • 旧尾向左走,新头向右走vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)
  • 处理完后就重开,从step1开始,到step2再次命中,此时oldEndInxnewEndInx齐头并进向左走(注意这里是不用去移动节点的哦)(左), 然后重开,在step2再次命中...(右)

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 重开, 这次在step3命中,然后将旧头结点结点的真实节点插在旧尾结点的后面,到这里其实真实节点就已经是我们所期望的了

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 上述处理完后,旧头向右走,新尾向左走,命中step1,新头和旧头都向左走,出现交叉情况,至此退出循环

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 通过上面这个例子,我们把四种情况都命中了一下(一开始随便画的图没想到都命中了哈哈哈),也成功通过复用节点将真实结点变为预期结果,这里便是双端diff一个核心体现了
  • 但是如果四种情况都没有命中的呢(如图下)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)
  • 则会走向我们最后一个分支,也就是后面介绍的列表寻找vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)
列表寻找-循环中
function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key
  const map = {}  //初始化一个对象
  for (i = beginIdx; i <= endIdx; ++i) { //从头到尾
    key = children[i].key  //提取每一项的key
    if (isDef(key)) map[key] = i  //key不为空的时候,存入对象,键为key,值为下标
  }
  return map  //返回对象
}
//所以该函数的作用其实就是生成了一个节点的键为key,值为下标的一个表
ログイン後にコピー
  function findIdxInOld(node, oldCh, start, end) {
  //其实就是进行了一个遍历的过程
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      if (isDef(c) && sameVnode(node, c)) return i  //判断是否有值得打补丁的节点,有则返回
    }
  }
ログイン後にコピー
  • 进入正文
 let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
 ....
else {
        if (isUndef(oldKeyToIdx))
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //传入的是旧节点孩子,所以生成了一个旧节点孩子的key表
          //使用三目运算符--- 这里也是要使用key的原因,key有效的话可以通过表获取,无效的话则得进行比遍历比较
        idxInOld = isDef(newStartVnode.key)  //判断新头结点的key是否不为空--是否有效
          ? oldKeyToIdx[newStartVnode.key]  //不为空的的话就到key表寻找该key值对象的旧节点的下标
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //遍历寻找旧节点数组中是否有和新头结点值得打补丁的节点,有的话则赋值其下标给idxInOld(不通过key)
        if (isUndef(idxInOld)) {  //发现找不到了就直接创建新真实节点
          createElm(...)
        } else { //找到了
          vnodeToMove = oldCh[idxInOld] //找到该下标对应的节点
          if (sameVnode(vnodeToMove, newStartVnode)) { //进行一个比较判断是否值得打补丁
            patchVnode(...) //打补丁 
            oldCh[idxInOld] = undefined  //置空,下次生成表就不会把它加进去
            canMove &&nodeOps.insertBefore( parentElm, vnodeToMove.elm,oldStartVnode.elm ) //移动节点
          } else {
          //不值得打补丁,创建节点
            createElm(...)
          }
        }
        newStartVnode = newCh[++newStartIdx]  //新头指针向前一步走
      }
    } //--- while循环到这里
ログイン後にコピー
  • 看完源码其实可以总结一下,就是前面四个都没有命中后,就会生成旧节点孩子key
  • 新头节点的key有效的话,就拿新头节点的key去旧节点的key表找,找不到就创建新的真实节点, 找得到的话就判断是否值得打补丁,值得的话就打补丁后复用节点,然后将该旧节点孩子值置空,不值得就创建新节点
  • 新头节点的key无效的话,则去遍历旧节点数组挨个进行判断是否值得打补丁,后续跟上述一样
  • 新头指针向前一步走

也使用一下上面的例子运用一下这个步骤,以下都为key有效的情况vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

(重新放一下图,方便看)

  • 生成了一个旧节点的key表(key为键,值为下标), 然后newStartVnodekey值为B,找到旧节点孩子该节点下标为1,则去判断是否直接打补丁,值得的话将该旧节点孩子置空再在A前面插入B

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

右图的表中B没有变为undefined是因为表示一开始就生成的,在下次进入循环的时候生成的表才会没有B

  • 然后将新头向右走一步,然后重开,发现前四步依旧没有命中,此时新头结点为B,但是生成的旧节点表没有B,故创建新的节点,然后插入

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 新头继续向右走,重开,命中step1(如图左), 之后新头和旧头齐头并进向右走, 此时,旧头指向的undefined(图右),直接向右走,重开

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 发现此时又都没有命中, 此时也是生成一个key表,发现找不到,于是创建新节点M插入

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 然后新头继续向前走,依旧都没有命中,通过key表去寻找,找到了D,于是移动插入,旧节点孩子的D置空,同时新头向前一步走

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 走完这一步其实就出现交叉情况了,退出循环,此时如下图,你会发现,诶,前面确实得到预期了,可是后面还有一串呢

vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

  • 别急,这就来处理
处理
    if (oldStartIdx > oldEndIdx) { //旧的交叉了,说明新增的节点可能还没加上呢
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(....) //新增
    } else if (newStartIdx > newEndIdx) {  //新的交叉了,说明旧节点多余的可能还没删掉呢
      removeVnodes(oldCh, oldStartIdx, oldEndIdx) //把后面那一段删掉
    }
ログイン後にコピー
  • 对于上面这个例子,就是把后面那一段,通过遍历的方式,挨个删除vue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)

到这里updateChildren函数就结束喽,自己推导一下节点的变化就会很清晰啦

(学习视频分享:编程基础视频

以上がvue2 diff アルゴリズムを 1 つの記事で理解します (画像付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
vue
ソース:juejin.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート