preface

In the last article, we analyzed the promotion of static nodes during compilation. In addition, at the end of the article, the patch process will be introduced in the next article.

Speaking of the patch process of “Vue3”, targeted update is the most talked about. Targeted update, as the name implies, that is, the process of update is targeted and direct. This, like static node improvement, is a major optimization of “Vue3” for VNode update performance issues.

So, today, we will reveal how Vue3 compile and Runtime patch process is implemented!

What is a shapeFlag

Speaking of the patch of “Vue3”, patchFlag is a platitude. So, shapeFlag, I think you might be a little confused, what is it?

ShapeFlag, as the name suggests, marks elements that have shapes, such as normal elements, function components, slots, Keep Alive components, and so on. Its function is to help render processing in Rutime, and different patch operations can be carried out according to the enumeration values of different ShapeFlag.

In the “Vue3” source codeShapeFlagpatchFlagAs defined by thetaEnumerated typeEach enumeration value and its meaning would look like this:

Component creation process

Those who have known the source code of “Vue2. X” should know that the first patch is triggered in the process of component creation. However, the oldVNode is null, so it behaves as a mount. So, before we get to the pathC process, it’s essential that we know how the components are created, right?

Since it is said that the first trigger of patch will be the component creation process, what will be the component creation process in “Vue3”? It goes through three processes:

Earlier, we said that the compile process turns our template into executable code, the render function. Instead, the Render function generated by the Compiler is bound to the Render property of the current component instance. For example, we have a template template like this:

<div><div>hi vue3</div><div>{{msg}}</div></div>
Copy the code

The render function generated after compile looks like this:

const _Vue = Vue
const _hoisted_1 = _createVNode("div".null."hi vue3", -1 /* HOISTED */)

function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _createVNode("div".null, [
        _hoisted_1,
        _createVNode("div".null, _toDisplayString(msg), 1 /* TEXT */)]]))}}Copy the code

The time when the render function is actually executed is when the global render corresponding effect is installed, i.e., setupRenderEffect. Render effect is triggered when a component is created and updated.

At this time, some students may ask what is effect? Effect is not a new concept of “Vue3”, it is essentially watcher in the “Vue2. X” source code, as well as being responsible for relying on the collection and distribution of updates.

For those who are interested in learning more about the process of dependent collection and distribution of updates for “Vue3”, take a look at this article.

The pseudo-code for the setupRenderEffect function would look like this:

function setupRenderEffect() {
    instance.update = effect(function componentEffect() {
      // The component is not mounted
      if(! instance.isMounted) {// Create a VNode tree for the component
        const subTree = (instance.subTree = renderComponentRoot(instance))
        ...
        instance.isMounted = true
      } else {
        // Update the component. }}Copy the code

RenderComponentRoot (Instance) = renderComponentRoot(Instance) = render (instance); The entire VNode Tree, the subTree here, is then constructed for the current component instance. The pseudocode corresponding to the renderComponentRoot function would look like this:

function renderComponentRoot(instance) {
  const{... render, ShapeFlags, ... } = instanceif(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { ... result = normalizeVNode( render! .call( proxyToUse, proxyToUse! , renderCache, props, setupState, data, ctx ) ) ... }}Copy the code

As you can see in renderComponentRoot, the logic that calls Render will be hit if the current ShapeFlags is STATEFUL_COMPONENT. The render function is the executable code generated by compile. It will eventually return a VNode Tree, which will look like this:

{...children: (2) ({...}, {...}),...dynamicChildren: (2) ({...}, {...}),...el: null.key: null.patchFlag: 64.shapeFlag: 16.type: Symbol(Fragment),
  ...
}
Copy the code

Taking targeted update as an example, students who have learned about it should know that its implementation cannot be separated from the dynamicChildren attribute on the VNode Tree, dynamicChildren is used to undertake all dynamic nodes in the whole VNode Tree. And the process of marking dynamic nodes is in the transform stage of compile compilation, which can be said to be linked to each other. Therefore, this is also the clever combination of “Vue3” Runtime and compile.

Obviously, “ve2. X” does not have the condition to build the dynamicChildren property of VNode. Then, how does “Vue3” generate dynamicChildren?

Block VNode creation process

Block VNode

Block VNode is a concept proposed by “Vue3” for targeted update, and its essence is the VNode corresponding to dynamic nodes. The dynamicChildren property on a VNode is derived from a Block VNode, so it acts as a target in targeted updates.

Here, we return to the render function generated by the compiler, which returns the result:

(_openBlock(), _createBlock(_Fragment, null, [
  _createVNode("div".null, [
    _hoisted_1,
    _createVNode("div".null, _toDisplayString(msg), 1 /* TEXT */)]]))Copy the code

Note that openBlock must be written before createBlock because Children in the Block Tree will always be executed before createBlock.

You can see that there are two block-related functions: _openBlock() and _createBlock(). In fact, they correspond to the openBlock() and createBlock() functions in the source code, respectively. So, let’s take a look at the two:

openBlock

OpenBlock initializes an array currentBlock for the current Vnode to hold the Block. The openBlock function definition is quite simple, and it looks like this:

function openBlock(disableTracking = false) {
    blockStack.push((currentBlock = disableTracking ? null : []));
}
Copy the code

The openBlock function takes a parameter disableTracking, which is used to determine whether currentBlock is initialized. So, when do I not need to create currentBlock?

When a VNode is formed by v-for, the openBlock() parameter disableTracking in its render function is true. Because it does not need targeted update to optimize the update process, that is, it will undergo a complete diff process at patch.

Let’s think about it in a different way. Why is it designed this way? The essence of targeted update is to screen dynamic nodes from a VNode Tree with dynamic and static nodes to form a Block Tree, namely dynamicChildren, and then achieve accurate and rapid update during patch. Therefore, it is obvious that the VNode Tree formed by V-for does not require targeted update.

Why are Block vNodes pushed into the blockStack? What does it do? If you are interested, try the V-IF scenario, which eventually constructs a Block Tree. Vue3 Compiler optimizes details of how to write high-performance rendering functions

createBlock

CreateBlock creates Block VNodes by calling the createVNode method. CreateBlock function definition:

function createBlock(type, props, children, patchFlag, dynamicProps) {
    const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true);
    // Create a Block Tree
    vnode.dynamicChildren = currentBlock || EMPTY_ARR;
    closeBlock();
    if (shouldTrack > 0 && currentBlock) {
        currentBlock.push(vnode);
    }
    return vnode;
}
Copy the code

You can see that createVNode is still called to create a VNode in createBlock. The createVNode function essentially calls the _createVNode function in the source code, and its type definition would look something like this:

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 {}
Copy the code

When we call _createVNode() to create a Block VNode, we need to pass isBlockNode true to indicate whether the current VNode is a Block VNode, so that the Block VNode does not depend on itself. That is, the current VNode will not be added to currentBlock. The corresponding pseudocode would look like this:

function _createVNode() {...if (
    shouldTrack > 0&&! isBlockNode && currentBlock && patchFlag ! == PatchFlags.HYDRATE_EVENTS && (patchFlag >0 || shapeFlag & ShapeFlags.COMPONENT)
  ) {
    currentBlock.push(vnode)
  }
  ...
}
Copy the code

A VNode that meets all of the conditions in the if statement can be considered a Block Node.

  • SholdTrack is greater than 0, that is, nonev-onceUnder the instruction ofVNode.
  • Whether isBlockNode forBlock Node.
  • Created when currentBlock is an arrayBlock Nodeforv-forScenarios,curretBlocknullIt does not require targeted updates.
  • PatchFlag is meaningful and not32Event listeners are cached only in event listening cases.
  • If shapeFlags is a component, it must beBlock NodeThis is to ensure that the nextVNodeNormal uninstallation of.

As for why, take it a step further? Interested students can find out by themselves.

summary

After the process of creating a VNode, I think we all realize that we need to be familiar with the concepts of createBlock, openBlock, and other functions if we use the hand-written render function. Only in this way can we write the render function to take full advantage of the targeted update process and achieve the best application update performance.

The patch process

Compare the patch of Vue2. X

As mentioned earlier, patch is the last step in component creation and update, and is sometimes referred to as diFF. In “Vue2. X”, its patch process will look like this:

  • Same levelVNodeA comparison between the old and the newVNodeIf it belongs to the same reference, if yes, no subsequent comparison is performed; if no, comparisons are performed at each levelVNode.
  • Compare the process and define four Pointers to old and newVNodeAnd the cyclic condition isHead pointer indexLess thanTail pointer index.
  • The match is successfulVNodeThe current match was successfulreal DOMMove to corresponding newVNodeThe location was successfully matched.
  • If the match is not successful, a newVNodeIn thereal DOMNode inserted into oldVNodeIn the corresponding position of, that is, the old is createdVNodeDoes not exist inDOMNode.
  • Keep recursing untilVNodechildrenUntil it doesn’t exist.

At a glance, it can be understood that “Vue2. X” patch is a hard comparison process. So, this is where it falls short of properly handling VNode updates for large applications.

The patch Vue3

Although the patch of “Vue3” does not rename some functions such as baseCompile and transform stages as compile does. However, its internal processing is much smarter than “Vue2. X”.

It will use the type and patchFlag in compile stage to deal with updates in different situations, which can also be understood as a divide-and-conquer strategy. The corresponding pseudocode would look like this:

function patch(.) {
  if(n1 && ! isSameVNodeType(n1, n2)) { ... }if (n2.patchFlag === PatchFlags.BAIL) {
    ...
  }
  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(...)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(...)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(...)
      }else if(shapeFlag & ShapeFlags.TELEPORT) { ; (typeas typeof TeleportImpl).process(...)
      } else if(__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (typeas typeof SuspenseImpl).process(...)
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, ` (The ${typeof type}) `)}}Copy the code

As you can see, except for text, static, document fragments, comments, and so on, VNode handles it according to type. By default, components, common elements, Teleport, Suspense components, and so on are handled according to shapeFlag. So, that’s why shapeFlag is introduced at the beginning of this article.

In addition, from the render stage to the patch stage, Block vnodes are created according to the different processing of specific shapeFlag. To some extent, shapeFlag has the same value as patchFlag!

Here we take one of the cases, when ShapeFlag is ELEMENT, and analyze how processElement handles VNode patches.

processElement

Similarly, processElement handles mount cases when oldVNode is null. The definition of the processElement function:

const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
    }
  }
Copy the code

In fact, I think oldVNode is changed to N1 and newVNode is changed to N2.

As you can see, processElement actually calls the patchElement function when it handles the update.

patchElement

PatchElement handles familiar props, lifecycle, custom event instructions, and so on. We won’t analyze what happens in each case here. Let’s take the targeted update mentioned at the beginning of this article as an example. How is it handled?

In fact, the processing of targeted update is very simple, that is, if n2 (newVNode) dynamicChildren exists at this time, it can directly “boha” and update dynamicChildren without processing other VNodes. The corresponding pseudocode would look like this:

function patchElement(.) {...if (dynamicChildren) {
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG
    )
    ...
  }
  ...
}
Copy the code

Therefore, if n2’s dynamicChildren exists, the patchBlockChildren method is called. However, patchBlockChildren method is actually a layer of encapsulation based on patch method.

patchBlockChildren

PatchBlockChildren will iterate over newChildren (dynamicChildren) to process each oldVNode and newVNode of the same class, and call patch as arguments. And so on and so on.

const patchBlockChildren: PatchBlockChildrenFn = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) = > {
    for (let i = 0; i < newChildren.length; i++) {
      const oldVNode = oldChildren[i]
      const newVNode = newChildren[i]

      constcontainer = oldVNode.type === Fragment || ! isSameVNodeType(oldVNode, newVNode) || oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!) ! : fallbackContainer patch( oldVNode, newVNode, container,null,
        parentComponent,
        parentSuspense,
        isSVG,
        true)}}Copy the code

You will notice that the container that the current VNode needs to mount is also retrieved, because dynamicChildren sometimes cross hierarchically and the VNode is not its parent. There are two specific situations:

1. The parent node of an oldVNode serves as a container

  • When this timeoldVNodeIs of type document fragment.
  • oldVNodenewVNodeIt’s not the same node.
  • shapeFlagteleportcomponentAt the right time.

2. The container that invokes patch initially

  • Except in the above cases, it is initialpatchMethod passed inThe rootVNodeMount points as containers.

Why each case needs to be dealt with in this way will be a lengthy discussion, which is expected to be covered in the next article.

Write in the last

Originally the original intention is to simplify the complex, did not expect to finally write 3k+ words. Because “Vue3” uses compile and Runtime together to achieve many optimizations. Therefore, it is impossible to analyze patch like “Vue2. X”, only need to pay attention to runtime, do not need to pay attention to compile before this, do some processing to set the tone. Therefore, the article will inevitably be a little obscure, here is a suggestion to deepen the impression of the students can be combined with the actual step mode.

Review previous articles

Understand Vue3 static node enhancement from the compile process (source code analysis)

From zero to one, take you to thoroughly understand the HMR principle in Vite (source code analysis)

❤️ Love triple punch

Through reading, if you think you have something to gain, you can love the triple punch!!

Wechat official account: Code Center