As we’ve seen, the transition from virtual Node to real Node is done using a function called render. This article will take you into the render function, first from the overall grasp of Vue3 rendering core process and part of the source code implementation details. The more important details, such as how components are rendered and updated, and how the diff algorithm is implemented, will be analyzed in subsequent articles.

renderfunction

Let’s look directly at the render function code implementation:

// Snippet 1
const render: RootRenderFunction = (vnode, container, isSVG) = > {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null.null.true)}}else {
      patch(container._vnode || null, vnode, container, null.null.null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
Copy the code

The first argument is a virtual Node object, the second argument is an Element object, and the third argument is ignored for now. The internal logic of the render function is also simple and does the following things:

  1. If the incomingVirtual NodeIf the object is empty, it is determinedcontainerWhether the corresponding element has ever rendered other elementsVirtual NodeIf yes, run thecontainerUnload theVirtual NodeThe corresponding node, if not do nothing, willcontainer._vnodeEmpty.container._vnodeThe value ofrenderThe last line of the function;
  2. If the incomingVirtual NodeIf it is not empty, then sum is requiredcontainerMounted on the element_vnodeRepresented by theDOMElement to compare and modify the current realityDOMTree, this logic is all based onpatchFunction to achieve, is also the focus of this article;
  3. performflushPostFlushCbsWill be saved in an arraypendingPostFlushCbsWhen is the array givenpendingPostFlushCbsThe details of how these functions are executed are not covered in this article, but will be covered in a section in a future article if necessary.

patchIs the soul

Although Vue3’s rendering process is realized by calling render function, patch is the soul of the whole rendering process. Let’s take a look at the specific implementation of patch function:

// Snippet 2
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false:!!!!! n2.dynamicChildren) = > {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    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(
         // Omit some code here...
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            // Omit some code here...)}else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            // Omit some code here...)}else if(shapeFlag & ShapeFlags.TELEPORT) { ; (type as typeof TeleportImpl).process(
            // Omit some code here...)}else if(__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (type as typeof SuspenseImpl).process(
            // Omit some code here...)}else if (__DEV__) {
          warn('Invalid VNode type:'.type.` (The ${typeof type}) `)}}// set ref
    if(ref ! =null&& parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, ! n2) } }Copy the code

The patch function invokes different functions for processing according to the type of virtual Node that is passed in. There are two points of concern:

  1. To clear uppatchThe function’s mission;
  2. Through the way of bit operation to determine the type;

patchFunction’s mission

Maybe you will feel strange, didn’t we just talk about the main logic of patch function is to call different functions according to different types of virtual Node to process? What else? Yes, the logic of patch function is very clear, but I want to emphasize here that the fundamental meaning of patch is to find the difference between the new virtual Node and the old virtual Node corresponding to the current real Node, and modify the DOM tree according to the difference to smooth out the difference. Understanding this makes it easy to understand why there are such statements:

// Snippet 3
if (n1 === n2) {
      return
 }
Copy the code

Since there is no difference between the old and new virtual nodes, there is no need to continue. We can also easily understand the following code:

// Snippet 4
if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
  n1 = null
}
Copy the code

If the old virtual Node exists and the type of the new virtual Node is different from that of the old virtual Node, unmount the old virtual Node and empty the old virtual Node. It will be found that there is an Anchor variable here. If the Anchor is always null, the new element will always be inserted at the tail, which is inconsistent with the position of the element it replaces. Therefore, the next element should be recorded with anchor before unloading the real Node corresponding to the old virtual Node.

At the same time, we have understood the mission of patch function, so we can try to imagine how to implement patch function. It may be natural for us to think that we can simply delete the old node and insert the content of the new node. To achieve the same function, thousands of lines of code can be simplified to several lines. The seemingly low-level implementation made us realize the essence of patch function. In the latter part of this paper, many other functions called in patch function will be introduced. I believe that with our previous knowledge, we can better understand why Vue3 is implemented in this way.

Type judgment mode

We found several places in snippet 2 that looked like if (shapeFlag & ShapeFlags.element). Why? To answer this question, let’s first look at what shapeFlag is and where shapeFlags.element comes from.

ShapeFlag is deconstructed from the second parameter of patch function, that is, the new virtual Node, and the value is a numeric type. Let’s look at the ShapeFlags code again:

// Snippet 5
export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
Copy the code

You can see from snippet 5 that ShapeFlags is an enumerated type. A friend who does not know about parapposition operation may have been full of doubts, why so expressed? To answer this question, we need to understand the left shift, and, or operations of bitwise operations.

Suppose we have eight binary bits 00000000, each binary bit represents whether little A has A capability, 1 means yes, 0 means no, the specific capability mapping is as follows.

basketball football swimming English drinking food running Driving a car
0 0 0 0 0 0 0 0

If little A can run, it can be described as follows:

basketball football swimming English drinking food running Driving a car
0 0 0 0 0 0 1 0

If little A can not only run but also drink, it can be described as:

basketball football swimming English drinking food running Driving a car
0 0 0 0 1 0 1 0

Based on the above knowledge, we can express the different states as follows:

// xiaoAState is 0, which means small A
let xiaoAState = 0;  // 0 0 0 0 0 0 0 0
const DRIVE_CAR = 1; // 0 0 0 0 0 0 1
const RUN = 1 << 1;  // 0 0 0 0 0 1 0
const FOOD = 1 << 2; // 0 0 0 0 1 0 0
const DRINK = 1 << 3;// 0 0 0 0 1 0 0 0 0

// Let small A have the ability to drink alcohol, can be calculated as follows:
xiaoAState |= DRINK; // 0 0 0 0 1 0 0 0 0
// Let little A have the ability of running, can be calculated as follows:
xiaoAState |= RUN;   // 0 0 0 1 0 1 0
Copy the code

The or operation can be understood in the following table:

basketball football swimming English drinking food running Driving a car Sign of operation meaning
0 0 0 0 1 0 0 0 DRINK
0 0 0 0 0 0 1 0 or RUN
0 0 0 0 1 0 1 0 The results of

When we want to judge whether little A has A certain ability, we can use the & operation, for example:

if(xiaoAState & DRINK){
  console.log('Little A does drink.')}if(xiaoAState & FOOD){
  console.log('Little A can cook')}else{
  console.log('Little A can't cook')}Copy the code

Why can we judge in this way? Let’s take a look at xiaoAState & FOOD’s hint:

basketball football swimming English drinking food running Driving a car Sign of operation meaning
0 0 0 0 1 0 1 0 xiaoAState
0 0 0 0 0 1 0 0 & FOOD
0 0 0 0 0 0 0 0 The results of

It is not difficult to find that xiaoAState calculates & with its own abilities, and the result value is 0. On the contrary, if it calculates & with its own abilities, the result value is 1. That is why it can judge whether xiaoAState has a certain ability through & operation. At this point, it’s not hard to see why snippet 5 starts with 1 and moves to the left by one bit, all for ease of calculation. At the same time, in this way, one attribute value can represent multiple states. For example, xiaoAState demonstrated above can represent not only the ability to drink, but also the ability to run or many other abilities. I have to say that this way is very clever, and the performance is relatively high, in the actual work of similar scenarios can be used for reference.

Let’s start exploring the other functions that patch calls internally:

processText

// Snippet 6
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if(n2.children ! == n1.children) { hostSetText(el, n2.childrenas string)}}}Copy the code

The logic is simple. If the old virtual Node is null, insert the text directly into the container. If the old virtual Node is not null, update is required. Here are three points to note:

  1. hostInsert,hostSetTextWhere did it come from? Remember when we wereruntime-domParameter passed inconst rendererOptions = extend({ patchProp }, nodeOps)Oh, yeah, exactlyDOMNodes can be deleted, modified, or addedruntime-domOr any other method passed in by the platform.runtime-coreYou only care what kind of operations are being performed on the node, but how those operations are implemented depends on the parameters passed in. This is theruntime-corePlatform-independent reasons.
  2. coden2.el = hostCreateText(n2.children as string)It can be seen thatVirtual NodetheelProperty, which holds aDOMObject, even thisDOMThe object is text.
  3. const el = (n2.el = n1.el!)This is a clever line of codeOld virtual NodetheelAttribute value assigned toNew virtual NodeThe properties of theelIs equivalent toOld virtual NodeThe correspondingDOMOperations are performed on nodes instead of creating new nodes, reducing performance costs.

processCommentNode

// Snippet 7
const processCommentNode: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateComment((n2.children as string) | |' ')),
        container,
        anchor
      )
    } else {
      // there's no support for dynamic comments
      n2.el = n1.el
    }
  }
Copy the code

The logic here is simple. If the new virtual Node is an annotation type, the system checks whether the old virtual Node exists. If not, the system inserts the new virtual Node directly. If it exists, the el element corresponding to the old virtual Node is directly assigned to the EL of the new virtual Node, without any other processing, because Vue3 does not support annotation responsive change, that is, annotations will not be changed after creation.

mountStaticNode

// Snippet 8
const mountStaticNode = (
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    isSVG: boolean
  ) = > {
    // static nodes are only present when used with compiler-dom/runtime-dom
    // which guarantees presence of hostInsertStaticContent.; [n2.el, n2.anchor] = hostInsertStaticContent! ( n2.childrenas string,
      container,
      anchor,
      isSVG,
      n2.el,
      n2.anchor
    )
  }
Copy the code

A mountStaticNode can mount the static content of a new virtual Node to a Container. You can call hostInsertStaticContent, which is passed in from the Run-time dom. There are two details to note:

  1. In the normal coding process, to(,[The expression at the beginning should be preceded by one;To prevent concatenation of a property access statement with the contents of the previous line after the code is compressed.
  2. Not sure about the syntax of deconstructing assignment[n2.el, n2.anchor] = xxxThe expression may be very confused, you can refer toMDNThis document provides relevant information.

patchStaticNode

// Snippet 9
const patchStaticNode = (
    n1: VNode,
    n2: VNode,
    container: RendererElement,
    isSVG: boolean
  ) = > {
    // static nodes are only patched during dev for HMR
    if(n2.children ! == n1.children) {const anchor = hostNextSibling(n1.anchor!)
      // remove existing
      removeStaticNode(n1)
      // insert new; [n2.el, n2.anchor] = hostInsertStaticContent! ( n2.childrenas string,
        container,
        anchor,
        isSVG
      )
    } else {
      n2.el = n1.el
      n2.anchor = n1.anchor
    }
  }
Copy the code

The patchStaticNode function can only be called in a development environment. Why? Since it is a static node, there is no responsive data change and therefore no update, so this function is not called. However, the development environment hot update may change the corresponding data, the logic is relatively simple, if you still feel difficult to read can be skipped first, do not do the key grasp.

processFragment,processComponent

The flow within the functions processFragment and processComponent will be examined in a future article.

setRef

// Snippet 10
if(ref ! =null&& parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, ! n2) }Copy the code

Remember in our last article about getting child components by ref, when we introduced the core function of getExposeProxy to protect the child’s content from being accessed by the parent. SetRef is called in the patch function, and getExposeProxy is called in the setRef function. Let’s see what setRef does:

// Snippet 11
export function setRef(
  rawRef: VNodeNormalizedRef,
  oldRawRef: VNodeNormalizedRef | null,
  parentSuspense: SuspenseBoundary | null,
  vnode: VNode,
  isUnmount = false
) {
  // A lot of code is omitted here...
  constrefValue = vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT ? getExposeProxy(vnode.component!) || vnode.component! .proxy : vnode.elconst value = isUnmount ? null : refValue

  const { i: owner, r: ref } = rawRef
  // A lot of code is omitted here...
  const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
  const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
  const setupState = owner.setupState

  if(oldRef ! =null&& oldRef ! == ref) {if (isString(oldRef)) {
      refs[oldRef] = null
      if (hasOwn(setupState, oldRef)) {
        setupState[oldRef] = null}}else if (isRef(oldRef)) {
      oldRef.value = null}}if (isFunction(ref)) {
    callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
  } else {
    const _isString = isString(ref)
    const _isRef = isRef(ref)
    if (_isString || _isRef) {
      const doSet = () = > {
        // A lot of code is omitted here...
      }
      if(value) { ; (doSetas SchedulerJob).id = -1
        queuePostRenderEffect(doSet, parentSuspense)
      } else {
        doSet()
      }
    }
    // A lot of code is omitted here...}}Copy the code

There are three main things we need to know about the setRef function so far:

  1. To obtainrefProxy object of;
  2. findOld virtual NodeThe correspondingref, if there is and andNew virtual NodeThe correspondingrefIf no, set it tonull;
  3. Will the newrefThe proxy object assigns a value toNew virtual NodeCorresponding properties.

The various properties and details of the REF shown in snippet 11 will be explored at an appropriate time in a later article.

Write in the last

After reading the article, you can do the following things to support you:

  • ifLike, look, forwardCan let the article help more people in need;
  • If you are the author of wechat public account, you can find me to openWhite list.reprintedMy original articles;

Finally, please pay attention to my wechat public account: Yang Yitao, you can get my latest news.