The following parsing is based on the [email protected] version

Vue3.0 series – Responsive

There is an overall mind map at the end

preface

Renderers are the most important part of Vue and are also very complex, including element rendering, Component rendering, text rendering, etc. Vue3 also references Teleport and Suspense. This article focuses on how the following code is eventually rendered into the browser.

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp({
    template: '<div>demo</div><App />'.components: {App}
})
Copy the code

Before introducing rendering logic, vue3.0 has already extracted dom operations, including prop handling (class, style, event, attR), into the Runtime-DOM module, while the core rendering logic is in the Runtime-core module. This makes vue3.0 easy to render on platforms other than browsers, such as applets, RN, etc.

The whole process

Here is a brief introduction to the previous process, detailed can be combined with the final mind map to view the source

  • Execute createApp to create an app instance containing global methods such as use, Component, and provide
  • Run app.mount to create a vnode for template
  • Perform patch

patch

Patch is the entry point for all rendering. Check out the code

  • n1: old vnode, render null for the first time, compare N1 and n2 for update
  • n2: The new Vnode to render
  • container: render container
  • anchorThe adjacent rendered node, n2, is rendered above the anchor for positioning
const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,isSVG = false,optimized = false
  ) = > {
    // if n1 exists and is not the same node, unload N1 and render n2 directly
    if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if(shapeFlag & ShapeFlags.TELEPORT) { ; (typeas typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if(__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (typeas typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        }
    }
  }
Copy the code

The main thing here is to call different render functions based on different types. Can be divided into:

  • processText: Processing text
  • processCommentNode: Handling comments
  • mountStaticNodeandpatchStaticNode, this is mainly to render static HTML generated by compile, used for SSR rendering
  • processFragment: deal with fragments
  • processElement: Processing node
  • processComponent: Processing components
  • type.processSuspense: Handle teleport and suspense

This section describes the implementation of processFragment, processElement, processComponent, and Teleport

Start by listing what the compiled code looks like

processFragment

DynamicChildren is a new feature of vue3.0. During compilation, dynamicChildren will be put into the dynamicChildren attribute. During patch, only the nodes in dynamicChildren need to be patched, not the static nodes. For example, _hoisted_1 above skips patch, which can be implemented in the next article

const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    // Fragment start node, empty text
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '))!
     // End node of fragment, empty text. Fragment children will be rendered in this area
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '))!
    // patchFlag is highlighted in red in the figure above. If it is smaller than patchFlag, it means that it is a static node, and patch is not required. DynamicChildren is a dynamic node
    let { patchFlag, dynamicChildren } = n2
    if (patchFlag > 0) {
      optimized = true
    }
    
    if (n1 == null) {
    // n1 == null, then render node
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // Perform patch on each child
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
    // If a patch is required with a dynamic node, only a patch dynamic node is required, which is an optimization of VUe3
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren
      ) {
      // Make patch for each dynamic nodepatchBlockChildren( n1.dynamicChildren! , dynamicChildren, container, parentComponent, parentSuspense, isSVG )if( n2.key ! =null ||
          (parentComponent && n2 === parentComponent.subTree)
        ) {
        // This is mainly because only patch dynamic node, n2 static node does not have EL, and the subsequent mount will report an error, so the el of N1 is assigned to n2.el
          traverseStaticChildren(n1, n2, true /* shallow */)}}else {
        // The fragment generated by V-for does not have dynamicChildren, because Vue considers that every node is a block and each node needs to do patch
        patchChildren(
          n1,
          n2,
          container,
          fragmentEndAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    }
  }
Copy the code

The above process mainly creates the empty text node, and then renders children directly in the empty text node. Next, we will focus on the implementation of patchChildren

patchChildren

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

    const { patchFlag, shapeFlag } = n2
    // If patchFlag is greater than 0, patchFlag needs to be made
    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
        // If the key is set, the diff algorithm is performed
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        If it is the same node, do patch. If it is not, uninstall the old node and render the new node.
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return}}// I don't understand why the following comparison, temporarily did not think of the scene
    // children has 3 possibilities: text, array or no children.
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // text children fast path
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if(c2 ! == c1) { hostSetElementText(container, c2as string)
      }
    } else {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else {
          // no new children, just unmount old
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)}}else {
        // prev children was text OR null
        // new children is array OR null
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(container, ' ')}// mount new if array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        }
      }
    }
  }

Copy the code

Todo: I don’t understand why patchFlag>0 is used and why I continue to make judgment. I haven’t thought of the scene for the time being. There are big guys think of the corresponding scene under guidance ~

The diff algorithm

Diff algorithm will be carried out in patchKeyedChildren, which is also different from VUe2, because there are too many codes, so the code is subsidized, and the process is expressed in words

vue2.x

Vue2 uses two-pointer traversal

  • Whether the first node of the old children and the first node of the new children are the same, then patch
  • If the old tail node and the new tail node are the same, patch
  • If the old tail node is the same as the new first node, patch is displayed
  • If the old first node is the same as the new last node, patch is displayed
  • Whether the key of the new node is in the old children. If yes, is it the same node? If yes, patch; otherwise, create a new node. If not, create a new node
  • Whether the traversal of the old node or the new node is complete, if not, continue the above process
  • After the traversal is complete, the unprocessed nodes in the new Choldren are inserted and the redundant nodes in the old children are deleted

vue3.x

Vue3 uses the algorithm of single pointer traversal + longest increasing subsequence

  • If n nodes in the head are the same, the patch nodes are the same
  • N nodes at the tail are the same, and nodes at the tail of patch are the same
  • If no differential node exists and the new node is redundant, mount the new node
  • If there is no differential node and the old node is redundant, unmount the redundant old node
  • There are difference nodes in the middle
    • Create the map object for key: I (key value and subscript) of the new difference node
    • The old difference node is traversed from left to right. If no match is found in the new difference node, it is unloaded; otherwise, patch is made to the matched node. At the same time, the new difference node is generated into the corresponding old difference node index array
    • The matching nodes of the new difference node and the old difference node are in order, so the new node is directly rendered from right to left without moving the node (the new node can be confirmed according to whether the object of the old and new node index generated in the previous step can match the old node).
    • If the order does not match, the longest increasing subsequence is obtained from the index mapping array of the old and new nodes, and the nodes on the increasing subsequence do not need to be moved
    • The number of new differential nodes is traversed from right to left. If the index is not in the increasing subsequence, it means that the corresponding old node needs to be moved to this position

Here’s an example:

n1: n k a b c d e f g m

n2: n k e b a d f c g m

  1. patch nk
  2. patch gm
  3. Create a map object: keyToNewIndexMap = {e: 2, b: 3, a: 4, D: 5, f: 6, c: 7}
  4. righta b c d e fMake patch(the node position is still the original, but the node properties are updated). Create array: newIndexToOldIndexMap = [7, 4, 3, 6, 8, 5]
  5. If newIndexToOldIndexMap is not an increasing sequence, the longest increasing sub-sequence can be calculated: [4, 6, 8], i.eb d fThe positions of the three nodes do not need to change
  6. N2 then moves the node from right to left.
    1. Start by changing the position of C from 5(bAfter) move togIn front of;
    2. becausefIn an increasing subsequence, sofYou don’t need to move;
    3. becausedIn an increasing subsequence, sodYou don’t need to move;
    4. willaMoved todIn the front of the
    5. becausebIn an increasing subsequence, sobYou don’t need to move;
    6. The final will beeMoved tobJust in front of

processElement

MountElement and patchElement are invoked in processElement. The former is the rendering process and the latter is the update process

mountElement

This piece of code is relatively simple and mainly lists its steps

  • Create the corresponding EL
  • Set the content of el, if the content is text, create a file to insert EL, if the content is array, patch each child, again follow the judgment logic
  • Created hooks that trigger the directive
  • Handles props for el, including class, style, event, etc
  • Set el’s scopedId
  • Triggers the beforeMount hook of directive
  • To perform the transition. BeforeEnter (el)
  • Insert el into the page
  • To perform the transition. Enter (el)
  • Mounted hook that triggers the directive

patchElement

  • Triggers the beforeUpdate hook of directive
  • patchProp
    • There is an optimization because dynamic prop is marked at compile time. Specific patches can be removed according to class, style, prop, etc. If it is full_props (with dynamic keys), the following patches need to be made
  • If there are dynamic nodes, thenpatchBlockChildrenOr go if you need topatchChildren

processComponent

There are also mountComponent and updateComponent. Too much content, subsidize the source code

mountComponent

  • CreateComponentInstance, which creates a component instance. Emit = emit. Bind (null, instance)
  • SetupComponent (instance), installs the component
    • initProps(instance, props, isStateful, isSSR)
      • Handle prop, assign to instance.prop and instance.attrs, and undeclared attributes and emit events will be placed in attrs
      • This is going to be responsive to props. In production mode, this props is passed to the first parameter of setup (dev mode wraps another layer with readOnly), so modifying props directly in setup also triggers an update of the current component, but not the parent component, and the value of the parent component does not change

    • InitSlots (instance, children), which binds the slot contents to instance instances
    • Create a proxy for CTX (_: instance) that contains props, setupResult, data, $, and so on.
    • Execute the setup function to get the result. If the result is a function, assign instance.render = setupResult as the render function. If it is an object, instance.setupState = proxyRefs(setupResult). Proxy result, do unref processing
  • finishComponentSetup
    • If the render function is not already available, compile is executed to generate the render function
    • ApplyOptions, which deals with options compatibility, is rewritten with the new API
  • setupRenderEffect(instance.update = effect(function componentEffect() {})), effect principle for referenceResponsive article
    • Wrap the render function with effect if the component is not rendered. Go render logic; If the component is rendered, go to the update logic

teleport

  • Gets the node, target, marked by to
  • Insert the empty text node, targetAnchor, into the container, along with the empty text node mainAnchor where the Teleport was
  • If the teleport is not disabled, render the children in the Teleport before the targetAnchor in target
  • Otherwise, render the content in its original position

Mind mapping