preface

Static node improvement is an optimization point proposed by “Vue3” to solve the performance problem of VNode update process. As is known to all, in large-scale application scenarios, the patchVNode process of “Vue2. X”, i.e., the diff process, is very slow, which is a very troublesome problem.

Although, the diff process, which is often asked in interviews, reduces the direct manipulation of the DOM to a certain extent. But this reduction comes at a cost. If it is a complex application, there will be vNodes with very complex parent-child relationship, which is the pain point of DIff. It will recursively call patchVNode and stack it continuously for a few milliseconds, eventually causing slow update of VNode.

So, back to today’s topic, let’s take a look at the source code during the entire compilation process “Vue3” static node enhancement exactly what?

What is a patchFlag

Because the patchFlag property on the AST Element is referred to during the transfrom phase of the compile process. Therefore, before formally understanding complie, we should first make clear a concept: what is patchFlag?

PatchFlag is the optimization mark of AST Element parsing in the transform phase of Complier. In addition, as the name implies, patchFlag, patch means that it will provide basis for patchVNode at runtime, so as to achieve the effect of targeted update of VNode. Therefore, in this way, Vue3, which is well known for its clever combination of Runtime and Compiler, realizes targeted update and static improvement.

In the source code, patchFlag is defined as a numeric enumeration type, and the corresponding identification meaning of each enumeration value is as follows:

Moreover, it is worth mentioning that patchFlag is divided into two categories on the whole:

  • whenpatchFlagThe value of theIs greater thanWhen 0, the corresponding element is inpatchVNodeorrenderCan be optimally generated or updated.
  • whenpatchFlagThe value of theLess thanWhen 0, the corresponding element is inpatchVNodeIs the need to befull diff, that is, recursive traversalVNode treeThe comparative update process.

In fact, there are two special types of flags: shapeFlag and slotFlag. I will not expand on them here, but students who are interested can learn about them by themselves.

Compile process

Compare the Vue2. X compilation process

For those of you who know the source code of “Vue2. X”, I think we all know that the Compile process in “Vue2. X” will look like this:

  • parseCompile the template to generate the raw AST.
  • optimizeOptimize the raw AST by marking the AST Element as a static root or static node.
  • generateGenerate executable code from the optimized AST, for example_c,_lAnd so on.

In “Vue3”, the overall Compile process is still three stages, but different from “Vue2. X”, the second stage is changed into the transform stage, which will exist in normal compilers. So, it would look like this:

In the source code, the corresponding pseudocode would look like this:

export function baseCompile(template: string | RootNode, options: CompilerOptions = {}) :CodegenResult {...const ast = isString(template) ? baseParse(template, options) : template
  ...
  transform(
    ast,
    extend({}, options, {....})
  )

  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
Copy the code

So, I think the question at this point is why transform? What are its responsibilities?

By simply comparing the optimize of the second phase of the vue2.x compilation process, transform is obviously not a piece of paper, it still has the function of optimizing the original AST, and the specific functions are:

  • Add all AST elementscodegenAttributes to helpgenerateMore accurately generatedThe optimalExecutable code.
  • Add static AST ElementhoistsProperty to implement the static nodeCreate alone.
  • .

In addition, transform identifies properties such as isBlock and helpers to generate optimal executable code, but we won’t go into details here, if you’re interested.

BaseParse builds primitive Abstract Syntax Trees (AST)

BaseParse plays the role of parsing as its name suggests. It behaves like the parse of “Vue2. X” in that the parse template Tempalte generates the raw AST.

Suppose we have a template like this:

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

Then, the AST it generates after baseParse will look like this:

{
  cached: 0.children: [{...}],codegenNode: undefined.components: [].directives: [].helpers: [].hoists: [].imports: [].loc: {start: {... },end: {... },source: "<div><div>hi vue3</div><div>{{msg}}</div></div>"},
  temps: 0.type: 0
}
Copy the code

Most of the properties of the AST above will be familiar to those who are familiar with the compilation process of “Vue2. X”. The essence of an AST is to describe a DSL (domain-specific language) by using objects, such as:

  • childrenThe outermost layer is stored in thedivThe offspring.
  • locIs used to describe the AST Element in the entire string (templateLocation information in.
  • typeIs the type used to describe the element (for example, 5 for interpolation, 2 for text), and so on.

In addition, we can see that AST is different from “Vue2. X”, where we have helpers, codegenNode, Hoists, etc. These attributes are assigned during the Transform phase to help generate better executable code during the Generate phase.

Transfrom Optimizes primitive Abstract Syntax Tree (AST)

For the Transform phase, those of you who have seen the compiler’s workflow know that a complete compiler’s workflow looks like this:

  • First of all,parseThe raw code string is parsed to generate the abstract syntax tree AST.
  • Second,transformTransform the abstract syntax tree into a structure closer to the target DSL.
  • In the end,codegenGenerate executable code for the target “DSL” from the transformed abstract syntax tree.

While “Vue3” uses Monorepo to manage projects, compile’s corresponding capability is a compiler. Therefore, transform is also the most important part of the compilation process. In other words, “Vue” would still hang in the diff, a much-maligned process, without a Transform that transforms the AST on many levels.

By contrast, vue2. x didn’t have a full transform, just a bit of AST optimization, so it’s hard to imagine how popular it would be when Vue was designed.

So, let’s look at the definition in the source code for the transform function:

function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if(! options.ssr) { createRootCodegen(root, context) }// finalize meta information
  root.helpers = [...context.helpers]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = [...context.imports]
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}
Copy the code

The definition of the transform function tells you exactly what it does. Here we mention two things that are critical to its static ascension:

  • Assign the AST Element corresponding to the static node in the original AST to the Element of the root ASThoistsProperties.
  • Obtain the key name of the helpers required by the original ASTgeneratePhase generates executable code to get corresponding functions, for examplecreateTextVNode,createStaticVNode,renderListAnd so on.

In addition, the AST Element will be traversed using a transform function, which can be divided into two classes:

  • Static nodetransformApplication, that is, the node has no bindings for interpolation, instructions, props, dynamic styles, etc.
  • Dynamic nodetransformApplications, i.e. nodes with interpolations, instructions, props, dynamic style bindings, and so on.

So, let’s see how static node transform is applied.

Static nodetransformapplication

Here, for the chestnut we talked about above, the static node is this part:

<div>hi vue3</div>
Copy the code

Before the transform application, its AST would look like this:

{
  children: [{
    content: "hi vue3"
    loc: {start: {... },end: {... },source: "hi vue3"}
    type: 2}].codegenNode: undefined.isSelfClosing: false.loc: {start: {... },end: {... },source: "<div>hi vue3</div>"},
  ns: 0.props: [].tag: "div".tagType: 0.type: 1
}
Copy the code

As you can see, its codegenNode is undefined at this point. In the source code, various transform functions are defined as plugin, which will apply the corresponding plugin recursively according to the AST generated by baseParse. Then, create a CodeGen object corresponding to the AST Element.

So, at this point we hit the logic of the two plugins transformElement and transformText.

transformText

TransformText, as the name suggests, is related to text. Obviously, the AST Element is of type Text. So, let’s look at the pseudocode corresponding to the transformText function:

export const transformText: NodeTransform = (node, context) = > {
  if (
    node.type === NodeTypes.ROOT ||
    node.type === NodeTypes.ELEMENT ||
    node.type === NodeTypes.FOR ||
    node.type === NodeTypes.IF_BRANCH
  ) {
    return () = > {
      const children = node.children
      let currentContainer: CompoundExpressionNode | undefined = undefined
      let hasText = false

      for (let i = 0; i < children.length; i++) { / / {1}
        const child = children[i]
        if (isText(child)) {
          hasText = true. }}if (
        !hasText ||
        (children.length === 1 &&
          (node.type === NodeTypes.ROOT ||
            (node.type === NodeTypes.ELEMENT &&
              node.tagType === ElementTypes.ELEMENT)))
      ) { / / {2}
        return}... }}}Copy the code

As you can see, here we hit {2} logic, that is, if a node contains a single text transformText no additional processing is required, that node is still treated in the same way as the “ve2. X” version.

Where transfromText really comes into play is when there is a situation in the template like this:

<div>ab {a} {b}</div>
Copy the code

At this point, the transformText needs to place both under a separate AST Element, which in the source code is called a “Compound Expression”, a combined Expression. The purpose of this combination is to better locate and update DOM when patchVNode is used. On the contrary, if it is a text node and interpolation dynamic node, the same operation needs to be carried out twice in the patchVNode stage, for example, for the same DOM node.

transformElement

TransformElement is a plugin that is implemented by all AST elements. Its core is to generate basic codeGen properties for AST elements. For example, identify the corresponding patchFlag to provide a basis for generating VNodes, such as dynamicChildren.

Static nodes also initialize their codegenNode properties. In addition, from the type of patchFlag introduced above, we can know that its patchFlag is the default value 0. So, its codegenNode property value will look like this:

{
  children: {
    content: "hi vue3"
    loc: {start: {... },end: {... },source: "hi vue3"}
    type: 2
  },
  directives: undefined.disableTracking: false.dynamicProps: undefined.isBlock: false.loc: {start: {... },end: {... },source: "<div>hi vue3</div>"},
  patchFlag: undefined.props: undefined.tag: ""div"".type: 13
}
Copy the code

Generate generates executable code

Generate is the last step of compile stage. It is used to generate the corresponding executable code from the transformed AST, so that the corresponding VNode Tree can be generated through the executable code in the Render stage of the later Runtime. It then eventually maps to the actual DOM Tree on the page.

Similarly, this stage in “Vue2. X” is also done by generate, which generates functions such as _L and _c, essentially encapsulating the _createElement function. Compared to “Vue2. X” version of generate, “Vue3” has changed a lot. The pseudo-code for its generate function would look like this:

export function generate(ast: RootNode, options: CodegenOptions & { onContextCreated? : (context: CodegenContext) =>void
  } = {}
) :CodegenResult {
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context
  ...
  genFunctionPreamble(ast, context)
  ...

  if(! ssr) { ... push(`function render(_ctx, _cache${optimizeSources}) {`)}...return {
    ast,
    code: context.code,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined}}Copy the code

So, let’s take a look at the process of generating executable code with the AST corresponding to static nodes.

CodegenContext Code generation context

As you can see from the pseudocode for generate above, createCodegenContext is called at the beginning of the function to generate a context for the current AST. Throughout the execution of Generate relies on the ability of a CodegenContext to generate a code context (object), which is generated by the createCodegenContext function. The interface definition for CodegenContext would look like this:

interface CodegenContext
  extends Omit<Required<CodegenOptions>, 'bindingMetadata'> {
  source: string
  code: string
  line: number
  column: number
  offset: number
  indentLevel: number
  pure: boolean map? : SourceMapGenerator helper(key: symbol): string push(code: string, node? : CodegenNode):void
  indent(): voiddeindent(withoutNewLine? : boolean):void
  newline(): void
}
Copy the code

You can see that the CodegenContext object has methods like Push, Indent, newLine, and so on. They are used to wrap, add, indent, and so on when generating code from the AST. The resulting executable code, known as the Render function, is returned as the value of the CodegenContext’s code property.

Next, let’s look at the core of executable code generation for static nodes, called Preamble precursors.

Preparation before genFunctionPreamble generation

The entire static promotion of executable code generation is done in the genFunctionPreamble function section. And, we carefully consider the word static promotion, we can not ignore the static word, but the promotion word, express it (static node) has been improved.

Why did it go up? Because the representation in the source code is really improved. In the previous generate function, we can see that genFunctionPreamble is added to context.code before the render function, so it executes before the render function at Runtime.

GeneFunctionPreamble function (pseudocode) :

function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
  const {
    ssr,
    prefixIdentifiers,
    push,
    newline,
    runtimeModuleName,
    runtimeGlobalName
  } = context
  ...
  const aliasHelper = (s: symbol) = > `${helperNameMap[s]}: _${helperNameMap[s]}`
  if (ast.helpers.length > 0) {...if (ast.hoists.length) {
      const staticHelpers = [
        CREATE_VNODE,
        CREATE_COMMENT,
        CREATE_TEXT,
        CREATE_STATIC
       ]
        .filter(helper= > ast.helpers.includes(helper))
        .map(aliasHelper)
        .join(', ')
      push(`const { ${staticHelpers} } = _Vue\n`)}}... genHoists(ast.hoists, context) newline() push(`return `)}Copy the code

As you can see, the length of the Hoists property we mentioned earlier in the transform function is judged. Obviously, for the previous chestnut, its ast.hoists.length is greater than 0. So, the corresponding executable code is generated based on the AST in Hoists. So, at this point, the generated executable code would look like this:

const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// Static promotion part
const _hoisted_1 = _createVNode("div".null."hi vue3", -1 /* HOISTED */)
// The render function will be below here
Copy the code

summary

Static node improvement is reflected in the whole compile stage, from the initial baseCompile to transform the original AST, and then the priority render function of generate to generate executable code. Finally, Render at Runtime executes, which is pretty neat! As a result, “Vue3”, which we often see mentioned in some articles, will only execute the source code implementation created once in the lifetime of a static node, reducing the performance overhead somewhat.

Write in the last

After seeing the processing of static nodes in the whole compilation process, I think everyone may be eager to know what is the scene of patchVNode for static nodes? Originally, I had intended to describe the entire process in one article, but later I realized that this added to the cost of reading. Because patchVNode in “Vue3” version is not only a comparison process of DIff, but also realizes different patch process for each VNode. Therefore, the process of patchVNode will be written in the next article, please look forward to it!

Review previous articles

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

Explain the process of exporting files from the back end to the front end (Blob) download

❤️ Love triple punch

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