Vue3 source code parsing – virtual DOM

What is the virtual DOM

In the browser, the HTML page is composed of basic DOM tree to, when some of the changes, is actually corresponds to a DOM node has changed, when change the DOM node will trigger the corresponding redrawn or rearrangement, when excessive redrawn and rearrangement occurred in a short time, will be likely to cause page card, So there are costs to changing the DOM, so how to optimize the number of DOM changes and when to change the DOM is something that developers need to pay attention to.

The virtual DOM was designed to address these browser performance issues. When there are 10 DOM updates in one operation, the virtual DOM does not operate the DOM immediately, but compares it with the original DOM, saves the 10 changes in the MEMORY, and finally applies them to the DOM tree once, and then performs subsequent operations to avoid a lot of unnecessary computation.

Virtual DOM is actually the use of JavaScript objects to store DOM node information, the DOM update into object modification, and these modifications are calculated in memory, when the modification is completed, the JavaScript is converted into a real DOM node, handed to the browser, so as to achieve performance improvement. For example, the following DOM node looks like this:

<div id="app">
  <p class="text">Hello</p>
</div>
Copy the code

Convert to a generic virtual DOM object structure, as shown in the following code:

{
  tag: 'div'.props: {
    id: 'app'
  },
  chidren: [{tag: 'p'.props: {
        className: 'text'
      },
      chidren: [
        'Hello']]}}Copy the code

The code above is a basic virtual DOM, but it is not the virtual DOM structure used in Vue, which is much more complex.

Vue 3 virtual DOM

In Vue, the content we write in

  • extracting<template>Compile the content.
  • Get the AST syntax tree and generate the Render method.
  • Execute the Render method to get the VNode object.
  • VNode converts the real DOM and renders it to the page.

The complete process is as follows:

We take a simple demo as an example, in the source of Vue 3 to find, exactly how the step by step, demo code as shown below:

<div id="app">
  <div>
    {{name}}
  </div>
  <p>123</p>
</div>
Vue.createApp({
  data(){
    return {
      name : 'abc'
    }
  }
}).mount("#app")
Copy the code

In the code above, a reactive data name is defined in data and is used in

To obtain<template>content

Call createApp() to access the createApp() method in source packages/ Runtime-dom/SRC /index.ts, as shown below:

export const createApp = ((. args) = > {
  constapp = ensureRenderer().createApp(... args) ... app.mount = (containerOrSelector: Element | ShadowRoot | string):any= > {
    if(! isFunction(component) && ! component.render && ! component.template) {// Assign the HTML bound to #app to the template item
      component.template = container.innerHTML

      // Call the mount method to render
    const proxy = mount(container, false, container instanceof SVGElement)
    return proxy
  }
  ...
  return app
}) as CreateAppFunction<Element>
Copy the code

For the root component, the contents of

Generate AST syntax tree

After getting

whileB indicates0
  if a > b
a: = a - belse
b: = b - areturn a
Copy the code

If you translate the above code into a syntax tree in a broad sense, it looks like this.

The content of

export function baseCompile(template: string | RootNode, options: CompilerOptions = {}) :CodegenResult {...// Generate an AST tree structure from template
  const ast = isString(template) ? baseParse(template, options) : template
  ...
  / / conversion
  transform(
    ast,
    ...
  )
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
Copy the code

The baseCompile method does the following:

  • Generate AST objects in Vue.
  • Pass the AST object as an argument to the transform function for the transformation.
  • Generate the Render function by passing the transformed AST object as an argument to generate.

The baseParse method is used to create an AST object. In Vue 3, the AST object is a RootNode type tree structure. In source packages\ Compiler-core \ SRC \ AST, the structure is shown as follows:

export function createRoot(children: TemplateChildNode[], loc = locStub) :RootNode {
  return {
    type: NodeTypes.ROOT, // Element type
    children, / / child elements
    helpers: [].// Help function
    components: []./ / child component
    directives: []./ / instructions
    hoists: [].// Identify static nodes
    imports: [].cached: 0.// Cache flag bit
    temps: 0.codegenNode: undefined.// Store the generated render function string
    loc // Describes the position of the element in the AST tree}}Copy the code

Where, children stores the data of the descendant element node, which forms an AST tree structure. Type represents the type of element NodeType, which is mainly divided into HTML common type and Vue instruction type, etc. Common ones are as follows:

ROOT, // ROOT ELEMENT 0 ELEMENT, // Normal ELEMENT 1 TEXT, // TEXT ELEMENT 2 COMMENT, // COMMENT ELEMENT 3 SIMPLE_EXPRESSION, // Expression 4 INTERPOLATION, {{}} 5 ATTRIBUTE, // ATTRIBUTE 6 DIRECTIVE, // DIRECTIVE 7 IF, // IF node 9 JS_CALL_EXPRESSION, // method call 14...Copy the code

Hoists is an array that stores elements that can be promoted statically. The transform creates static and reactive elements separately, which is also optimized in Vue 3. CodegenNode stores the string of the render method that is generated. Loc represents the location of an element in the AST tree.

When generating the AST tree, Vue 3 uses a stack to hold the parsed element labels as it parses the

The AST syntax tree generated in the Demo code is shown in the following figure.

Generate the Render method string

After obtaining the AST object, the transform method is entered. In source packages\ Compiler-core \ SRC \transform.ts, the core code is shown as follows:

export function transform(root: RootNode, options: TransformOptions) {
// Data assembly
const context = createTransformContext(root, options)
  // Convert the code
  traverseNode(root, context)
  // Static promotion
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }// Server render
  if(! options.ssr) { createRootCodegen(root, context) }// Pass through the meta information
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
  if(__COMPAT__) { root.filters = [...context.filters!] }}Copy the code

Transform method is mainly to further transform AST to prepare for generate function to generate render method, which mainly does the following:

  • TraverseNode methods recursively examine and parse AST element node properties, such as adding methods and event callbacks to events such as @click in conjunction with helpers, and dynamic binding to interpolations, instructions, and props.
  • The processing type logic includes static promotion logic, assigning static nodes to Hoists, and making different patchflags according to different types of nodes for the convenience of subsequent diff.
  • Bind and pass through some metadata on the AST.

The generate method mainly generates the string code of the render method. In source packages\ Compiler-core \ SRC \ codeGen.ts, the core code is shown as follows:

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
  ...
  // Indent
  indent()
  deindent()
  // Handle component, directive, filters separately
  genAssets()
  // Handle all types in NodeTypes
  genNode(ast.codegenNode, context)
  ...
  // Returns the code string
  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.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

In the genNode method, the logic is to construct different render method strings according to different NodeTypes, some of which are shown in the following code:

switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:// for keyword element nodegenNode(node.codegenNode! , context)break
case NodeTypes.TEXT:// Text element node
  genText(node, context)
  break
case NodeTypes.VNODE_CALL:// Core: VNode mixed type node (AST syntax tree node)
  genVNodeCall(node, context)
  break
case NodeTypes.COMMENT: // Comment the element node
  genComment(node, context)
  break
case NodeTypes.JS_FUNCTION_EXPRESSION:// method calls node
  genFunctionExpression(node, context)
  break.Copy the code

Among them:

  • NodeTypes.VNODE_CALL corresponds to genVNodeCall and the ast.ts createVNodeCall method, which returns VNodeCall. The former generates the corresponding VNodeCall render string. Is the core of the render method string.
  • NodeTypes.FOR corresponds to the FOR keyword element node, inside which the genNode method is recursively called.
  • NodeTypes.TEXT The corresponding TEXT element node is responsible for generating static TEXT.
  • Node type NodeTypes.JS_FUNCTION_EXPRESSION Corresponds to the method invocation node, which is responsible for generating method expressions.

Finally, after a series of processing, the final result of the render method string is as follows:

(function anonymous(
) {
const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = ["data-a"] // Static node
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p".null."123", -1 /* HOISTED */)// Static node

return function render(_ctx, _cache) {/ / render method
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue / / helper method

    return (_openBlock(), _createElementBlock(_Fragment, null, [
      _createElementVNode("div", { "data-a": attr }, _toDisplayString(name), 9 /* TEXT, PROPS */, _hoisted_1),
      _hoisted_2
    ], 64 /* STABLE_FRAGMENT */}}})))Copy the code

_createElementVNode, _openBlock, and other helper methods passed in from the previous step.

123

is a static node with no responsive binding, and the createElementVNode method is used to create dynamic nodes. The createElementVNode method is used to create the VNode.

The render method uses the with keyword, as shown in the following code:

const obj = {
  a:1
}
with(obj){
  console.log(a) / / print 1
}
Copy the code

Under the with(_ctx) wrapper, the reactive variables we define in data can only be used properly, such as calling _toDisplayString(name), where name is the reactive variable.

Get the final VNode object

Finally, this is executable code that will be assigned to the component. render method. The source code is in packages run-time core SRC component.ts as follows:

. Component.render = compile(template, finalCompilerOptions) ...if (installWithProxy) { // Bind the proxy
   installWithProxy(instance)
}
...
Copy the code

Compile method is the entry to the original baseCompile method. After the assignment, we need to bind the installWithProxy method to the run-time core/ SRC /component.ts method.

export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
  installWithProxy = i= > {
    if(i.render! ._rc) { i.withProxy =new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }
}
Copy the code

This is mainly to render the _ctx response variable binding, when the above render method name is used, can listen to the call through the proxy, which will enter the responsive listener collection track, when trigger listener, diff.

At runtime – core/SRC/componentRenderUtils. Ts source in renderComponentRoot execution will render method get VNode object in the method, its core code as shown below:

export function renderComponentRoot(){
  / / render execution
  letresult = normalizeVNode(render! .call( proxyToUse, proxyToUse! , renderCache, props, setupState, data, ctx )) ...return result
}
Copy the code

The resulting VNode object in the demo code is shown below.

Above is through the render method of operation after get VNode object, you can see the children and dynamicChildren distinguish, the former includes two child nodes are respectively the < div > and < p > this and in < the template > definition is corresponding to the content of the inside, while the latter only store the dynamic node, Includes the dynamic props, or data-a attribute. Vnodes are also a tree structure, progressing through children and dynamicChildren layer by layer.

The process of obtaining VNode through render method is also the process of parsing and constructing a series of Vue syntax such as instructions, interpolation expressions, responsive data and slots, and finally generating structured VNode objects. The whole process can be summarized into a flow chart for readers to understand, as shown in the following figure.

Another attribute that needs to be paid attention to is patchFlag, which is the flag bit used in later VNode diff. The number 64 indicates that it is stable and does not need to be changed. Finally, the VNode object needs to be converted into a real DOM node. This part of the logic is completed in the diff of the virtual DOM, which will be explained in the following bidirectional binding principle analysis.