This is the 120th original article without water. If you want to get more original articles, please search the public account to follow us. This article was first published on the blog of Political Research Cloud: Why is index not recommended as the key in Vue

preface

In front-end development, whenever it comes to list rendering, both React and Vue frameworks will prompt or require each list item to use a unique key. Therefore, many developers will directly use the array index as the key value without knowing the principle of key. This article will explain what keys do and why it’s best not to use index as a key attribute.

The key role

Virtual DOM is used in Vue and the old and new DOM are compared according to diFF algorithm to update the real DOM. Key is the unique identifier of virtual DOM object, and key plays an extremely important role in DIFF algorithm.

Key’s role in the Diff algorithm

In React Vue, the diff algorithm is basically the same, but the diff comparison method is quite different, and even each version of diFF is quite different. Next, we take Vue3.0 DIFF algorithm as a starting point to analyze the role of key in diFF algorithm

The specific DIFF process is as follows

Vue3.0 has such a source code in the patchChildren method

if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) { 
         /* Diff algorithm for the presence of key */
        patchKeyedChildren(
         ...
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
         /* 对于不存在 key 的情况,直接 patch  */
        patchUnkeyedChildren( 
          ...
        )
        return}}Copy the code

PatchChildren performs real diff or patch directly according to whether there is a key. We won’t go into the case where key doesn’t exist.

Let’s start by looking at some declared variables.

/* c1 Old vnode c2 new vnode */
let i = 0              /* Record index */
const l2 = c2.length   /* Number of new vNodes */
let e1 = c1.length - 1 /* The index of the last node of the old vnode */
let e2 = l2 - 1        /* The index of the last node of the new node */
Copy the code

Synchronous head node

The first step is to start from scratch to find the same vnode, and then patch. If the node is not the same, then jump out of the loop immediately.

//(a b) c
//(a b) d e
/* Find the same node patch from scratch, find the difference, immediately jump */
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
        /* Check whether key and type are equal */
      if (isSameVNodeType(n1, n2)) {
        patch(
          ...
        )
      } else {
        break
      }
      i++
    }
Copy the code

The process is as follows:

IsSameVNodeType Checks whether the current VNode type is the same as the key of the Vnode

export function isSameVNodeType(n1: VNode, n2: VNode) :boolean {
  return n1.type === n2.type && n1.key === n2.key
}
Copy the code

If you look at this, you already know that the key is used in the diff algorithm to determine if it’s the same node.

Synchronous tail node

The second step starts at the end with the previous diff

//a (b c)
//d e (b c)
/* If the first step does not finish the patch, immediately start the patch from back to front
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
         ...
        )
      } else {
        break
      }
      e1--
      e2--
    }
Copy the code

After the operation of the first step, if it is found that the patch is not finished, the second step is immediately carried out and diff is proceeded from the tail. If the node is not the same, jump out of the loop immediately. The process is as follows:

Adding a new node

Step 3 If the old nodes are all patched but the new nodes are not, create a new VNode

//(a b)
//(a b) c
//i = 2, e1 = 1, e2 = 2
//(a b)
//c (a b)
//i = 0, e1 = -1, e2 = 0
/* If the number of new nodes is greater than the number of old nodes, the remaining nodes are processed with the new vNode (in this case, the same Vnode has been patched) */
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch( /* Create a new node */
            ...
          )
          i++
        }
      }
    }
Copy the code

The process is as follows:

Deleting Redundant Nodes

Step 4 If all the new nodes are patched and the old nodes are left, uninstall all the old nodes

//i > e2
//(a b) c
//(a b)
//i = 2, e1 = 2, e2 = 1
//a (b c)
//(b c)
//i = 0, e1 = 0, e2 = -1
else if (i > e2) {
   while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
   }
}
Copy the code

The process is as follows:

Longest increasing subsequence

At this stage, the core scene has not yet appeared, and if we are lucky, it may end here, so we can not rely on luck. The remaining scenario is where both the old and new nodes still have multiple children. So let’s see what Vue3 does. To combine move, add, and unload operations

Every time we move an element, we can find a rule. If we want to move the least number of elements, it means that some elements are stable and motionless. Then what are the rules of elements that can stay stable and motionless?

Take a look at the example above: C H D E VS D E I C. When comparing, you can see with the naked eye that you just need to move C to the end, then uninstall H and add I. D e can remain fixed, and it can be found that the order of d e in the old and new nodes is unchanged. D comes after E, and the subscripts are increasing.

I'm going to introduce a concept called longest increasing subsequence. In a given array, find an increasing set of values as large as possible. Const arr = [10, 9, 2, 5, 3, 7, 101, 18] => [2, 3, 7, 18] is the longest increasing subsequence of ARR. So the longest increasing subsequence meets three requirements: 1) the value in the subsequence is increasing; 2) the subsequence's index is increasing in the original array; 3) The subsequence is the longest we can find, but we usually find the smaller sequence because they have more space to grow.Copy the code

So the next idea is: if you can find the old nodes in the same order in the new node sequence, you know which nodes don’t need to move, and then you just insert the nodes that aren’t here. ** Because the last order to be presented is the order of the new nodes, the move is as long as the old node moves, so as long as the old node keeps the longest order unchanged, by moving individual nodes, it can be consistent with it. ** So before this, first find all nodes, and then find the corresponding sequence. The final result is the array [2, 3, add, 0]. So that’s the idea of Diff moving

Why not index

Performance overhead

When index is used as the key, some nodes cannot be reused because the corresponding key cannot be found on each node, and all new VNodes need to be created again.

Example:

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}}</li>
      <br>
      <button @click="addStudent">Add a piece of data</button>
    </ul>

  </div>
</template>

<script>
export default {
  name: 'HelloWorld'.data() {
    return {
      studentList: [{id: 1.name: 'Joe'.age: 18 },
        { id: 2.name: 'bill'.age: 19},]}; },methods: {addStudent(){
      const studentObj = { id: 3.name: 'Cathy'.age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>

Copy the code

Let’s open the Chorme debugger and double-click to modify the text

Let’s run the code above and see what happens

From the above results, we can see that we only added one piece of data, but three pieces of data need to be re-rendered. Isn’t it surprising that ALL three pieces of data need to be re-rendered when I just inserted one piece of data? And all I want is for that new piece of data to be rendered.

We talked about diif comparisons, so let’s draw a graph based on diff comparisons and see how they work

When we added a data item to the front of the page, the index order would be broken, causing the new node key to change completely, thus causing the data on our page to be re-rendered.

In order to ensure the uniqueness of the key, we use the UUID as the key

Let’s do it again with index as the key

<template>
  <div class="hello">
    <ul>
      <button @click="addStudent">Add a piece of data</button>
      <br>
      <li v-for="(item,index) in studentList" :key="index">{{item.id}}</li>
    </ul>
  </div>
</template>

<script>
import uuidv1 from 'uuid/v1'
export default {
  name: 'HelloWorld'.data() {
    return {
      studentList: [{id:uuidv1()}],
    };
  },
  created(){
    for (let i = 0; i < 1000; i++) {
      this.studentList.push({
        id: uuidv1(), }); }},beforeUpdate(){
    console.time('for');
  },
  updated(){
    console.timeEnd('for')/ / for: 75.259033203125 ms
  },
  methods: {addStudent(){
      const studentObj = { id: uuidv1() };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>
Copy the code

Replace the key with id

<template>
  <div class="hello">
    <ul>
      <button @click="addStudent">Add a piece of data</button>
      <br>
      <li v-for="(item,index) in studentList" :key="item.id">{{item.id}}</li>
    </ul>
  </div>
</template>
  beforeUpdate(){
    console.time('for');
  },
  updated(){
    console.timeEnd('for')/ / for: 42.200927734375 ms
  },
Copy the code

As you can see from the above comparison, using a unique value as a key can save overhead

Data dislocation

In the example above, I may think that using index as the key only affects the efficiency of page loading, and think that a small amount of data does not affect the efficiency of page loading. In the following case, I may have some unexpected problems when using index. In this case, I will add an input box after each text content. And manually fill in some content in the input box, and then append a student forward through the button

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}}<input /></li>
      <br>
      <button @click="addStudent">Add a piece of data</button>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld'.data() {
    return {
      studentList: [{id: 1.name: 'Joe'.age: 18 },
        { id: 2.name: 'bill'.age: 19},]}; },methods: {addStudent(){
      const studentObj = { id: 3.name: 'Cathy'.age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>
Copy the code

Let’s put some values into the input and add a student to see what it looks like:

At this point, we find that the data entered before adding is misplaced. After the addition, Wang Wu’s input box left Zhang SAN’s information, which is obviously not the result we want.

As can be seen from the above comparison, when index was used as the key, although the text value was changed, the DOM node was still the same as the original when the comparison was continued, so the reuse was made. However, I did not expect the residual input value in the input box. At this point, the input value will appear to be misplaced

The solution

Since we know that using index can have a bad effect in some cases, how can we solve this problem in normal development? In fact, as long as you ensure that the key is unique on the line, generally used in development is the following three situations.

  1. In the development, it is best to use the unique identification fixed data as the key for each data, such as the ID, mobile phone number, ID number and other unique values returned from the background
  2. ES6 introduces a new primitive data type Symbol that represents a unique value. The largest use is to define a unique attribute name for an object.
Let a=Symbol(' test ') let b=Symbol(' test ') console.log(a===b)//falseCopy the code
  1. You can use uUID as a key, short for Universally Unique Identifier, which is a machine-generated Identifier that is Unique within a range (from a specific namespace to the world)

Let’s take a look at the above situation by using the first scheme as the key, as shown in the figure. Nodes with the same key are multiplexed. The diff algorithm really works.

conclusion

  • When index is used as the key, out-of-order operations such as adding or deleting data in reverse order will result in unnecessary real DOM updates, resulting in low efficiency
  • Using index as the key causes incorrect DOM updates if the structure contains the DOM of the input class
  • In the development, it is best to use the unique identification fixed data as the key for each data, such as the ID, mobile phone number, ID number and other unique values returned from the background
  • If there is no operation of adding or deleting data in reverse order, and it is only used for rendering, using index as key is also acceptable (however, it is still not recommended to use index as a good development habit).

Refer to the link

Vue3.0 DiFF algorithm in detail

Vue 3 Virtual Dom Diff

Recommended reading

E-commerce minimum inventory – SKU and algorithm implementation

What you need to know about project management

Backflow redraw of browser rendering

Anti-shake throttling scenario and application

Open source works

  • Political cloud front-end tabloid

Open source address www.zoo.team/openweekly/ (wechat communication group on the official website of tabloid)

  • skuDemo

Open source addressGithub.com/zcy-inc/sku…

, recruiting

ZooTeam, a young passionate and creative front-end team, belongs to the PRODUCT R&D department of ZooTeam, based in picturesque Hangzhou. The team now has more than 50 front-end partners, with an average age of 27, and nearly 30% of them are full-stack engineers, no problem in the youth storm group. The members consist of “old” soldiers from Alibaba and netease, as well as fresh graduates from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. In addition to daily business docking, the team also carried out technical exploration and practice in material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, promoted and implemented a series of internal technical products, and continued to explore the new boundary of front-end technology system.

If you want to change what’s been bothering you, you want to start bothering you. If you want to change, you’ve been told you need more ideas, but you don’t have a solution. If you want change, you have the power to make it happen, but you don’t need it. If you want to change what you want to accomplish, you need a team to support you, but you don’t have the position to lead people. If you want to change the pace, it will be “5 years and 3 years of experience”; If you want to change the original savvy is good, but there is always a layer of fuzzy window… If you believe in the power of believing, believing that ordinary people can achieve extraordinary things, believing that you can meet a better version of yourself. If you want to be a part of the process of growing a front end team with deep business understanding, sound technology systems, technology value creation, and impact spillover as your business takes off, I think we should talk. Any time, waiting for you to write something and send it to [email protected]