I let the diff algorithm process of virtual DOM move

I wrote an article last year and handwritten a virtual DOM library, so that you can fully understand the diff algorithm and introduce the virtual DOMprocess patchand diffalgorithm process. At that time, the double-ended diffalgorithm was used. This year, I saw Vue3that the fast algorithm was used diff, so I also wanted to write an article. Let me record it, but someone must have already written it, so I was wondering if it could be a little different. The last article mainly showed every situation and process of the algorithm step by step by drawing pictures, so I was wondering if it could diffbe Changed to the form of animation, so there is this article. Of course, the current implementation is still based on the double-ended diffalgorithm, and the fast algorithm will be added later diff.

Portal: Animation demonstration of double-ended Diff algorithm .

The interface is like this. On the left side, you can enter the new and old lists to be compared VNode, and then click the start button to show the process from beginning to end in the form of animation. On the right are three horizontal lists, representing the old and new VNodelists respectively. As well as the current real DOMlist, DOMthe list is initially consistent with the old VNodelist, and will be consistent with the new VNodelist after the algorithm ends.

It should be noted that this animation only includes diffthe process of the algorithm, not patchthe process.

Let's first review diffthe function of the double-ended algorithm:

const diff = (el, oldChildren, newChildren) => {
    
    
  // 指针
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  // 节点
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    if (oldStartVNode === null) {
    
    
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (oldEndVNode === null) {
    
    
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (newStartVNode === null) {
    
    
      newStartVNode = oldChildren[++newStartIdx]
    } else if (newEndVNode === null) {
    
    
      newEndVNode = oldChildren[--newEndIdx]
    } else if (isSameNode(oldStartVNode, newStartVNode)) {
    
     // 头-头
      patchVNode(oldStartVNode, newStartVNode)
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldStartVNode, newEndVNode)) {
    
     // 头-尾
      patchVNode(oldStartVNode, newEndVNode)
      // 把oldStartVNode节点移动到最后
      el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameNode(oldEndVNode, newStartVNode)) {
    
     // 尾-头
      patchVNode(oldEndVNode, newStartVNode)
      // 把oldEndVNode节点移动到oldStartVNode前
      el.insertBefore(oldEndVNode.el, oldStartVNode.el)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldEndVNode, newEndVNode)) {
    
     // 尾-尾
      patchVNode(oldEndVNode, newEndVNode)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else {
    
    
      let findIndex = findSameNode(oldChildren, newStartVNode)
      // newStartVNode在旧列表里不存在,那么是新节点,创建插入
      if (findIndex === -1) {
    
    
        el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
      } else {
    
     // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
        let oldVNode = oldChildren[findIndex]
        patchVNode(oldVNode, newStartVNode)
        el.insertBefore(oldVNode.el, oldStartVNode.el)
        // 将该VNode置为空
        oldChildren[findIndex] = null
      }
      newStartVNode = newChildren[++newStartIdx]
    }
  }
  // 旧列表里存在新列表里没有的节点,需要删除
  if (oldStartIdx <= oldEndIdx) {
    
    
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    
    
      removeEvent(oldChildren[i])
      oldChildren[i] && el.removeChild(oldChildren[i].el)
    }
  } else if (newStartIdx <= newEndIdx) {
    
    
    let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
    for (let i = newStartIdx; i <= newEndIdx; i++) {
    
    
      el.insertBefore(createEl(newChildren[i]), before)
    }
  }
}

The specific implementation steps of this function can refer to the previous article, so this article will not repeat them.

If we want to diffmove this process, we must first find out what objects are animated. Starting from the parameters of the function, first oldChildrenand newChildrentwo VNodelists are essential, which can be represented by two horizontal lists, and then four pointers , which is the key to the double-ended diffalgorithm. We represent it through four arrows, pointing to the current node being compared, and then start the cycle. In fact, the old and new VNodelists in the cycle basically do not change. What we actually operate is VNodethe corresponding Real DOMelements, including patchpatching, moving, deleting, adding, etc., so we have another horizontal list to represent the current real list. At the beginning , it must correspond DOMto the old list. Step by step through the algorithm, it will become and The new list corresponds.VNodediffVNode

Let's look back at the function VNodethat creates the object h:

export const h = (tag, data = {
     
     }, children) => {
    
    
  let text = ''
  let el
  let key
  // 文本节点
  if (typeof children === 'string' || typeof children === 'number') {
    
    
    text = children
    children = undefined
  } else if (!Array.isArray(children)) {
    
    
    children = undefined
  }
  if (data && data.key) {
    
    
    key = data.key
  }
  return {
    
    
    tag, // 元素标签
    children, // 子元素
    text, // 文本节点的文本
    el, // 真实dom
    key,
    data
  }
}

VNodeThe list data we input will be hcreated into VNodeobjects using functions, so the simplest structure that can be input is as follows:

[
  {
    
    
    tag: 'div',
    children: '文本节点的内容',
    data: {
    
    
      key: 'a'
    }
  }
]

The entered new and old VNodelist data will be saved in store, which can be obtained in the following ways:

// 输入的旧VNode列表
store.oldVNode
// 输入的新VNode列表
store.newVNode

Next define the relevant variables:

// 指针列表
const oldPointerList = ref([])
const newPointerList = ref([])
// 真实DOM节点列表
const actNodeList = ref([])
// 新旧节点列表
const oldVNodeList = ref([])
const newVNodeList = ref([])
// 提示信息
const info = ref('')

The moving animation of the pointer can be realized by using the property cssof transitionthe pointer element, as long as the value of the pointer element is modified , the moving animation of the leftreal list can be easily realized by using the list transition component TransitionGroup , the template is as follows:DOMVue

<div class="playground">
  <!-- 指针 -->
  <div class="pointer">
    <div
         class="pointerItem"
         v-for="item in oldPointerList"
         :key="item.name"
         :style="{ left: item.value * 120 + 'px' }"
         >
      <div class="pointerItemName">{
   
   { item.name }}</div>
      <div class="pointerItemValue">{
   
   { item.value }}</div>
      <img src="../assets/箭头_向下.svg" alt="" />
    </div>
  </div>
  <div class="nodeListBox">
    <!-- 旧节点列表 -->
    <div class="nodeList">
      <div class="name" v-if="oldVNodeList.length > 0">旧的VNode列表</div>
      <div class="nodes">
        <TransitionGroup name="list">
          <div
               class="nodeWrap"
               v-for="(item, index) in oldVNodeList"
               :key="item ? item.data.key : index"
               >
            <div class="node">{
   
   { item ? item.children : '空' }}</div>
          </div>
        </TransitionGroup>
      </div>
    </div>
    <!-- 新节点列表 -->
    <div class="nodeList">
      <div class="name" v-if="newVNodeList.length > 0">新的VNode列表</div>
      <div class="nodes">
        <TransitionGroup name="list">
          <div
               class="nodeWrap"
               v-for="(item, index) in newVNodeList"
               :key="item.data.key"
               >
            <div class="node">{
   
   { item.children }}</div>
          </div>
        </TransitionGroup>
      </div>
    </div>
    <!-- 提示信息 -->
    <div class="info">{
   
   { info }}</div>
  </div>
  <!-- 指针 -->
  <div class="pointer">
    <div
         class="pointerItem"
         v-for="item in newPointerList"
         :key="item.name"
         :style="{ left: item.value * 120 + 'px' }"
         >
      <img src="../assets/箭头_向上.svg" alt="" />
      <div class="pointerItemValue">{
   
   { item.value }}</div>
      <div class="pointerItemName">{
   
   { item.name }}</div>
    </div>
  </div>
  <!-- 真实DOM列表 -->
  <div class="nodeList act" v-if="actNodeList.length > 0">
    <div class="name">真实DOM列表</div>
    <div class="nodes">
      <TransitionGroup name="list">
        <div
             class="nodeWrap"
             v-for="item in actNodeList"
             :key="item.data.key"
             >
          <div class="node">{
   
   { item.children }}</div>
        </div>
      </TransitionGroup>
    </div>
  </div>
</div>

diffThe new list will not be modified during the double-ended algorithm VNode, but the old VNodelist may be modified, that is, when the end-to-end comparison does not find a node that can be reused, but it is VNodefound by directly searching in the old list, Then the VNodecorresponding reality will be moved DOM. After moving, it VNodeis actually equivalent to having been processed, but the VNodeposition of the object is in the middle of the current pointer and cannot be deleted directly, so it has to be set to empty null, so you can see in the template There are handles for this situation.

In addition, we also created an infoelement to display the text information of the prompt as a description of the animation.

But this is not enough, because each old one VNodehas a corresponding real DOMelement, but what we input is just an ordinary jsondata, so the template also needs to add a new list as the associated node of the old VNodelist. This list only needs to provide nodes It can be referenced and does not need to be visible, so set it displayto none:

// 根据输入的旧VNode列表创建元素
const _oldVNodeList = computed(() => {
    
    
  return JSON.parse(store.oldVNode)
})
// 引用DOM元素
const oldNode = ref(null)
const oldNodeList = ref([])
<!-- 隐藏 -->
<div class="hide">
  <div class="nodes" ref="oldNode">
    <div
         v-for="(item, index) in _oldVNodeList"
         :key="index"
         ref="oldNodeList"
         >
      {
   
   { item.children }}
    </div>
  </div>
</div>

Then when we click the start button, we can assign values ​​to our three list variables, and use hfunctions to create new and old VNodeobjects, and then pass them to the patched patchfunction to start comparing and updating the actual DOMelements:

const start = () => {
    
    
  nextTick(() => {
    
    
    // 表示当前真实的DOM列表
    actNodeList.value = JSON.parse(store.oldVNode)
    // 表示旧的VNode列表
    oldVNodeList.value = JSON.parse(store.oldVNode)
    // 表示新的VNode列表
    newVNodeList.value = JSON.parse(store.newVNode)
    nextTick(() => {
    
    
      let oldVNode = h(
        'div',
        {
    
     key: 1 },
        JSON.parse(store.oldVNode).map((item, index) => {
    
    
          // 创建VNode对象
          let vnode = h(item.tag, item.data, item.children)
          // 关联真实的DOM元素
          vnode.el = oldNodeList.value[index]
          return vnode
        })
      )
      // 列表的父节点也需要关联真实的DOM元素
      oldVNode.el = oldNode.value
      let newVNode = h(
        'div',
        {
    
     key: 1 },
        JSON.parse(store.newVNode).map(item => {
    
    
          return h(item.tag, item.data, item.children)
        })
      )
      // 调用patch函数进行打补丁
      patch(oldVNode, newVNode)
    })
  })
}

It can be seen that the new and old lists we input VNodeare used as child nodes of a node. This is because only when the two compared nodes have child nodes that are not text nodes, algorithms need to be used to diffupdate their child nodes efficiently. When patchAfter the function runs, you can open the console to view the hidden list, and you will find that it is consistent with the DOMnew list. Then you may ask, why not just use this list as the real list, and create an additional list yourself , in fact, it is possible, but in the process of the algorithm , the real nodes are moved by other methods , so it is not easy to add transition animations, only to see the nodes change positions instantly, which does not meet our animation needs.VNodeDOMactNodeListdiffinsertBeforeDOM

The effect here is as follows:

Next, let's get out the pointer first. We create a processing function object, which will mount some methods for calling during the diffalgorithm and updating the corresponding variables in the function.

const handles = {
    
    
  // 更新指针
  updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx) {
    
    
    oldPointerList.value = [
      {
    
    
        name: 'oldStartIdx',
        value: oldStartIdx
      },
      {
    
    
        name: 'oldEndIdx',
        value: oldEndIdx
      }
    ]
    newPointerList.value = [
      {
    
    
        name: 'newStartIdx',
        value: newStartIdx 
      },
      {
    
    
        name: 'newEndIdx',
        value: newEndIdx
      }
    ]
  },
}

Then we can update the pointer in diffthe function handles.updatePointers():

const diff = (el, oldChildren, newChildren) => {
    
    
  // 指针
  // ...
  handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
  // ...
}

So the pointer comes out:

Then whilethese four pointers will be constantly changed in the loop, so they also need to be updated in the loop:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
  // ...
  handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
}

But this is obviously not possible, why, because the loop ends in an instant, and we hope to stay for a while every time, it is very simple, we write a waiting function:

const wait = t => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(
      () => {
    
    
        resolve()
      },
      t || 3000
    )
  })
}

Then we async/awaitcan easily implement waiting in loops using the syntax:

const diff = async (el, oldChildren, newChildren) => {
    
    
  // ...
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    // ...
    handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
    await wait()
  }
}

Next we add two variables to highlight the two we are currently comparing VNode:

// 当前比较中的节点索引
const currentCompareOldNodeIndex = ref(-1)
const currentCompareNewNodeIndex = ref(-1)

const handles = {
    
    
  // 更新当前比较节点
  updateCompareNodes(a, b) {
    
    
    currentCompareOldNodeIndex.value = a
    currentCompareNewNodeIndex.value = b
  }
}
<div
     class="nodeWrap"
     v-for="(item, index) in oldVNodeList"
     :key="item ? item.data.key : index"
     :class="{
         current: currentCompareOldNodeIndex === index,
     }"
     >
  <div class="node">{
   
   { item ? item.children : '空' }}</div>
</div>
<div
     class="nodeWrap"
     v-for="(item, index) in newVNodeList"
     :key="item.data.key"
     :class="{
         current: currentCompareNewNodeIndex === index,
     }"
     >
  <div class="node">{
   
   { item.children }}</div>
</div>

Add a class name to the node in the current comparison for highlighting, the next step is still the same, you need to diffcall the function in the function, but how to add it:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    if // ...
    } else if (isSameNode(oldStartVNode, newStartVNode)) {
    
    
      // ...
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldStartVNode, newEndVNode)) {
    
    
      // ...
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameNode(oldEndVNode, newStartVNode)) {
    
    
      // ...
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldEndVNode, newEndVNode)) {
    
    
      // ...
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else {
    
    
      // ...
      newStartVNode = newChildren[++newStartIdx]
    }

We want to show the process of head-to-tail comparison, in fact, in these ifconditions, that is, to ifstay in each condition for a period of time, so can we do this directly:

const isSameNode = async () => {
    
    
  // ...
  handles.updateCompareNodes()
  await wait()
}

if (await isSameNode(oldStartVNode, newStartVNode))

Unfortunately, I failed to try, so I can only rewrite it in other forms:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
  let stop = false
  let _isSameNode = false
  if (oldStartVNode === null) {
    
    
    callbacks.updateInfo('')
    oldStartVNode = oldChildren[++oldStartIdx]
    stop = true
  }
  // ...
  if (!stop) {
    
    
    callbacks.updateInfo('头-头比较')
    callbacks.updateCompareNodes(oldStartIdx, newStartIdx)
    _isSameNode = isSameNode(oldStartVNode, newStartVNode)
    if (_isSameNode) {
    
    
      callbacks.updateInfo(
        'key值相同,可以复用,进行patch打补丁操作。新旧节点位置相同,不需要移动对应的真实DOM节点'
      )
    }
    await wait()
  }
  if (!stop && _isSameNode) {
    
    
    // ...
    oldStartVNode = oldChildren[++oldStartIdx]
    newStartVNode = newChildren[++newStartIdx]
    stop = true
  }
  // ...
}

We use a variable to indicate whether we have entered a certain branch, and then save the result of checking whether the node can be reused in a variable, so that we can constantly check the values ​​of these two variables to determine whether to enter the subsequent In the comparison branch, the logic of the comparison is not in ifthe condition, and it can be used await. At the same time, we also use updateInfothe added prompt:

const handles = {
    
    
  // 更新提示信息
  updateInfo(tip) {
    
    
    info.value = tip
  }
}

Next, let’s look at the moving operation of the node. When the head ( oldStartIdxcorresponding oldStartVNodenode) and the tail ( newEndIdxcorresponding newEndVNodenode) are compared and found to be reusable, after the patch is completed, the oldStartVNodecorresponding real DOMelement needs to be moved to the position of oldEndVNodethe corresponding real DOMelement, that is Insert in front of oldEndVNodethe corresponding real DOMback one node:

if (!stop && _isSameNode) {
    
    
  // 头-尾
  patchVNode(oldStartVNode, newEndVNode)
  // 把oldStartVNode节点移动到最后
  el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
  // 更新指针
  oldStartVNode = oldChildren[++oldStartIdx]
  newEndVNode = newChildren[--newEndIdx]
  stop = true
}

Then we can call the method of moving nodes of our simulated list immediately after the method insertBeforemoves the real elements:DOM

if (!stop && _isSameNode) {
    
    
  // ...
  el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
  callbacks.moveNode(oldStartIdx, oldEndIdx + 1)
  // ...
}

What we want to operate is actually a list representing real DOMnodes actNodeList, so the key is to find out which one, first of all, the four node pointers at the head and tail indicate the position in the old and new VNodelists, so we can obtain oldStartIdxthe corresponding Position , and then find the corresponding node in the list through the value , and perform operations such as moving, deleting, and inserting:oldEndIdxoldVNodeListVNodekeyactNodeList

const handles = {
    
    
  // 移动节点
  moveNode(oldIndex, newIndex) {
    
    
    let oldVNode = oldVNodeList.value[oldIndex]
    let newVNode = oldVNodeList.value[newIndex]
    let fromIndex = findIndex(oldVNode)
    let toIndex = findIndex(newVNode)
    actNodeList.value[fromIndex] = '#'
    actNodeList.value.splice(toIndex, 0, oldVNode)
    actNodeList.value = actNodeList.value.filter(item => {
    
    
      return item !== '#'
    })
  }
}

const findIndex = (vnode) => {
    
    
  return !vnode
    ? -1
    : actNodeList.value.findIndex(item => {
    
    
        return item && item.data.key === vnode.data.key
      })
}

Other insert nodes and delete nodes are similar:

Insert node:

const handles = {
    
    
  // 插入节点
  insertNode(newVNode, index, inNewVNode) {
    
    
    let node = {
    
    
      data: newVNode.data,
      children: newVNode.text
    }
    let targetIndex = 0
    if (index === -1) {
    
    
      actNodeList.value.push(node)
    } else {
    
    
      if (inNewVNode) {
    
    
        let vNode = newVNodeList.value[index]
        targetIndex = findIndex(vNode)
      } else {
    
    
        let vNode = oldVNodeList.value[index]
        targetIndex = findIndex(vNode)
      }
      actNodeList.value.splice(targetIndex, 0, node)
    }
  }
}

delete node:

const handles = {
    
    
  // 删除节点
  removeChild(index) {
    
    
    let vNode = oldVNodeList.value[index]
    let targetIndex = findIndex(vNode)
    actNodeList.value.splice(targetIndex, 1)
  }
}

The execution position of these methods in diffthe function is actually the place where the method is executed insertBefore. removeChildYou can refer to the source code of this article for details, so I won’t introduce it here.

In addition, you can also highlight the elements that have been compared, the elements that will be added, the elements that will be deleted, etc., the final effect:

For time reasons, currently only the effect of the double-ended algorithm has been realized diff, and the animation process of the fast algorithm will be added in the future diff. If you are interested, you can pay attention~

Warehouse: https://github.com/wanglin2/VNode_visualization .

Guess you like

Origin blog.csdn.net/sinat_33488770/article/details/127604224