What does key do

Key is the unique ID given to each Vnode. You can rely on the key to get the corresponding VNode in oldVnode more accurately and faster.

A more accurate

In vue/patch.js, the sameVnode function needs to make a judgment: A. key === B. key. In the case that there is no key, both A.key and B.key are undefined. For list rendering, it can already be judged as the same node, and then call patchVnode. In the case of key, the situation of local reuse can be avoided in the comparison of A. key === B. Key, so it is more accurate.

faster

Both VUE and React use the diff algorithm to compare old and new virtual nodes to update nodes.

The uniqueness of key is used to generate map objects to obtain corresponding nodes, which is faster than traversal. In the updateChild function of vue/patch.js, the new node and the old node are cross-compared. If the new node and the old node are not cross-compared, the key in the old node array will be compared according to the key of the new node. To find the corresponding old node (in this case, a map map of key => index). If it is not found, it is considered a new node. If there is no key, a traversal lookup is used to find the corresponding old node. One is a map, and the other is a traversal search. By comparison. Map enables faster mapping.

// Vue project SRC /core/vdom/ patch.js-488 line
// Here is the code formatted for readability

// oldCh is an old virtual node array
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
  // Map
  idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
  // Get by traversal
  idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}
Copy the code

Creating a map function

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}
Copy the code

Traversal search

// sameVnode is a function that compares whether old and new nodes are the same
 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
    }
  }
Copy the code

Update 2021/7/21

In ve3. X, branch keys for V-if/V-ELSE/V-else -if are no longer necessary, because Vue now automatically generates unique keys.

Supplement knowledge

The diff algorithm

Diff flow chart: When data changes, the set method will call dep. notify to notify all subscribers Watcher, and the subscribers will call Patch to patch the real DOM and update the corresponding view.

patch

Core code of Patch:

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
    	patchVnode(oldVnode, vnode)
    } else {
    	const oEl = oldVnode.el // The real element node corresponding to the current oldVnode
    	let parentEle = api.parentNode(oEl)  / / the parent element
    	createEle(vnode)  // Generate new elements based on Vnode
    	if(parentEle ! = =null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Add the new element to the parent element
            api.removeChild(parentEle, oldVnode.el)  // Remove the old element node
            oldVnode = null}}// some code 
    return vnode
}

Copy the code

The patch function receives two arguments oldVnode and Vnode representing the new node and the previous old node respectively.

Determine whether the two nodes are worthy of comparison, and then execute patchVnode.

function sameVnode (a, b) {
  return (
    a.key === b.key &&  / / key values
    a.tag === b.tag &&  / / tag name
    a.isComment === b.isComment &&  // Whether it is a comment node
    // Whether data is defined, data contains some specific information, such as onclick, style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // When the tag is , the type must be the same)}Copy the code

If not, replace oldVnode with Vnode

If both nodes are the same, drill down on their children. If the two nodes are different then Vnode has been completely changed and oldVnode can be replaced directly.

What if these two nodes are different but their children are the same? Remember, diff is a layer by layer comparison, if the first layer is not the same then you don’t go into the second layer comparison.

patchVnode

When we determine that the two nodes are worth comparing, we will specify the patchVnode method for both nodes. So what does this method do?

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if(oldVnode.text ! = =null&& vnode.text ! = =null&& oldVnode.text ! == vnode.text) { api.setTextContent(el, vnode.text) }else {
        updateEle(el, vnode, oldVnode)
    	if(oldCh && ch && oldCh ! == ch) { updateChildren(el, oldCh, ch) }else if (ch){
            createEle(vnode) //create el's children dom
    	}else if (oldCh){
            api.removeChildren(el)
    	}
    }
}
Copy the code

This function does the following:

  • Find the corresponding real DOM, called EL
  • Check whether Vnode and oldVnode refer to the same object. If so, return
  • If they both have text nodes and are not equal, set the text node for EL to the text node for Vnode.
  • If oldVnode has children and Vnode does not, the children of EL are removed
  • If oldVnode does not have children and Vnode does, add Vnode’s children to el after they are materialized
  • If both have child nodes, it is important to perform the updateChildren function to compare the children

The other points are easy to understand, so let’s talk about updateChild in detail

updateChildren

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, 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
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {   // For vnode.key comparisons, oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // The comparison with key
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // Create index table with key
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if(! idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode = newCh[++newStartIdx] }else {
                elmToMove = oldCh[idxInOld]
                if(elmToMove.sel ! == newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}
Copy the code

So what does this function do

  • willVnodeThe child nodes of theVchandoldVnodeThe child nodes of theoldChextracted
  • oldChandvChEach variable has two heads and two tailsStartIdxandEndIdx, their two variables are compared with each other, and there are altogether four ways of comparison. If none of the four comparisons match, the comparison will be performed by key with key set. During the comparison, the variable will move towards the middle, and the comparison will end as soon as StartIdx>EndIdx indicates that at least one of the oldCh and vCh has been traversed.

There are four ways to compare samevNodes in oldS, oldE, S, and E. When two of them match, the corresponding node in the real DOM will be moved to the corresponding position in Vnode. This sentence is a bit twisted, for example

  • If it isoldSandEOnce matched, the first node in the real DOM moves to the end
  • If it isoldEandSOnce matched, the last node in the real DOM moves to the front, and the two matched Pointers move to the center
  • If none of the four matches are successful, there are two cases
    • If both old and new child nodesThere is the key, then will be based onoldChildGenerates a key ofThe hash tablewithSIs matched with the hash table, and determines whether S and the matching node aresameNode, if so, move the successful node to the front in the real DOM; otherwise, insert the corresponding node generated by S into the DOMoldSThe S pointer moves to the center and is set to null by the node in the matching old.
    • If there is no key, then S generates a new node and inserts it into the real DOM.

In situ reuse

Official explanation:

If the order of data items is changed, Vue will not move DOM elements to match the order of data items, but simply reuse each element there

Explain this in code:

<div v-for='item in list'>Text {{}}<input/><button @click="Move the item down the list.">
</div>
Copy the code

Demo address: JsBin

In In-place reuse, click the button and the input box does not move down with the text. This is because the input box is not data-bound, so vue defaults to using the already rendered DOM. However, the text is bound to the data, so the text is re-rendered. This is the default list rendering strategy in Vue or Angular because it is efficient.

There is no problem with this “in-place reuse” generic list presentation, so why recommend a key? Because this mode is only good for rendering simple stateless components. For most scenarios, the list component has its own state.

Official explanation:

The “reuse in place” mode is efficient, but only for list rendering output that does not depend on child component state or temporary DOM state (for example, form input values).

For example: a list of news items, you can click on the list item to mark it as visited, and you can TAB to “entertainment News” or “Social News.”

Without the key attribute, select the second item under entertainment News and switch to Social News. The second item in Social News will also be selected because it has reused the component and retains the previous state. To solve this problem, you can attach a news ID to the list item as a unique key, so that every time the list is rendered, all components are completely replaced with the correct state.

This is a simple example, but the practical application is more complicated. Wearing a unique key adds overhead, but the difference is almost invisible to the user and ensures that the component state is correct, which is why it is recommended to use a unique ID as the key. As to specific how to use, be about to choose according to actual situation.

Therefore, the suggestion is that if the list element has a user interaction scenario (such as form or reorder, etc.), you need to set the key parameter for the V-for directive, which only wants the unique value of each element in the list.

Refer to the article

According to Yang – Daily – Interview – Question

Windlany — Elaborate on vue’s diff algorithm

What is the “local reuse” strategy in v-for in Vue2.0?