Vue3 source code parsing – bidirectional binding

In Vue, bidirectional binding mainly refers to the change of corresponding DOM after the change of responsive data, and the way that DOM change affects responsive data with also belongs to bidirectional binding. Its essence is a series of changes caused by the change of responsive data, including the trigger of responsive methods, the generation of new VNodes, The diff process of the old and new VNodes corresponds to the generation and rendering of DOM nodes that need to be changed. The overall process is shown in the figure:

Let’s modify the demo code from the previous chapter to trigger a reactive data change, as shown below:

<div id="app">
  <div>
    {{name}}
  </div>
  <p>123</p>
</div>
const app = Vue.createApp({
  data(){
    return {
      attr : 'attr'.name : 'abc'}},mounted(){
    setTimeout(() = >{
        // Change the responsive data
        this.name = 'efg'
    },1000*5)
    
  }
}).mount("#app")
Copy the code

When this. Name is modified, the corresponding name value on the page will change accordingly. The whole process to the final DOM change is executed at the source level as shown below (from bottom to top).

The above process includes reactive method triggering, generation of new VNodes, diff process of comparison between old and new VNodes, generation and rendering of DOM nodes that need to be changed. When the setElementText method is executed, the DOM of the page is modified as follows:

setElementText: (el, text) = > {
  el.textContent = text// Change to efg
}
Copy the code

Below, explain these processes one by one.

Responsive triggering

In the previous reactive principle, listening will be collected when creating reactive data. In the source code reactivity/ SRC /effect.ts, track method, its core code is as follows:

export function track(target: object, type: TrackOpTypes, key: unknown) {...// Get the depsMap of the current target object
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the deP dependency for the current key
  let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) {// Collect current effects as dependencies
    dep.add(activeEffect)
    // The current effect collects the DEP collection as a dependency
    activeEffect.deps.push(dep)
  }
}
Copy the code

After collecting the listener, you get the targetMap, and when the listener trigger is triggered, you get the current target from the targetMap.

Name is a responsive data, so the trigger name value changes, will enter the corresponding Proxy set method in the handler for this object, in source reactivity/SRC/baseHandlers ts, its core code as shown below:

function createSetter() {...// Trigger the listener
   trigger(target, TriggerOpTypes.SET, key//name, value//efg, oldValue//abc). }Copy the code

In the source code reactivity/ SRC /effect.ts, trigger method, its core code is as follows:

export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {...// Get the dependency mapping table for the current target
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }
  // Declare a collection and method to add the dependency collection corresponding to the current key
  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > effects.add(effect))
    }
  }

  // Declare a scheduling method
  const run = (effect: ReactiveEffect) = > {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // Add the current key dependency to effects in different ways depending on the type.// Loop over runs the dependency in a certain scheduling manner
  effects.forEach(run)
}
Copy the code

The trigger method, summed up, does the following:

  • First, obtain the dependency mapping table corresponding to the current target. If no, the target has no dependency. Otherwise, go to the next step.
  • You then declare a collection of ReactiveEffects and a method to add elements to the collection.
  • Add a dependency for the current key to a ReactiveEffect in different ways depending on the type.
  • Declare a scheduling method, using different scheduling run methods based on the parameters we passed into the ReactiveEffect function, and loop through the execution.

The main purpose of the trigger method is to schedule the call of the method, that is, to run a ReactiveEffect object bound to the run method. How to bind the corresponding run method? Let’s look at the implementation of ReactiveEffect, in the source code reactivity/ SRC /effect.ts, its core code is as follows:

export class ReactiveEffect<T = any> {
  ...
  constructor(
    public fn: () => T, // Pass in the callback method
    public scheduler: EffectScheduler | null = null.// Scheduling functionscope? : EffectScope |null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    if(! effectStack.includes(this)) {
      try{...// Perform the binding method
        return this.fn()
      } finally{... }}}}Copy the code

Renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts ();

/ / setupRenderEffect () method.const effect = new ReactiveEffect(
  componentUpdateFn,// Bind to the run method, which includes VNode generation logic
  () = > queueJob(instance.update),
  instance.scope // track it in component's effect scope
)
Copy the code

Through ReactiveEffect, reactive logic and VNode logic are linked, which itself is an event object based on publish/subscribe mode. Track is responsible for subscription, i.e. collecting listener; trigger is responsible for publishing, i.e. triggering listener; Effect is a bridge, storing event data.

ReactiveEffect also exposes the effect method of the Composition API, which can be customized to add listener collection. In the source code of reactivity/ SRC /effect.ts, the core code is as follows:

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // Create a ReactiveEffect object
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}
Copy the code

When used, the following code looks like:

// this. Name changes when triggered here
Vue.effect(() = >{
  console.log(this.name)
})
Copy the code

Based on the explanation of the principle of responsiveness in the previous chapter, we summarize the process of the triggering of responsiveness into a flow chart for readers to understand, as shown in the figure.

When the reactive triggering is complete, the VNode generation process is initiated.

Generate new VNode

Renderer.ts () {componentUpdateFn () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {componentUpdateFn ();}

const componentUpdateFn = () = > {
  // Find the corresponding DOM mount for the first rendering, no need to compare old and new vNodes
  if(! instance.isMounted) { ... instance.isMounted =true. }else {
    let { next, bu, u, parent, vnode } = instance
    let originNext = next
    let vnodeHook: VNodeHook | null | undefined
    
    // Determine if the update was brought by the parent
    if (next) {
      next.el = vnode.el
      // Child component updates
      updateComponentPreRender(instance, next, optimized)
    } else {
      next = vnode
    }
    ...
    // Get a new VNode (based on the new responsive data, execute render method to get the VNode)
    const nextTree = renderComponentRoot(instance)
    // Get the old VNode from the subTree field
    const prevTree = instance.subTree
    // Assign the new value to the subTree field
    instance.subTree = nextTree
    
    // Compare the old and new vNodes
    patch(
      prevTree,
      nextTree,
      / / teleport judgmenthostParentNode(prevTree.el!) ! ./ / fragments
      getNextHostNode(prevTree),
      instance,
      parentSuspense,
      isSVG
    )
  }
}
Copy the code

RenderComponentRoot is also used in the previous chapter of the virtual DOM. It will execute the render method of the component internally. The render method can be used to obtain the new VNode. Assign the new VNode to the subTree field for next comparison.

Then the patch method will be entered to conduct the comparison diff of virtual DOM.

Virtual DOM diff

The core of the diff process of the virtual DOM is the patch method, which mainly uses patchFlag (or type) in the compile stage to deal with updates under different circumstances. This can also be understood as a divide-and-conquer strategy. In this method, DOM nodes are not updated by violence directly through the current VNode. Instead, patchflags of the old and new vNodes are compared according to the situation, and then the different attributes or nodes are found out through the comparison results, so as to reduce unnecessary overhead and improve performance.

In the process of patch, the following things are mainly accomplished:

  • Create a node to be added.
  • Remove an obsolete node.
  • Move or modify nodes that need to be updated.

In the whole process, patchFlag will be used for judgment. In the process from AST tree to render and then to VNode generation, corresponding patchFlag will be added according to the node type. It is not enough to have patchFlag alone, but also depends on the setting of shapeFlag. The corresponding createVNode method in the source code is as follows:

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
) :VNode {
  const shapeFlag = isString(type)
  ...
  const vnode = {
      __v_isVNode: true["__v_skip" /* SKIP */] :true,
      type,
      props,
      key: props && normalizeKey(props),
      ref: props && normalizeRef(props),
      scopeId: currentScopeId,
      children: null.component: null.suspense: null.ssContent: null.ssFallback: null.dirs: null.transition: null.el: null.anchor: null.target: null.targetAnchor: null.staticCount: 0,
      shapeFlag,
      patchFlag,
      dynamicProps,
      dynamicChildren: null.appContext: null}; .return vnode
}
Copy the code

The _createVNode method is used to standardize vNodes and add corresponding shapeFlag and patchFlag. The shapeFlag value is a number, and each shapeFlag represents a different VNode type. The shapeFlag value is based on the NodeType of the AST tree, so the shapeFlag value is similar to the NodeType, as shown below:

export const enum ShapeFlags {
  ELEMENT = 1./ / element string
  FUNCTIONAL_COMPONENT = 1 << 1.// 2 function
  STATEFUL_COMPONENT = 1 << 2.// 4 object
  TEXT_CHILDREN = 1 << 3./ / 8 text
  ARRAY_CHILDREN = 1 << 4./ / 16 array
  SLOTS_CHILDREN = 1 << 5./ / 32 slot
  TELEPORT = 1 << 6.// 64 teleport
  SUSPENSE = 1 << 7.// 128 suspense
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8.// 256 Keep alive component
  COMPONENT_KEPT_ALIVE = 1 << 9.// 512 Keep alive component
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT / / component
}
Copy the code

PatchFlag means that different policies are adopted during update, and each meaning is as follows:

export const enum PatchFlags {
  // Dynamic text content
  TEXT = 1./ / dynamic class
  CLASS = 1 << 1.// Dynamic style
  STYLE = 1 << 2./ / dynamic props
  PROPS = 1 << 3.// There is a dynamic key, that is, the key of the props object is not certain
  FULL_PROPS = 1 << 4.// Merge events
  HYDRATE_EVENTS = 1 << 5.// children fragment in order
  STABLE_FRAGMENT = 1 << 6.// children fragment with key node
  KEYED_FRAGMENT = 1 << 7.// Fragment of children without key
  UNKEYED_FRAGMENT = 1 << 8.// Only non-props need patch, such as' ref '
  NEED_PATCH = 1 << 9.// Dynamic slots
  DYNAMIC_SLOTS = 1 << 10.// Special flags, which are not used in optimization, are built-in special flags. SPECIAL FLAGS// Indicates that it is a static node, its content never changes, and there is no need to diff its children during hydrate
  HOISTED = -1.// The diff used to indicate a node should end
  BAIL = -2,}Copy the code

Including shapeFlag and patchFlag, which have the same meaning as its name, are actually a series of flags to identify how a node should be updated. CLASS = 1 << 1 is used to represent the bit operation, that is, each patchFlag is represented by a certain digit in binary. So that we can more convenient extension, for example, the TEXT | CLASS can get 0000000011, this value can be said that he has the characteristics of the TEXT, namely also has the characteristics of the CLASS, if need to add a new flag, num is left with a new number one can directly: 1 < < num.

ShapeFlag can be understood as the VNode type, while patchFlag is more like the VNode change type.

For example, in the demo code, we bind the responsive variable attr to props as follows:

. <div :data-a="attr"></div>
...
Copy the code

The patchFlag obtained is 8 (1<<3). In the source code compiler – core/SRC/transforms/transformElement ts can see the corresponding Settings in logic, the core code is as follows:

.// Multiple values can be set by bit each time
if (hasDynamicKeys) {
  patchFlag |= PatchFlags.FULL_PROPS
} else {
  if(hasClassBinding && ! isComponent) { patchFlag |= PatchFlags.CLASS }if(hasStyleBinding && ! isComponent) { patchFlag |= PatchFlags.STYLE }if (dynamicPropNames.length) {
    patchFlag |= PatchFlags.PROPS
  }
  if (hasHydrationEventBinding) {
    patchFlag |= PatchFlags.HYDRATE_EVENTS
  }
}
Copy the code

Renderer. ts/run-time core/ SRC /renderer.ts /renderer.ts

const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = false
) = > {
  // The old and new vNodes are the same object, so no comparison is made
  if (n1 === n2) {
    return
  }
  // Patching & vNodes that are not of the same type are uninstalled from the node tree
  if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
    n1 = null
  }
    // PatchFlag is the BAIL type
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }

  const { type, ref, shapeFlag } = n2
  switch (type) { // Determine according to the Vnode type
    case Text: // Text type
      processText(n1, n2, container, anchor)
      break
    case Comment: // Comment type
      processCommentNode(n1, n2, container, anchor)
      break
    case Static: // Static node type
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      }
      break
    case Fragment: / / type of fragments
      processFragment(/* Ignore the argument */)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) { // Element type
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) { // Component type. }else if (shapeFlag & ShapeFlags.TELEPORT) { / / TELEPORT type. }else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { / / type of SUSPENSE. }}}Copy the code

N1 is the old VNode and n2 is the new VNode. If the old and new vnodes are the same object, no comparison will be made. If the old node exists and the new and old nodes are not of the same type, the old node will be uninstalled from the node tree. In this case, ShapeFlag is used. For common HTML ELEMENT types, the default branch is entered. Let’s take ELEMENT as an example and enter the processElement method. Renderer. Ts/run-time core/ SRC /renderer.ts

const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) = > {
  // If the old node does not exist, render directly
  if (n1 == null) {
    mountElement(
      n2,
      container,
      anchor
      ...
    )
  } else {
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}
Copy the code

The logic of the processElement method is relatively simple, but adds another layer of judgment. When there are no old nodes, the render process goes directly, which is what happens when the root instance is called to initialize createApp. Renderer.ts/runtime-core/ SRC /renderer.ts /renderer.ts

const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) = > {
  let { patchFlag, dynamicChildren, dirs } = n2
  ...

  // Trigger some hooks
  if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
    invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
  }
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')}...// When a new VNode has a dynamic node, update the dynamic node first (efficiency improvement)
  if(dynamicChildren) { patchBlockChildren( n1.dynamicChildren! , dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds )if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
      traverseStaticChildren(n1, n2)
    }
  } else if(! optimized) {/ / the diff
    // full diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false)}// Use different update logic according to different patchFlag
  if (patchFlag > 0) {

    if (patchFlag & PatchFlags.FULL_PROPS) {
      // If the element props contains dynamic keys, a full comparison is required
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG
      )
    } else {
      / / dynamic class
      if (patchFlag & PatchFlags.CLASS) {
        if(oldProps.class ! == newProps.class) { hostPatchProp(el,'class'.null, newProps.class, isSVG)
        }
      }

      / / dynamic style
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
      }

      / / dynamic props
      if (patchFlag & PatchFlags.PROPS) {
        // if the flag is present then dynamicProps must be non-null
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          // #1471 force patch value
          if(next ! == prev || key ==='value') {
            hostPatchProp(
              el,
              key,
              prev,
              next,
              isSVG,
              n1.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }
    }

    // Interpolate text
    if (patchFlag & PatchFlags.TEXT) {
      if(n1.children ! == n2.children) { hostSetElementText(el, n2.childrenas string)
      }
    }
  } else if(! optimized && dynamicChildren ==null) {
    / / the diff
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG
    )
  }
  ...
}
Copy the code

At the beginning of the processElement method, some hook functions are executed, and the new node is checked to see if it has identified dynamic nodes (i.e. the static promotion part of the optimization, separating the dynamic nodes from the static nodes). If so, the new node is updated first (no comparison is required, so it is faster). Then update the props, style, and class of the node using the patchProps method. The main logic is as follows:

  • If patchFlag is FULL_PROPS, it indicates that the element may contain dynamic keys and needs to perform full props diff.
  • When patchFlag is CLASS, when the classes of the old and new nodes are inconsistent, CLASS atch will be performed. When the CLASS attributes of the new and old nodes are completely consistent, no operation is required. This Flag is added when an element has a dynamic class binding.
  • When patchFlag is a STYLE, the STYLE will be updated, which will be done every patch. This Flag will be added when there is a dynamic STYLE binding.
  • When patchFlag is PROPS, note that this Flag is added when the element has dynamic properties or attrs bindings. Unlike class and style, these dynamic prop or attrs keys are saved for faster iteration.
  • When patchFlag is TEXT, hostSetElementText will be called for updating if the TEXT of the child nodes in the old and new nodes changes. This Flag is added when the child nodes of the element contain only dynamic text.

The corresponding methods of each patchFlag will eventually enter the logic of DOM operation. For example, for STYLE update, the setStyle method will be entered. In the source runtime-dom/ SRC /modules/style.ts, the core code is as follows:

function setStyle(style: CSSStyleDeclaration, name: string, val: string | string[]) {
  if (isArray(val)) { // Multiple styles can be set simultaneously
    val.forEach(v= > setStyle(style, name, v))
  } else {
    if (name.startsWith(The '-')) {
      // custom property definition
      style.setProperty(name, val)/ / dom operation
    } else {
      const prefixed = autoPrefix(style, name)
      if (importantRE.test(val)) {
        / /! important
        style.setProperty(
          hyphenate(prefixed),
          val.replace(importantRE, ' '),
          'important')}else {
        style[prefixed as any] = val
      }
    }
  }
}
Copy the code

hi

text hi is also a child node. For the child node, the patchChildren method is used. Renderer. Ts/run-time core/ SRC /renderer.ts

const patchChildren: PatchChildrenFn = (
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized = false
) = > {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children

  const { patchFlag, shapeFlag } = n2
  if (patchFlag > 0) {
    // The key value is Fragment: KEYED_FRAGMENT
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      // this could be either fully-keyed or mixed (some keyed some not)
      // presence of patchFlag means children are guaranteed to be arrays
      patchKeyedChildren(
      ...
      )
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // The key value is UNKEYED_FRAGMENT
      patchUnkeyedChildren(
      ...
      )
      return}}// The new node is a text type child node (single child node)
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // The old node is an array type, so the new node is overwritten
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    // Set a new node
    if(c2 ! == c1) { hostSetElementText(container, c2as string)
    }
  } else {
    // The new node is an array type child node.
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // New and old metropolitan array types, then full diff
        patchKeyedChildren(
        ...
        )
      } else {
        // no new children, just unmount old
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)}}else {
      // Set an empty string
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, ' ')}// mount new if array
      if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( ... ) }}}}Copy the code

In the above code, the patchFlag is first judged:

  • If patchFlag is a Fragment: KEYED_FRAGMENT with a key value, patchKeyedChildren is called to continue processing child nodes.
  • When patchFlag is UNKEYED_FRAGMENT without key value, patchUnkeyedChildren is called to process the child node without key value.

Then judge according to shapeFlag:

  • If the new child node is a text type and the old child node is an array type (containing multiple children), the child node of the old node is simply unloaded and replaced with the new node.
  • If the old child node type is array type, and if the new child node is also array type, then patchKeyedChildren is called for full diff. If the new child node is not array type, it indicates that there is no new child node, and the old node can be directly uninstalled from the tree.
  • If the old child node is a text type, you can be sure that the new child node is not a text type because you have already determined whether the new child node is a text type in the first place, and you can simply set the element’s text to an empty string.
  • If the type of the new child node is array but that of the old child node is not array, mount the new child node to the tree.

No matter how complicated the nesting of node arrays is, in the end, it all comes down to basic DOM operations, including creating nodes or deleting nodes, modifying node properties, etc., but the core is how to find the nodes that need to be changed between the old and new trees, and that’s the heart of diff. The real Diff needs to get inside patchUnkeyedChildren and patchKeyedChildren to find out. Renderer. Ts/SRC/patchUnkeyedChildren /renderer.ts

const patchUnkeyedChildren = () = >{... c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARRconst oldLength = c1.length
  const newLength = c2.length
  // Get the minimum length of the old and new nodes
  const commonLength = Math.min(oldLength, newLength)
  let i
  // Patch through the old and new nodes
  for (i = 0; i < commonLength; i++) {
    Clone a new VNode if it has already been mounted. Otherwise, create a new VNode
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    patch()
  }
  // If the number of old nodes is greater than the number of new nodes
  if (oldLength > newLength) {
    // Uninstall the redundant nodes directly
    unmountChildren( )
  } else {
    // old length < new length => Create new length
    mountChildren()
  }
}
Copy the code

The main logic is to first get the shortest public length of the old and new nodes, then traverse the public part, recursively perform patch method for the public part again, if the number of old nodes is greater than the number of new nodes, directly uninstall the redundant nodes, or new nodes.

It can be seen that in the case of no key, DIff is relatively simple, but the performance is relatively low. DOM reuse is rarely realized, and more nodes are created and deleted. This is why Vue recommends adding unique key values to array nodes.

Below is patchKeyedChildren method in source runtime – core/SRC/renderer. Ts, its core code as shown below:

const patchKeyedChildren = () = > {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // prev ending index
  let e2 = l2 - 1 // next ending index

  // 1. Iterate through the head, continue with the same node, and break out of the loop with different nodes
  while(i <= e1 && i <= e2) {... }// 2. Perform tail traversal, continue when the same node is encountered, and jump out of loop when different nodes are encountered
  while(i <= e1 && i <= e2) {... }// 3. If the old node has been traversed and the new node is still available, the new node is traversed and added
  if (i > e1) {
    if (i <= e2) {...}
  }

  // 4. If the new node has been traversed and the old node is still available, unmount the new node
  else if (i > e2) {
    while (i <= e1) {...}
  }

  // 5. The old and new nodes are not traversed completely
  else {
    // 5.1 Create a map to store key-value pairs for the remaining new nodes. The mapping is key => index
    // 5.2 Traverse the remaining old nodes, compare the old and new data, and remove the unused old nodes
    // 5.3 Get the longest increment subsequence to move or mount}}Copy the code

The patchKeyedChildren method is the core of the whole DIFF, and it contains specific algorithms and logic, which is complicated to explain in code. Here, a simple example is used to illustrate what this method does. There are two arrays as shown in the following code:

/ / the old array [" a ", "b", "c", "d", "e", "f", "g", "h"] / / new array (" a ", "b", "d", "f", "c", "e", "x", "y", "g", "h"]Copy the code

Each element in the array above represents a key. Perform the following steps:

  • 1. Compare from beginning to end, [a, B] is sameVnode, enter patch, stop at [C].
  • 2. Compare from end to end, [h,g] is sameVnode, enter patch, and stop at [f].
  • 3. Check whether the old data has been compared. The unnecessary information is added and needs to be mounted.
  • 4. Check whether the new data has been compared. Unnecessary information is deleted and unmount is required.
    • To enter here, the order is out of order, to enter 5:
    • 5.1 to create a new data haven’t compare the index Map: [{d: 2}, {f: 3}, {c: 4}, {e: 5}, {x: 6}, {y: 7}].
    • 5.2 According to the length of uncompared data, build an array [0,0,0,0,0] filled with 0, and then loop through the old remaining data to find the index ARr of the uncompared data :[4(d),6(f),3(c),5(e),0,0]. If it is not found in the new remaining data, it indicates that it is deleted and unmounted. If you find it, you can match it with the previous patch.
    • Index arr = 0; index arR = 0; index arR = 0; index arR = 0;
      • Let’s move f before C.
      • Let’s move d in front of f.
      • After moving, c will naturally move to the front of e, which can be found by the previous arr index by the longest increasing subsequence, so that the corresponding c and e of [3,5] need not move.

So far, this is the core content and principle of diff in the whole patchKeyedChildren method. Of course, there are many code details. Readers who are interested can read the complete source code of patchKeyedChildren.

The actual DOM modification is complete

No matter how complex the nested node array is, it will eventually fall to the basic DOM operations, including creating or deleting nodes, modifying node properties, etc. When the result is obtained, the corresponding DOM operation method will be called. This part of the logic in the source code runtime-dom\ SRC \ nodeops.ts. Store are some tools and methods, the core code is as follows:

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  // Insert elements
  insert: (child, parent, anchor) = > {
    parent.insertBefore(child, anchor || null)},// Delete elements
  remove: child= > {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },
  // Create the element
  createElement: (tag, isSVG, is, props): Element= >{... },// Create text
  createText: text= > doc.createTextNode(text),
  // Create a comment
  createComment: text= > doc.createComment(text),
  // Set the text
  setText: (node, text) = > {
    node.nodeValue = text
  },
  // Set the text
  setElementText: (el, text) = > {
    el.textContent = text
  },
  parentNode: node= > node.parentNode as Element | null.nextSibling: node= > node.nextSibling,

  querySelector: selector= > doc.querySelector(selector),
  // Set element attributes
  setScopeId(el, id) {
    el.setAttribute(id, ' ')},/ / clone DOM
  cloneNode(el){... },// Insert static content, including processing SVG elements
  insertStaticContent(content, parent, anchor, isSVG){... }}Copy the code

This part of the logic is normal DOM operations, relatively simple, you can directly read the source code. This completes the bidirectional binding process.