Vue中v-for有没有key到底有什么区别???

237 阅读4分钟

思考:v-for中的key是什么作用?

在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性

这个key属性有什么作用呢? 在这之前,先了解几个概念:

1.认识虚拟节点(VNode)

VNode是virtual Node的全称,叫做虚拟节点

事实上,无论是组件还是元素,它们最终在vue中表示出来的都是一个个Vnode

VNode的本质是一个Javascript对象:

这里有一个div,在页面上它可能是这样的:

<div class="title",style="font-size:30px;color:red";>哈哈哈</div>

但是本质上,它是这样的:

const vnode = {
    type:'div',
    props: {
        class:'title',
        style: {
            "font-size":"30px",
            color:"red"
        },
    },
    children: "哈哈哈"    //如果这个div元素里有多个子节点,那么children为数组
}
2.虚拟DOM

如果这里不是一个简单的div,而是有一大堆元素,那么这些元素会形成一个由很多个VNode形成的树,这个树就叫做虚 拟DOM树,简称虚拟DOM

注意:虚拟节点是单个元素,而虚拟DOM是由多个虚拟节点组成的树

3.插入f的案例

在列表渲染中,我们渲染了四个字母abcd,但是之后我们想在b和c之间插入一个f,在这之前我们先来思考一个问题:

首先当前的数组发生了变化,那么一些元素必定会被重新渲染,那么必定要重新进行遍历。 那重新遍历的话就会有一个问题:

第一次遍历的时候生成了四个VNodes :a b c d,之后在中间插入了一个f,这时候怎么渲染效率是最高的?

第一种方法:

不管三七二十一,插入之后所有的元素都重新生成Vnodes,进行渲染,因为所有的元素都进行了渲染,那么效率必然很低

还有第二种方法:

因为ab不用变,所以就把c的位置改成f,d的位置改成c,最后再添加一个位置给d,这样的话性能也不太高

因为c和d和f都渲染了,但是c和d没必要重新渲染。

第三种办法就是:

你要插入元素,就生成这个元素,之后在要插入的地方直接插入,不要影响其他元素,不要让其他元素去重新渲染,这样的性能是最高的,这就涉及到diff算法。

4.diff算法

那么如何去判断要不要生成元素,然后在某个地方插入呢?

在vue中,元素展示在页面上要经过template模板--->Vnode---->真实DOM三个阶段,

但是在视图发生更新的时候,会生成新的Vnode,试图改变之前的Vnode叫做旧Vnode

将新旧Vnode进行对比的算法,就是diff算法。

事实上有key和没有key,diff算法会采用不同的方式来处理:

    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) { //判断有无key
​
        // 有key会调用patchKeyedChildren函数
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        // unkeyed
        //没有key会调用patchUnkeyedChildren函数
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        return
      }
    }
5.没有绑定key

没有绑定key时,diff算法其实就会按照我们说的第二种方法来计算,a b不变, c的位置渲染成f, d的位置渲染成c,而最后再添加一个位置给d

也就是调用patchUnkeyedChildren方法,大概有4个步骤:

//源码:
const patchUnkeyedChildren = (
    c1: VNode[], //旧的Nodes
    c2: VNodeArrayChildren, //新的Nodes
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength) //取新旧Nodes中长度较短的那个进行遍历
    let i
    for (i = 0; i < commonLength; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      //新旧Nodes中相同位置上的元素进行patch,如果相同,不做处理,如果不同就更新内容
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
    //如果旧Nodes长度大于新Nodes,就把旧Nodes中多余的部分删除
    if (oldLength > newLength) {
      // remove old
      unmountChildren(
        c1,
        parentComponent,
        parentSuspense,
        true,
        false,
        commonLength
      )
    } else {
      // mount new
      //如果旧Nodes长度小于新Ndoes,说明旧Nodes要新增一部分元素。
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        commonLength
      )
    }
  }
​
6.绑定了key

绑定了key时,就会使用第三种方法,有key会调用patchKeyedChildren方法,这样效率是最高的.

patchKeyedChildrend方法大概有5步:

//源码:
   const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index
​
    // 1.先从新旧Nodes的头部进行遍历:
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      //判断新旧Nodes中相同位置的节点类型和key是否相同,如果相同进行patch
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        //如果不相同退出循环
        break
      }
      i++
    }
​
    // 2.再从新旧Nodes的尾部进行遍历
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      //同样判断相同位置处的新旧节点类型和key是否相同,如果相同,进行patch
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        //不同就跳出循环
        break
      }
      e1--
      e2--
    }
​
    // 3.之后如果是新Nodes长度更长,那么会在旧Nodes中挂载新增的节点
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null, //n1为null对比就相当于挂载
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
    }
​
    // 4.如果是旧Nodes更长,那么就会在旧Nodes中卸载多余节点
    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }
 
    //5.对于既有插入又有删除的情况,会尽量找出类型和key值相同的节点进行复用,其他多余的节点就会进行卸载和挂载操作
    else {
     ...
    }
  }

\