diff algorithm in depth?

Keywords: Javascript Algorithm

1, Foreword

A classmate asked: could you elaborate on the diff algorithm.

To put it simply: diff algorithm is an optimization method, which compares the difference between the two modules before and after, and the process of repairing (updating) the difference is called patch, also known as patching.

The article mainly solves the following problems:

  • 1. Why this diff algorithm?

  • 2. diff algorithm for virtual dom

  • 3. Why use virtual dom?

  • 4. Complexity and characteristics of diff algorithm?

  • 5. How is vue's template file compiled and rendered?

  • 6. Is there any difference between diff in vue2.x and vue3.x

  • 7. The source of diff algorithm is snabbdom algorithm

  • 8. What are the differences between diff algorithm and snabbdom algorithm?

2, Why this diff algorithm?

Because diff algorithm is the key core of vue2.x, vue3.x and react, understanding diff algorithm is more helpful to understand the essence of each framework.

When it comes to "diff algorithm", we have to say "virtual Dom", because these two are closely related.

For example:

  • vue's responsive principle?

  • How is the template file of vue compiled?

  • Introduce the Virtual Dom algorithm?

  • Why use virtual dom?

  • diff algorithm complexity and the biggest characteristics?

  • What is the node comparison in the diff algorithm of vue2.x?

wait

3, diff algorithm for virtual dom

Let's talk about virtual DOM first, which is to realize DOM through JS simulation. The next difficulty is how to judge the difference between old objects and new objects.

Dom is a multi tree structure. If you need to completely compare the differences between the two trees, the time complexity of the algorithm is O(n ^ 3), which is difficult to accept, especially in the case of large N, so the React team optimized the algorithm and realized the complexity of O(n) to compare the differences.

The key to achieving O(n) complexity is to only compare nodes in the same layer rather than cross layer comparison, which also takes into account that cross layer mobile DOM elements are rarely used in actual business.

The steps of virtual DOM difference algorithm are divided into two steps:

  • First, traverse the object from top to bottom, from left to right, that is, the depth of the tree. In this step, an index will be added to each node to facilitate the final rendering of the difference

  • Once the node has child elements, judge whether the child elements are different

3.1 diff algorithm in Vue

In the actual diff algorithm comparison, there are mainly five kinds of rules for node comparison

  • 1. If the old and new vnodes are static, their key s are the same (representing the same node), and the new vnodes are clone or marked with once (marked with v-once attribute, only rendered once), you only need to replace elm and componentInstance.

  • 2. If the new and old nodes have children child nodes, diff the child nodes and call updateChildren. This updateChildren is also the core of diff.

  • 3. If the old node has no child nodes and the new node has child nodes, clear the text content of the old node DOM first, and then add child nodes to the current DOM node.

  • 4. When the new node has no child nodes and the old node has child nodes, all child nodes of the DOM node are removed.

  • 5. When the new and old nodes have no child nodes, they are just text replacement

Partial source code https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L501 As follows:

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  if (oldVnode === vnode) {
    return;
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    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;
  }
  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)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode "i");
    if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
  }
  if (isUndef(vnode.text)) {
    //  Child nodes are defined and different, which are compared with diff algorithm
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
      //  The new node has child elements. Old node has no
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        //  Check key
        checkDuplicateKeys(ch);
      }
      //  Clear the text attribute of the old node
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
      //  Add new Vnode
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      //  If the child node of the old node has content, the new node does not. Then directly delete the content of the sub element of the old section
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1);
      //  As above. Just judge whether it is a text node
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '');
    }
    //  If the text node is different, replace the node content
  } 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);
  }
}

3.2 React diff algorithm

In the input parameter of the reconcileChildren function

workInProgress.child = reconcileChildFibers(
  workInProgress,
  current.child,
  nextChildren,
  renderLanes,
);
  • workInProgress: passed in as the parent node, the return of the newly generated first fiber will be directed to it.

  • current.child: old fiber node. diff will compare the newly generated ReactElement with it when generating a new fiber node.

  • nextChildren: the newly generated ReactElement will be used as the standard to generate a new fiber node.

  • renderLanes: the rendering priority will eventually be attached to the lanes attribute of the new fiber.

The two main bodies of diff are: old fiber (current.child) and new children (next children, new ReactElement). They are two different data structures.

Partial source code

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
  /* * returnFiber: currentFirstChild Parent fiber node of
   * currentFirstChild: WIP (fiber) node currently executing update task
   * newChildren: The new ReactElement node rendered by the render method of the component
   * lanes: Priority correlation
   * */
  //  resultingFirstChild is the first fiber in the new fiber chain after diff.
  let resultingFirstChild: Fiber | null = null;
  //  resultingFirstChild is the first fiber of the new linked list.
  //  previousNewFiber is used to connect subsequent new fibers after the first fiber
  let previousNewFiber: Fiber | null = null;

  //  oldFiber node, and the new child node will be compared with it
  let oldFiber = currentFirstChild;
  //  Storage fixed node location
  let lastPlacedIndex = 0;
  //  Stores the index of the new node traversed
  let newIdx = 0;
  //  Record the next node of the oldFiber currently traversed
  let nextOldFiber = null;

  //  This round of traversal always processes node updates, and determines whether to interrupt traversal according to whether the node can be reused
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    //  The newChildren traversal is completed, but the oldFiber chain has not been traversed. At this time, the traversal needs to be interrupted
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      //  Use nextOldFiber to store the next node of the currently traversed oldFiber
      nextOldFiber = oldFiber.sibling;
    }
    //  Generate a new node and judge whether the key and tag are the same in updateSlot
    //  For DOM type elements, key   and   Only when the tag s are the same can oldFiber be reused
    //  And return out, otherwise return null
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);

    //  newFiber is   null description   key   or   tag   Different nodes are not reusable and interrupt traversal
    if (newFiber === null) {
      if (oldFiber === null) {
        //  oldFiber   null indicates that the oldfiber is also traversed at this time
        //  This is the following scenario, and D is the new node
        //  used   A  -  B  -  C
        //  new   A  -  B  -  C  -  D   oldFiber  =  nextOldFiber;
      }
      break;
    }
    if (shouldTrackSideEffects) {
      //  shouldTrackSideEffects   true indicates the update process
      if (oldFiber && newFiber.alternate === null) {
        //  newFiber.alternate   Equivalent to   oldFiber.alternate
        //  oldFiber is a WIP node and its alternate   namely   current node
        //  The old fiber exists, and the updated new fiber node does not have a current node,
        //  It indicates that there will be no current node on the screen after the update, and the WIP after the update
        //  The node will be called the current node, so you need to delete the existing WIP node
        deleteChild(returnFiber, oldFiber);
      }
    }
    //  Record the location of the fixed node
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    //  Connect the new fiber into a one-way linked list with sibling as the pointer
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    //  Point the oldFiber node to the next node and move synchronously with the traversal of newChildren
    oldFiber = nextOldFiber;
  }

  //  Process node deletion. After traversing the new child node, it indicates that the remaining oldFiber is useless and can be deleted
  if (newIdx === newChildren.length) {
    //  After the newChildren traversal, delete the remaining nodes in the oldFiber chain
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  //  Process new nodes. After the old traversal, all that can be reused are reused, so it means that the new ones are newly inserted
  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      //  Create a new Fiber node based on the newly generated ReactElement
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      //  Record the location of the fixed node lastPlacedIndex
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      //  Connect the newly generated fiber node into a one-way linked list with sibling as the pointer
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
  //  In this case, the remaining old child nodes are put into a map with the key as the key and the value as the oldFiber node
  //  In this way, when creating a new fiber node based on the oldFiber node, you can quickly find the oldFiber through the key
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  //  Node movement
  for (; newIdx < newChildren.length; newIdx++) {
    //  Create a new fiber based on the oldFiber node in the map
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          //  Because the remaining nodes in newChildren may be the same as the oldFiber node, but the location has changed,
          //  But it may also be new

          //  If the alternate of newFiber is not empty, it means that newFiber is not new.
          //  This means that it is created based on the oldFiber node in the map, which means that the oldFiber has been used, so it needs to be updated
          //  To delete oldFiber from the map
          existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
        }
      }

      //  Mobile node, the core of multi node diff, will really realize the movement of nodes here
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      //  Connect the new fiber into a one-way linked list with sibling as the pointer
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }
  if (shouldTrackSideEffects) {
    //  At this time, the newChildren are traversed, and all that should be moved are moved. Then delete the remaining oldFiber
    existingChildren.forEach((child) => deleteChild(returnFiber, child));
  }
  return resultingFirstChild;
}

4, Why use virtual dom?

Many times, manual optimization of DOM is indeed more efficient than virtual dom. There is no problem with manual optimization of relatively simple DOM structure. However, when the page structure is very large and the structure is very complex, manual optimization will take a lot of time, and the maintainability is not high, so everyone can not be guaranteed to have the ability of manual optimization. So far, the solution of virtual DOM came into being.

virtual dom is a solution to "solve the problem that too many operations affect performance".

virtual dom is often not the optimal operation, but it has universality and achieves a balance between efficiency and maintainability.

The meaning of virtual DOM:

  • 1. Provide a simple object to replace complex dom objects, so as to optimize dom operations

  • 2. Provide an intermediate layer, js to write ui, ios and Android, which are responsible for rendering, just like reactNative.

5, Complexity and characteristics of diff algorithm?

The diff of vue2.x is located in the patch.js file. The algorithm comes from snabbdom and the complexity is O(n). Understanding the diff process allows us to use the framework more efficiently. In fact, the diff of react is similar to that of Vue.

Biggest feature: the comparison will only be conducted at the same level, not cross level.

<!-- before -->
<div>              <!-- Level 1 -->
  <p>              <!-- Level 2 -->
    <b> aoy </b>   <!-- Level 3 -->
    <span>diff</Span>
  </p>
</div>

<!-- after -->
<div>             <!-- Level 1 -->
  <p>              <!-- Level 2 -->
    <b> aoy </b>   <!-- Level 3 -->
  </p>
  <span>diff</Span>
</div>

Compare before and after: you may expect to move < span > directly to the back of < p >, which is the best operation.

But the actual diff operation is:

  • 1. Remove < span > from < p >;

  • 2. Insert a new < span > after < p >. Because the newly added < span > is in Level 2 and the old one is in Level 3, which belongs to the comparison of different levels.

6, How is vue's template file compiled and rendered?

The diff algorithm is also used in Vue. It is necessary to understand how Vue works. Through this problem, we can well grasp which link and operation of diff algorithm in the whole compilation process, and then what is output after using diff algorithm?

Explanation:

1. mount function

The mount function mainly obtains the template and then enters the compileToFunctions function.

2. compileToFunction function

The compileToFunction function mainly compiles the template into the render function. First, read the cache. If there is no cache, call the compile method to get the string form of the render function, and generate the render function through the new Function.

//  If you have a cache, you can get it directly in the cache
const key = options && options.delimiters ? String(options.delimiters) + template : template;
if (cache[key]) {
  return cache[key];
}
const res = {};
const compiled = compile(template, options); //  compile   I'll talk about it in detail later
res.render = makeFunction(compiled.render); //adopt   new   Function   Generated by   render   Function and cache
const l = compiled.staticRenderFns.length;
res.staticRenderFns = new Array(l);
for (let i = 0; i < l; i++) {
  res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
}

// ......

return (cache[key] = res); //  Log to cache

3. compile function

The compile function compiles the template into the string form of the render function. Later we will mainly explain render

After the render method is generated, you will enter mount to update the DOM. The core logic of this method is as follows:

//  trigger   beforeMount   Lifecycle hook
callHook(vm, 'beforeMount');
//  Key: create a new one   Watcher   And assigned to   vm._watcher
vm._watcher = new Watcher(
  vm,
  function updateComponent() {
    vm._update(vm._render(), hydrating);
  },
  noop,
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
  vm._isMounted = true;
  callHook(vm, 'mounted');
}
return vm;
  • First, a new watcher object will be created (mainly to connect the template with the data). After the watcher object is created,

  • Will run the incoming method VM_ update(vm._render(), hydrating) . Where VM_ The main function of render () is to run the render method generated by the compiler and return a vNode object.

  • vm.update() compares the new vdom with the current vdom and renders the difference to the real DOM tree. (the implementation principle behind the watcher: the responsive principle of vue2.x)

The compile mentioned above is to compile the template into the string form of the render function. The core code is as follows:

export function compile(template: string, options: CompilerOptions): CompiledResult {
  const AST = parse(template.trim(), options); //1. parse
  optimize(AST, options); //2.optimize
  const code = generate(AST, options); //3.generate
  return {
    AST,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
}

The compile function mainly consists of three steps:

  • parse,

  • optimize

  • generate

Output one containing

  • AST string

  • Object string for staticRenderFns

  • The string of the render function.

Parse function: the main function is to parse the template string into AST (abstract syntax tree). The parse function of the previously defined data structure of ASTElement is to convert the structure (instruction, attribute and label) in the template into AST form and store it in ASTElement, and finally parse it to generate AST.

optimize function (src/compiler/optomizer.js): the main function is to mark static nodes. In the later patch process, compare the old and new VNode tree structure for optimization. Nodes marked as static will be directly ignored in the later diff algorithm without detailed comparison.

Generate function (src/compiler/codegen/index.js): the main function is to splice and generate the string of render function according to AST structure.

const code = AST ? genElement(AST) : '_c("div")';
staticRenderFns = prevStaticRenderFns;
onceCount = prevOnceCount;
return {
  render: `with(this){return ${code}}`, //One for the outermost package   with(this)   Then return
  staticRenderFns: currentStaticRenderFns,
};

The genElement function (src/compiler/codgen/index.js) calls different methods to generate a string return according to the properties of AST.

In summary:

This is the introduction to the three core steps in the compile function,

  • After compile, we get the string form of render function, and then get the real rendering function through new Function.

  • After the data changes, it will execute the command in the watcher_ Update function (src/core/instance/lifecycle.js)_ The update function will execute the rendering function and output the data of a new VNode tree structure.

  • Then we call the patch function to get the new VNode compared with the old VNode, and only the changed nodes will be updated to the new real DOM tree.

4. patch function

The patch function is a diff function compared with the old and new vnodes. It is mainly to optimize the dom and minimize the behavior of operating the dom through the algorithm. The diff algorithm comes from snabbdom and is the core of the idea of VDOM. Snabbdom's algorithm is optimized for dom operations with fewer cross level addition and deletion nodes. It will only be carried out at the same level without cross level comparison.

To sum up

  • The compile function mainly converts the template to ast, optimizes the ast, and then converts the AST to the string form of the render function.

  • Then get the real render function through new Function, and the render function is associated with the data through Watcher.

  • When the data changes, call the patch function, execute the render function, generate a new VNode, diff with the old VNode, and finally update the DOM tree.

7, Is there any difference between vue2.x, vue3.x and diff in React?

in general:

  • The core diff algorithm of vue2.x adopts a two terminal comparison algorithm. At the same time, it compares the two ends of the old and new children, and the nodes that can be reused with the help of key.

  • vue3.x draws lessons from some other algorithms, Inferno( https://github.com/infernojs/inferno )Solution: 1. Preprocess the same pre and post elements; 2. Once we need to move the DOM, the first thing we have to do is find the longest increasing subsequence of the source.

Determine the type when creating VNode, and use bit operation to judge the type of a VNode in the process of mount/patch. On the basis of this optimization, combined with Diff algorithm, the performance is improved.

You can take a look at the source code of vue3.x: https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js

  • react uses the key and tag to choose or reject nodes. Complex comparisons can be intercepted directly, and then degraded to simple operations such as node movement, addition and deletion.

The comparison between the old fiber and the new ReactElement node will generate new fiber nodes and mark them with effectTag. These fibers will be connected to the workInProgress tree as new WIP nodes. Therefore, the structure of the tree is determined bit by bit, and the new workInProgress node is basically finalized. After diff, the beginWork node of workInProgress node is completed, and then it will enter the completeWork phase.

8, The source of diff algorithm is snabbdom algorithm

Snabbdom algorithm: https://github.com/snabbdom/snabbdom

Positioning: a virtual DOM library focusing on simplicity, modularity, power and performance.

1. Type of Vnode defined in snabbdom

Type of Vnode defined in snabbdom( https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/vnode.ts#L12)

export interface VNode {
  sel: string | undefined; //  Abbreviation for selector
  data: VNodeData | undefined; //  The following is the content of the VNodeData interface
  children: Array<VNode | string> | undefined; //  Child node
  elm: Node | undefined; //  element, which stores the real HTMLElement
  text: string | undefined; //  If it is a text node, text is stored
  key: Key | undefined; //  The key of the node is very useful when making lists
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  is?: string; // for custom elements v1
  [key: string]: any; // for any other 3rd party module
}

2. init function analysis

Address of init function:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L63

The init() function receives a module array modules and an optional domApi object as parameters and returns a function, the patch() function.

The interface of domApi object contains many methods of DOM operation.

3. patch function analysis

Source code:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L367

  • The init() function returns a patch() function

  • The patch() function takes two VNode objects as parameters and returns a new VNode.

4. h-function analysis

Source code:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/h.ts#L33

The h() function receives multiple parameters, one of which must have a sel parameter to mount the node content into the container and return a new VNode.

9, What are the differences between diff algorithm and snabbdom algorithm?

vue2.x is not a complete snabbdom algorithm, but a vue based scenario. Some modifications and optimizations are made, mainly reflected in the part of judging key and diff.

1. In snabbdom, you can judge whether it is the same node through the key and sel. In vue, some judgments are added to judge whether the tag name is consistent, whether it is an annotation node, whether it is an asynchronous node, or whether the type is the same when it is input.

https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L35

/**
 * @param a Compared node
 * @param b  Comparison node
 * Compare whether the two nodes are the same
 * Conditions to be composed: the key is the same, the tag is the same, whether they are annotation nodes, and whether data is defined at the same time. If it is an input tag, the type must be the same
 */
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  );
}

2. diff difference. patchVnode is a function of comparing template changes. diff may be used or updated directly.

https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L404

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;
  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)) {
      // Vnode moved right
      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)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        // New element
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        );
      } else {
        //  vnodeToMove node to be moved
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          );
        }
      }
      //  vnodeToMove node to be moved
      newStartVnode = newCh[++newStartIdx];
    }
  }
  //  The old node is completed, and the new node is not completed
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    );
    //  The new is finished, the old is not finished
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}

Posted by Heywood on Thu, 28 Oct 2021 05:02:32 -0700