欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

vue中關(guān)于v-for循環(huán)key值問(wèn)題的研究

 更新時(shí)間:2023年06月27日 10:04:24   作者:小白路過(guò)  
這篇文章主要介紹了vue中關(guān)于v-for循環(huán)key值問(wèn)題的研究,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

介紹

關(guān)于key的作用,官方是這樣描述的

? key 的特殊屬性主要用在 Vue 的虛擬 DOM 算法,在新舊 nodes 對(duì)比時(shí)辨識(shí) VNodes。

如果不使用 key,Vue 會(huì)使用一種最大限度減少動(dòng)態(tài)元素并且盡可能的嘗試修復(fù)/再利用相同類型元素的算法。

使用 key,它會(huì)基于 key 的變化重新排列元素順序,并且會(huì)移除 key 不存在的元素。

有相同父元素的子元素必須有獨(dú)特的 key。重復(fù)的 key 會(huì)造成渲染錯(cuò)誤。

之前不知道在哪里看的文章,一直以為使用key是為了就地復(fù)用,但是從上面的描述中可以看到,實(shí)際上不用key,vue也會(huì)盡可能的嘗試使用復(fù)用修復(fù)策略,沒(méi)有設(shè)置key的時(shí)候,vnode的key值就是undefined,而在源碼patchVnode函數(shù)中有一個(gè)判斷函數(shù)sameVnode用于判斷是否是相同節(jié)點(diǎn)。

A.key(undefined )=== B.key(undefined) 

也是判斷為相同節(jié)點(diǎn)的。

/*
  判斷兩個(gè)VNode節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),需要滿足以下條件
  key相同
  tag(當(dāng)前節(jié)點(diǎn)的標(biāo)簽名)相同
  isComment(是否為注釋節(jié)點(diǎn))相同
  是否data(當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的對(duì)象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息)都有定義
  當(dāng)標(biāo)簽是<input>的時(shí)候,type必須相同
*/
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)
  )
}

由此也可以看出,如果要想不復(fù)用,可以通過(guò)給組件以不同的key值來(lái)實(shí)現(xiàn),用于強(qiáng)制替換元素/組件而不是重復(fù)使用它。

作用

既然key值不是用于就地復(fù)用的目的,那為什么要設(shè)置這個(gè)key值呢?

上面說(shuō)使用key值時(shí),提到了重新排列順序及刪除的問(wèn)題。

查看源碼中patch函數(shù)中對(duì)于key值得應(yīng)用,主要看updateChildren中節(jié)點(diǎn)比較部分,在前面的新舊首尾互比中,設(shè)置key和不設(shè)置key的比對(duì)是一樣的,在下面這塊,設(shè)置key的作用就凸顯出來(lái)了

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  ...
  //新舊首尾互比之后
  /*
          生成一個(gè)key與舊VNode的key對(duì)應(yīng)的哈希表(只有第一次進(jìn)來(lái)undefined的時(shí)候會(huì)生成,也為后面檢測(cè)重復(fù)的key值做鋪墊)
          比如childre是這樣的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          結(jié)果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode節(jié)點(diǎn)存在key并且這個(gè)key在oldVnode中能找到則返回這個(gè)節(jié)點(diǎn)的idxInOld(即第幾個(gè)節(jié)點(diǎn),下標(biāo))*/
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode沒(méi)有key或者是該key沒(méi)有在老節(jié)點(diǎn)中找到則創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*獲取同key的老節(jié)點(diǎn)*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            /*如果elmToMove不存在說(shuō)明之前已經(jīng)有新節(jié)點(diǎn)放入過(guò)這個(gè)key的Dom中,提示可能存在重復(fù)的key,確保v-for的時(shí)候item有唯一的key值*/
            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(elmToMove, newStartVnode)) {
            /*如果新VNode與得到的有相同key的節(jié)點(diǎn)是同一個(gè)VNode則進(jìn)行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因?yàn)橐呀?jīng)patchVnode進(jìn)去了,所以將這個(gè)老節(jié)點(diǎn)賦值undefined,之后如果還有新節(jié)點(diǎn)與該節(jié)點(diǎn)key相同可以檢測(cè)出來(lái)提示已有重復(fù)的key*/
            oldCh[idxInOld] = undefined
            /*當(dāng)有標(biāo)識(shí)位canMove實(shí)可以直接插入oldStartVnode對(duì)應(yīng)的真實(shí)Dom節(jié)點(diǎn)前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時(shí)候(比如說(shuō)tag不一樣或者是有不一樣type的input標(biāo)簽),創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
  ...
}

從上面的源碼中可以看到,key值作為快速索引,用于查找當(dāng)前vnode在舊的vnode序列中的位置。

假設(shè):oldVnodes里有

[{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}];

新的vnodes有

[{xx: xx, key: 'key1'}, {xx: xx, key: 'key3'}, {xx: xx, key: ‘key2'}]

在循環(huán)遍歷新的vnodes時(shí),例如key1,在oldVnodes中找到了,但是數(shù)組索引位置(1)和當(dāng)前新的vnodes中的位置(0)不一樣,表示需要進(jìn)行節(jié)點(diǎn)移動(dòng),此時(shí)做的操作

patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)

是用舊的vnode隊(duì)列中key值對(duì)應(yīng)的節(jié)點(diǎn)來(lái)復(fù)用patch生成新的vnode,然后進(jìn)行節(jié)點(diǎn)移動(dòng),節(jié)點(diǎn)移動(dòng)后,會(huì)將oldVnodes中(1)位置清空,置為undefined。

再例如key3節(jié)點(diǎn),在oldVnodes中沒(méi)有,就意味著這是一個(gè)新的節(jié)點(diǎn),需要進(jìn)行新建和插入指定位置的操作。

這里需要注意,即便是sameVnode,也是要進(jìn)行patch操作的,而不是直接拿舊的節(jié)點(diǎn)來(lái)用。

key值在新舊節(jié)點(diǎn)在變化前后順序不一致的情況下,能夠快速的定位,從舊的節(jié)點(diǎn)隊(duì)列中找到具有相同key值得節(jié)點(diǎn)來(lái)復(fù)用渲染。

應(yīng)用場(chǎng)景

日常開(kāi)發(fā)中最常見(jiàn)的用例是結(jié)合 v-for:,開(kāi)發(fā)過(guò)程中vue要求為循環(huán)生成的節(jié)點(diǎn)必須綁定key值

<ul>
  <li v-for="item in items" :key="item.id">...</li>
</ul>

為何不建議用index做key

使用index作為key值,有可能造成錯(cuò)誤的渲染。簡(jiǎn)單的來(lái)說(shuō),假如有oldVnode集合[A, B, C],刪除第一個(gè)元素,剩下的vnode集合就是[B,C],當(dāng)循環(huán)patchVnode的時(shí)候,oldVnode中A的key是0,vnode中B的key也是0,vue在實(shí)際渲染的過(guò)程中就會(huì)復(fù)用A來(lái)渲染B

例如下面根據(jù)list循環(huán)生成三個(gè)子組件,以數(shù)組index作為key值,當(dāng)在刪除的時(shí)候可能會(huì)出現(xiàn)異常。

image-20210611195145197

<div class="list">
     <child v-for="(item, i) in list" :data="item" :key="i" @del="onDelChild" ></child>
</div>
<script>
export default {
  data() {
    return {
      list: [
        { text: "circle", id: 1 },
        { text: "trangle", id: 2 },
        { text: "square", id: 3 }
      ]
    };
  },
  methods: {
    onDelChild(item) {
      const idx = this.list.findIndex(it => it.id == item.id);
      this.list.splice(idx, 1);
    }
  }
};
</script>
<template>
  <div>
    {{ data.id }}
    <input v-model="text" />
    <button @click="onDel">delete</button>
    <span @click="onClick">{{ staticText }}</span>
  </div>
</template>
<script>
export default {
  props: ["data"],
  data() {
    return {
      text: "hello",
      staticText: "click Me"
    };
  },
  methods: {
    onDel() {
      this.$emit("del", this.data);
    },
    onClick() {
      this.staticText = "clicked";
    }
  }
};
</script>

先點(diǎn)擊第一條的click Me,改變文本內(nèi)容。

再點(diǎn)擊第一條的刪除按鈕,結(jié)果如下圖所示,并沒(méi)有如我們期待的那樣刪除第一條數(shù)據(jù),你會(huì)發(fā)現(xiàn)視圖的一部分確實(shí)正確更新了,如前面的id,但是后面的input內(nèi)容和文本內(nèi)容都沒(méi)有按預(yù)期的刪除第一條。

image-20210611195438831

上面的異常情況中,很明顯的用oldVnode集合中的A復(fù)用渲染了vnode中的B,oldVnode中的B復(fù)用渲染了vnode中的C,然后刪除了oldVnode中的C。

注意:上面未正確更新的內(nèi)容,實(shí)際綁定的值是Child子組件內(nèi)部的自有屬性,如果將Child內(nèi)容改為一下,則可以得到預(yù)期效果

<div>
    {{ data.id }}
    <input v-model="data.text" />
    <button @click="onDel">delete</button>
  </div>

初步的結(jié)論是,循環(huán)渲染子組件,并以index作為key值綁定時(shí),當(dāng)動(dòng)態(tài)改變父組件中的list集合,vue會(huì)按index索引來(lái)查找oldVnode中的子節(jié)點(diǎn)用以復(fù)用,且在進(jìn)行patch時(shí),只更新了那些由父組件傳遞過(guò)來(lái)的prop綁定視圖,子組件自身的實(shí)例屬性不會(huì)更新

那么,為什么會(huì)出現(xiàn)這種渲染錯(cuò)誤的情況?

首先,問(wèn)題的源頭肯定是節(jié)點(diǎn)復(fù)用,也就是我們上面key值復(fù)用其中的一步

if (sameVnode(elmToMove, newStartVnode)) {
  /*如果新VNode與得到的有相同key的節(jié)點(diǎn)是同一個(gè)VNode則進(jìn)行patchVnode*/
  patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
  /*因?yàn)橐呀?jīng)patchVnode進(jìn)去了,所以將這個(gè)老節(jié)點(diǎn)賦值undefined,之后如果還有新節(jié)點(diǎn)與該節(jié)點(diǎn)key相同可以檢測(cè)出來(lái)提示已有重復(fù)的key*/
  oldCh[idxInOld] = undefined
  /*當(dāng)有標(biāo)識(shí)位canMove實(shí)可以直接插入oldStartVnode對(duì)應(yīng)的真實(shí)Dom節(jié)點(diǎn)前面*/
  canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
  newStartVnode = newCh[++newStartIdx]
}
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)

在這里面用eleToMove復(fù)用渲染生成newStartVnode時(shí),更新渲染的dom出現(xiàn)了問(wèn)題。

下面我們來(lái)看看這個(gè)錯(cuò)誤是如何出現(xiàn)的

在進(jìn)入patchVnode之前,我們首先需要明確當(dāng)前elmToMove是什么,newStartVnode又是怎樣的。

elmToMove是從舊的渲染隊(duì)列中拿出來(lái)的,由此可以看出它是一個(gè)完成的已經(jīng)渲染過(guò)的數(shù)據(jù),其中包含著數(shù)據(jù)變更前,child子組件對(duì)應(yīng)的實(shí)例及生成的dom等數(shù)據(jù)。

而newStartVnode則不盡然,它只是一個(gè)占位vnode,具體何為占位vnode,可以詳細(xì)了解下之前學(xué)習(xí)過(guò)的vue源碼中create-component這一節(jié)。

對(duì)于<child></child>這種自定義組件,在調(diào)用h函數(shù)生成vnode時(shí),會(huì)生成一個(gè)vue-component-${Ctor.cid}${name}的占位節(jié)點(diǎn),具體可以看源碼中create-component.js中的createComponent方法,這里只截取一部分,來(lái)簡(jiǎn)單說(shuō)明下占位節(jié)點(diǎn)也就是當(dāng)前newStartVnode有哪些內(nèi)容。

/*創(chuàng)建一個(gè)組件節(jié)點(diǎn),返回Vnode節(jié)點(diǎn)*/
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data?: VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  //.....
// return a placeholder vnode
  /** 組件節(jié)點(diǎn),本質(zhì)上是一個(gè)占位節(jié)點(diǎn),在實(shí)際dom中并沒(méi)有
   * 當(dāng)出現(xiàn) <HelloWorld></HelloWorld>  這種組件節(jié)點(diǎn)時(shí),
   * 會(huì)相應(yīng)生成一個(gè)tag為vue-component-xx-hellowrold的占位vnode
   * 占位vnode沒(méi)有children
   */
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined/** 組件是沒(méi)有children的 */, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }/** componentOptions */
  )
  return vnode
}

這里重點(diǎn)關(guān)注一下,占位vnode中componentOptions,后面在盡心patch時(shí)會(huì)用到,data里面是一些鉤子函數(shù),用于組件節(jié)點(diǎn)實(shí)例的創(chuàng)建或者patch更新,詳細(xì)的這里不過(guò)多解釋。

所以,此時(shí)新的vnode其實(shí)還沒(méi)有創(chuàng)建實(shí)例,生成dom節(jié)點(diǎn),接下來(lái)看看patchVnode中如何為新的vnode生成實(shí)例和dom節(jié)點(diǎn)的

/*patch VNode節(jié)點(diǎn)*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ...
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    /*i = data.hook.prepatch,如果存在的話,見(jiàn)"./create-component componentVNodeHooks"。*/
    // 根據(jù)vnode,更新oldVnode子組件實(shí)例對(duì)應(yīng)的相關(guān)屬性
    i(oldVnode, vnode)
  }
  //這里復(fù)用,直接將oldVnode的dom賦給了vnode
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    /*調(diào)用update回調(diào)以及update鉤子*/
    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)
  }
  ...
}

在上面的代碼中,導(dǎo)致了后續(xù)的渲染錯(cuò)誤。

首先,作為組件占位vnode,根據(jù)我們上面看到的create-component中Vnode的構(gòu)造函數(shù)中可以看出,vnode.data中是一堆hook函數(shù),用于組件vnode的實(shí)例創(chuàng)建和patch更新,這里調(diào)用了hook.prepatch方法,用oldVnode來(lái)patch生成vnode。然后直接將oldVnode的dom賦給了vnode

我們來(lái)看下這個(gè)prepatch都做了些啥

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  //props相關(guān)的數(shù)據(jù)在componentOptions里
  const options = vnode.componentOptions
  //實(shí)例覆蓋:將oldVnode的實(shí)例直接賦值給了vnode
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}

這里可以看到,在prepatch中,直接將oldVnode的實(shí)例直接賦值給了vnode,到這里,問(wèn)題就出現(xiàn)了。

vnode中那些實(shí)例相關(guān)的屬性(例如data中的數(shù)據(jù))就會(huì)丟失,轉(zhuǎn)而成為oldVnode中的實(shí)例屬性。

這里可能會(huì)有點(diǎn)繞,簡(jiǎn)單舉個(gè)例子

在這里插入圖片描述

上面例子中用A節(jié)點(diǎn)復(fù)用生成B節(jié)點(diǎn)的時(shí)候,在prepatch的時(shí)候?qū)的實(shí)例直接賦值給了B,此時(shí)新節(jié)點(diǎn)隊(duì)列中的B對(duì)應(yīng)的實(shí)例屬性text就是A,渲染后會(huì)生成對(duì)應(yīng)的text:A。而oldVnode中B對(duì)應(yīng)的text: B,這就出現(xiàn)了之前我們所說(shuō)的渲染錯(cuò)誤。

那為什么最上面例子中組件節(jié)點(diǎn)中的{{ data.id }}能正確渲染呢?

這里就要看下面這段代碼

updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )

這里的options.propsData用的是vnode.componentOptions中的propsData,而不是oldVnode,所以當(dāng)對(duì)應(yīng)的propsData有變化的時(shí)候,能夠正確的渲染。而覆蓋后的新舊實(shí)例屬性為同一實(shí)例,無(wú)法對(duì)比差異,所以會(huì)直接復(fù)用oldVnode的實(shí)例屬性dom。

所以,日常開(kāi)發(fā)中,如果需要用index作為key值時(shí),需要明確會(huì)不會(huì)造成這種渲染錯(cuò)誤,否則可能會(huì)出現(xiàn)意想不到的難題。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論