23

I recently reviewed virtual DOM and Diff, and read a lot of materials, hereby I summarize this long article and deepen my understanding of vue. This article analyzes vue's virtual DOM and Diff algorithm in more detail. Some of the key points are transported from other places for explanation (thanks to the graphics masters), and it also contains a more detailed source code interpretation.

Real DOM rendering

Before talking about the virtual DOM, let's talk about the rendering of the real DOM.

The real DOM rendering process of the browser is roughly divided into the following parts

  1. constructs the DOM tree . The HTML tags are parsed and processed by the HTML parser, and they are constructed as a DOM tree. When the parser encounters non-blocking resources (pictures, css), it will continue to parse, but if it encounters script tags (especially without async and defer) Attribute), which will block rendering and stop html parsing. This is why it is best to put the script tag under the body.
  2. constructs the CSSOM tree . Similar to building the DOM, the browser will also build style rules into CSSOM. The browser will traverse the rule set in the CSS, and create a node tree with parent-child, sibling, etc. relationships based on the CSS selector.
  3. constructs the Render tree . This step associates the DOM with CSSOM and determines what CSS rules should be applied to each DOM element. Match all relevant styles to each visible node in the DOM tree, and determine the calculation style of each node according to the CSS cascade. Invisible nodes (head, nodes whose attributes include display: none) will not be generated in the Render tree.
  4. Layout/Reflow . The first time the browser determines the location and size of the node is called layout. If the location and size of the , this step triggers the layout adjustment, that is, .
  5. Paint/Repaint . Draw every visible part of the element on the screen, including text, colors, borders, shadows, and replaced elements (such as buttons and images). If text, color, border, shadow and other elements change, it will trigger repaint (Repaint) . In order to ensure that the redrawing speed is faster than the initial drawing speed, the drawing on the screen is usually broken down into several layers. Upgrading the content to the GPU layer (triggered by tranform, filter, will-change, opacity) can improve the performance of drawing and redrawing.
  6. Compositing . This step merges the layers in the drawing process to ensure that they are drawn in the correct order on the screen to display the correct content.

Why do you need a virtual DOM

The above is a DOM rendering process. If the dom is updated, then the dom needs to be rendered again. If the following situation exists

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
                ....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName('content');
    for (let i = 0; i < 1000000; i++) {
        content[i].innerHTML = `This is a content${i}`;
        // 触发回流
        content[i].style.fontSize = `20px`;
    }
</script>

Then it needs to operate the DOM 100w times and trigger the reflow 100w times. Every time the DOM is updated, the real DOM will be updated indiscriminately according to the process. So it caused a lot of performance waste. If there are complex operations in the loop, frequently triggering reflow and redrawing, then it is easy to affect performance and cause stalls. In addition, it should be noted here that virtual DOM does not mean that it is faster than DOM. Performance needs to be divided into scenes. The performance of virtual DOM is positively related to the size of the template. The virtual DOM comparison process does not distinguish the size of the data. When there are only a few dynamic nodes inside the component, the virtual DOM still traverses the entire vdom, which is an extra layer of operation compared to direct rendering.

    <div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{ item }}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

For example, the above example, virtual DOM. Although there is only one dynamic node, the virtual DOM still needs to traverse the class, text, label and other information of the entire list of diff, and finally still needs to perform DOM rendering. If it's just a dom operation, you only need to manipulate a specific DOM and then perform rendering. The core value of the virtual DOM is that it can describe the real DOM through js, which is more expressive. Through declarative language operations, it provides developers with a more convenient and quick development experience. Moreover, without manual optimization, in most scenarios, The lower limit of performance is guaranteed and the cost performance is higher.

Virtual DOM

Virtual DOM is essentially a js object, through which the real DOM structure is represented. tag is used to describe tags, props is used to describe attributes, and children are used to indicate nested hierarchical relationships.

const vnode = {
    tag: 'div',
    props: {
        id: 'container',
    },
    children: [{
        tag: 'div',
        props: {
            class: 'content',
        },
          text: 'This is a container'
    }]
}

//对应的真实DOM结构
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>

The update of the virtual DOM will not immediately manipulate the DOM. Instead, it will use the diff algorithm to find the nodes that need to be updated, update as needed, and save the updated content as a js object, and then mount it to the real dom after the update is completed. Realize real dom update. Through the virtual DOM, the three problems of operating the real DOM are solved.

  1. Frequent updates indiscriminately lead to frequent updates of the DOM, causing performance problems
  2. Frequent reflow and redraw
  3. Development experience

In addition, because the virtual DOM saves js objects, it naturally has cross-platform , not just limited to the browser.

advantage

To sum up, the advantages of virtual DOM are as follows

  1. Small modifications do not need to update the DOM frequently, the diff algorithm of the framework will automatically compare, analyze the nodes that need to be updated, and update on demand
  2. Update data will not cause frequent reflow and redraw
  3. Stronger expressiveness and more convenient data update
  4. It saves js objects, with cross-platform capabilities

insufficient

Virtual DOM also has shortcomings. When rendering a large amount of DOM for the first time, it will be slower than innerHTML insertion due to an extra layer of virtual DOM calculation.

Principles of Virtual DOM Implementation

Mainly divided into three parts

  1. Create node description object through js
  2. The diff algorithm compares and analyzes the differences between the old and new virtual DOMs
  3. Patch the difference to the real dom to realize the update

Diff algorithm

In order to avoid unnecessary rendering and update on demand, the virtual DOM will use the Diff algorithm to compare virtual DOM nodes and compare node differences, so as to determine the nodes that need to be updated, and then perform rendering. uses 161bc044f00738 depth first, and compares the strategy of

The comparison between the new node and the old node is mainly around three things to achieve the purpose of rendering

  1. Create a new node
  2. Delete the scrap node
  3. Update existing node

compare whether the new and old nodes are consistent?

function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.asyncFactory === b.asyncFactory && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b) //对input节点的处理
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

//判断两个节点是否是同一种 input 输入类型
function sameInputType(a, b) {
    if (a.tag !== 'input') return true
    let i
    const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
    const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
    //input type 相同或者两个type都是text
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

As you can see, whether the two nodes are the same is to compare the tag (tag) , properties (in vue, use data to represent the property props in the vnode) , comment node (isComment) , and also encounter input If it does, special treatment will be done.

Create new node

When the new node has it, the old node does not, which means that this is a brand new content node. Only element nodes, text nodes, and comment nodes can be created and inserted into the DOM.

Delete old node

When the old node has but the new node does not, it means that the new node has given up part of the old node. Deleting a node will also delete the child nodes of the old node.

Update node

Both the new node and the old node exist, so everything is subject to the new one, and the old node is updated. How to judge whether the node needs to be updated?

  • Determine whether the new node is exactly the same as the old node, if the same, no update is required
  // 判断vnode与oldVnode是否完全一样
  if (oldVnode === vnode) {
    return;
  }
  • Determine whether the new node and the old node are static nodes, whether the key is the same, whether it is a cloned node (if it is not a cloned node, it means that the rendering function has been reset, and re-rendering is required at this time) or whether the once attribute is set to meet the conditions Replace componentInstance
  // 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  • Determine whether the new node has text (judging by the text attribute). If there is text, compare the old nodes of the same level. If the text of the old node is different from the text of the new node, use the new text content. If the new node has no text, then you need to judge the relevant situation of the child node later
//判断新节点是否有文本
if (isUndef(vnode.text)) {
  //如果没有文本,处理子节点的相关代码
  ....
} else if (oldVnode.text !== vnode.text) {
  //新节点文本替换旧节点文本
  nodeOps.setTextContent(elm, vnode.text)
}
  • Determine the relative status of the new node and the child nodes of the old node. Here can be divided into 4 situations

    1. new node and the old node 161bc044f00a98 have child nodes
    2. only the new node has child nodes
    3. only the old node has child nodes
    4. new node nor the old node 161bc044f00b1d has a child node

has child nodes

For the case where there are child nodes, you need to compare the old and new nodes. If they are not the same, then you need to perform a diff operation. In Vue, this is the updateChildren method, which will be discussed in detail later. The comparison of child nodes is mainly a double-ended comparison.

//判断新节点是否有文本
if (isUndef(vnode.text)) {
    //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法
    if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
} else if (oldVnode.text !== vnode.text) {
    //新节点文本替换旧节点文本
    nodeOps.setTextContent(elm, vnode.text)
}

Only the new node has children

Only the new node has child nodes, then it means that this is new content, then it is to add a child node to the DOM, before adding a duplicate key detection, and make a reminder, but also consider, If the old node is just a text node without child nodes, in this case, the text content of the old node needs to be cleared.

//只有新节点有子节点
if (isDef(ch)) {
  //检查重复key
  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(ch)
  }
  //清除旧节点文本
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  //添加新节点
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}

//检查重复key
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      //子节点每一个Key
      const key = vnode.key
      if (isDef(key)) {
          if (seenKeys[key]) {
              warn(
                  `Duplicate keys detected: '${key}'. This may cause an update error.`,
                  vnode.context
              )
          } else {
              seenKeys[key] = true
          }
      }
  }
}

Only the old node has children

Only the old node has it, it means that the new node discards the child nodes of the old node, so the child nodes of the old node need to be deleted

if (isDef(oldCh)) {
  //删除旧节点
  removeVnodes(oldCh, 0, oldCh.length - 1)
}

has no children

At this time, you need to judge the text of the old node to see if there is text in the old node, and clear it if there is.

if (isDef(oldVnode.text)) {
  //清空
  nodeOps.setTextContent(elm, '')
}

The overall logic code is as follows

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
) {
    // 判断vnode与oldVnode是否完全一样
    if (oldVnode === vnode) {
        return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // 克隆重用节点
        vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
        if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
        } else {
            vnode.isAsyncPlaceholder = true
        }
        return
    }
        // 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
        vnode.componentInstance = oldVnode.componentInstance
        return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children

    if (isDef(data) && isPatchable(vnode)) {
          //调用update回调以及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)
    }
        //判断新节点是否有文本
    if (isUndef(vnode.text)) {
          //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
              //只有新节点有子节点
            if (process.env.NODE_ENV !== 'production') {
                  //重复Key检测
                checkDuplicateKeys(ch)
            }
              //清除旧节点文本
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
              //添加新节点
            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)
    }

    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

With a flowchart, it will be clearer

child node comparison update updateChildren

When the old and new nodes have child nodes, the updateChildren method needs to be called at this time to compare and update the child nodes. So in terms of data, the child nodes of the old and new nodes are saved as two arrays.

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];

The child node update uses the double-ended comparison . What is a double-ended comparison, that is, the new and old nodes are compared by comparing the first and last elements (there are four comparisons), and then move closer to the middle ( newStartIdx, and oldStartIdx increase , newEndIdx and oldEndIdx decrement ) strategy.

Comparison process

moves closer to the middle

Here is an explanation of the new front, new rear, old front, and old rear that appear above

  1. Before the new, it refers new node untreated child array in first element corresponding to the source vue newStartVnode
  2. The new, refers new node untreated child array in last element corresponding to the source vue newEndVnode
  3. Before the old, it refers first element in the child node array of old node did not oldStartVnode in the vue source code
  4. The old post refers last element in the child node array of old node did not oldEndVnode in the vue source code

child node comparison process

Next, I will explain the above comparison process and the operations after the comparison.

  • new 161bc044f0102a with the old . If they are the same, then perform the patchVnode update operation mentioned above, and then the new and old nodes will go one step back and compare the second item... Until they encounter a difference, the comparison will be changed. Way

if (sameVnode(oldStartVnode, newStartVnode)) {
  // 更新子节点
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 新旧各向后一步
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
  • new 161bc044f010f4 with the old . If they are the same, update the pathchVnode, and then the new and the old will step forward to compare the previous one... Until they are different, the comparison method will be changed.

if (sameVnode(oldEndVnode, newEndVnode)) {
    //更新子节点
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 新旧向前
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • new back of 161bc044f01188 with the old front . If they are the same, perform an update operation, and then move the old front to the all unprocessed old node arrays in , so that the old front and new back positions are consistent, and then both Close to the middle, new forward, old backward. If they are different, continue to switch the comparison method

if (sameVnode(oldStartVnode, newEndVnode)) {
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  //将旧子节点数组第一个子节点移动插入到最后
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  //旧向后
  oldStartVnode = oldCh[++oldStartIdx]
  //新向前
  newEndVnode = newCh[--newEndIdx]
  • with the old rear 161bc044f0121d. If they are the same, update it, and then move the old back to the all unprocessed old node arrays of . , Old forward, continue to move closer to the middle. Continue to compare the remaining nodes. If they are different, use the traditional loop traversal search.

if (sameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  //将旧后移动插入到最前
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  //旧向前
  oldEndVnode = oldCh[--oldEndIdx]
  //新向后
  newStartVnode = newCh[++newStartIdx]
}
  • loops to find . If none of the above four are found, the key will be used to find a match.

At this step, for nodes that have not set a key, the mapping between key and index will be established for the first time through createKeyToOldIdx {key:index}

// 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

Then compare the key of the new node with the old node to find the location of the node whose key value matches. It should be noted here that if the new node does not have a key, then the findIdxInOld method will be executed to traverse the old node from beginning to end.

//通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找
idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

//findIdxInOld方法
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
  }
}

If through the above method, the subscript that matches the new node and the old node is still not found, it means that this node is a new node, and then perform the new operation.

//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
if (isUndef(idxInOld)) {
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}

If it is found, it means that an old node with the same key value or the same node and key has been found in the old node. If the nodes are the same, after patchVnode, the old node before all unprocessed nodes. For nodes with the same key and different elements, they are considered as new nodes and the new operation is performed. After the execution is complete, the new node takes one step back.

//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
if (isUndef(idxInOld)) {
  // 新增元素
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  // 在旧节点中找到了key值一样的节点
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    // 相同子节点更新操作
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // 更新完将旧节点赋值undefined
    oldCh[idxInOld] = undefined
    //将旧节点移动到所有未处理节点之前
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  } else {
    // 如果是相同的key,不同的元素,当做新节点,执行创建操作
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  }
}
//新节点向后
newStartVnode = newCh[++newStartIdx]

When the traversal of the old node is completed, but the traversal of the new node has not been completed, it means that the following are new nodes, and the new operation is performed. If the traversal of the new node is completed and the traversal of the old node has not been completed, then the old node Redundant nodes appear, perform the delete operation.

//完成对旧节点的遍历,但是新节点还没完成遍历,
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  // 新增节点
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  // 发现多余的旧节点,执行删除操作
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

child node comparison summary

The above is a complete process of updating the child nodes. This is the complete logic code.

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let 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, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(newCh)
    }

    //双端比较遍历
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            //旧前向后移动
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
        } else if (isUndef(oldEndVnode)) {
            // 旧后向前移动
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            //新前与旧前
            //更新子节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                // 新旧各向后一步
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            //新后与旧后
            //更新子节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //新旧各向前一步
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // 新后与旧前
            //更新子节点
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //将旧前移动插入到最后
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
                //新向前,旧向后
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // 新前与旧后
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)

            //将旧后移动插入到最前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

            //新向后,旧向前
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            //通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找
            idxInOld = isDef(newStartVnode.key) ?
                oldKeyToIdx[newStartVnode.key] :
                findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            //如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
            if (isUndef(idxInOld)) {
                // 新增元素
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 在旧节点中找到了key值一样的节点
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    // 相同子节点更新操作
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                        // 更新完将旧节点赋值undefined
                    oldCh[idxInOld] = undefined
                        //将旧节点移动到所有未处理节点之前
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 如果是相同的key,不同的元素,当做新节点,执行创建操作
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            //新节点向后一步
            newStartVnode = newCh[++newStartIdx]
        }
    }

    //完成对旧节点的遍历,但是新节点还没完成遍历,
    if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
            // 新增节点
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
        // 发现多余的旧节点,执行删除操作
        removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
}

Reference

  1. VirtualDOM and diff
  2. rendering page: how the browser works
  3. DOM-Diff in Vue
  4. in-depth analysis: the virtual DOM at the core of Vue

kerin
497 声望573 粉丝

前端菜鸟