In the last article, we introduced the principle of vUE’s implementation of responsiveness. Among them, there are two points that need to be analyzed:

    1. Vue component implementation
    1. What happens during render function execution

Before we discuss the above two issues, we need to understand the compiler process. This is the core premise, and it is only when we are familiar with it that we can clearly understand the flow of data.

Good, nonsense not much say, twist sleeve is dry ~

A Compiler.

In Vue, we write template, which obviously the browser doesn’t know. Then there needs to be an interpretation process.

Compiler can be divided into two cases:

    1. When building the compiler
    1. The runtime compiler

When building the compiler

For local development, use webpack + vue-loader to process.vue files. Such as:

<template>
  <div>{{ a }}</div>
</template>

<script>
export default {
  data() {
    return {
      a: '1'
    }
  }
}
</script>

<style>

</style>
Copy the code

When packing, vue-loader converts the contents of the.vue file into the render function

The runtime compiler

Runtime compiler, we do not use plug-ins like vue-loader, and write template directly, and let Vue dynamically convert template to render function when the browser runs. Such as:

<html>
  <head>
    <meta charset="utf-8"/>
  </head>

  <body>
    <div id='root'>

    </div>
    <script src=".. /vue/dist/vue.js"></script>
    <script>

      let vm = new Vue({
        el: '#root'.template: '<div>{{ a }}</div>'.data() {
          return {
            a: "This is the root node."}}})</script>
  </body>
</html>
Copy the code

Essentially, both build-time and run-time compiler are converted to render functions. Display build is more efficient, in our production environment, try to avoid running, then compiler.

Careful students will ask: since all are converted to the render function, it is not possible to write the render function?

The answer is yes, for example:

<html>
  <head>
    <meta charset="utf-8"/>
  </head>

  <body>
    <div id='root'>

    </div>
    <script src=".. /vue/dist/vue.js"></script>
    <script>

      let vm = new Vue({
        el: '#root'.data() {
          return {
            a: "This is the root node."}},render(createElement) {
          return createElement('div', {
            attrs: {
              id: 'test'}},this.a)
        }
      })
    </script>
  </body>
</html>
Copy the code

Write render by hand and Vue executes render directly, eliminating the compiler process. But handwriting render, for our development and maintenance are not friendly. It is still recommended that you use Webpack + VUe-loader and compiler when building.

In addition, runtime compiler is recommended for learning.

Now, we’ll use runtime Compiler to find out.

2. CompileToFunctions

Vue will first compileToFunctions if no render function is passed to mount vue and mount the render function to vn. $options. In this way, the virtual DOM can be generated before patch execution.

The core main process code is as follows:

export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  const ast = parse(template.trim(), options)
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
Copy the code

The overall process is divided into three parts:

    1. Template compilation stage
    1. The optimization phase
    1. Code generation phase – which translates to the render function

Now, we’re going to break it down one by one

3. The AST

Ast full name: Abstract Syntax Tree Is an abstract representation of the syntactic structure of source code.

In computers, the essence of any problem is data structure + algorithm, and ast is also a data structure that describes a structured representation of source code.

Take our run-time demo above as an example:

The first argument to the parse method is template, which is a string:

"
      
{{a}}
"
Copy the code

Let’s start with the core entry to the AST generation parse method:

/ /... Omit a bunch of function definitions
parseHTML(template, {
  // options. .start() {
     // ...
  },
  
  end() {
    // ...
  },
  
  chars() {
    // ...
  },
  comment() {
    // ...}})Copy the code

The parseHTML trunk is as follows:

export function parseHTML (html, options) {
  const stack = []
  / /... options
  let index = 0
  let last, lastTag;
  
  while(html) {
    last = html
    
    if(! lastTag || ! isPlainTextElement(lastTag)) {let textEnd = html.indexOf('<')
      
      if(textEnd == 0) {
        
        if(comment.test(html)) {
          const commentEnd = html.indexOf('-->')
          // ...
          if(commentEnd >= 0) {
            // ...
            advance(commentEnd + 3)
            continue}}if(conditionalComment.test(html)) {
          // ...
          const conditionalEnd = html.indexOf('] > ')
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue}}if(html.match(doctype)) {
          // ...
          advance(doctypeMatch[0].length)
          continue
        }
        
        if(html.match(endTag)) {
          // ...
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
        
        startTagMatch = parseStartTag()
        if(startTagMatch) {
          // ...
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)}continue}}if(textEnd >= 0) {
        // If < appears in plain text, it should be treated as text first
      }
      
      if(textEnd < 0) {
        / /... Assign text, advance to skip the length of the text}}}else {
    // Handle script,style,textarea elements
    // Here we only deal with the textarea element. The other two vues warn against this
    // ...
  }
  
  function advance (n) {
    index += n
    html = html.substring(n)
  }
}
Copy the code

You can see that parseHTML actually has a lot of re’s written to handle strings. This is not written from scratch, but from the HTML Parse library written by John Resig.

Who is John Resig? Is the famous father of JQuery. People who lived through the era of jquery, when jquery was a god.

Ok, back to business, template string processing, the general process is as follows:

    1. While loop template string
    1. Check whether the tag cannot be script or style, and give the corresponding warning message
    1. Gets the string position of the start tag < with the re
    1. Through the re, determine whether it is a comment node, call advance, re-record the index subscript, skip the comment length, intercept the comment to continue the loop
    1. Through the re, determine whether it is a conditional comment node. Because we might use conditional annotations in the template to do things for IE. Similarly, call advance, subscript index, jump to the end of the conditional comment string, intercept the conditional comment, and continue the loop.
    1. Determine whether it is a Doctype node through the re. Similarly, call advance to jump index to the end of the Doctype node string, intercept the Doctype, and continue the loop.
    1. Determine whether it is the start label by using the re, extract the content of the start label, and compare before and after extraction:
// Before matching the start tag
html = "
      
{{a}}
"
// After extraction html = "{{ a }}</div>" // startTagMatch looks like this: { start: 0.end: 24.tagName: 'div'.unarySlash: ' '.attrs: [ "Data-test =' This is a test attribute '".// ...]}Copy the code

After the start tag is parsed, the start method on the second parseHTML argument, options, is called.

/ /... Omit a bunch of function definitions
parseHTML(template, {
  // options. .start(tag, attrs, unary, start, end) {
     Parse a portion of the HTML string and call the lifecycle functions start, end, chars, comment
     
     // ...
     let element: ASTElement = createASTElement(tag, attrs, currentParent)
     
     // ...
  },
  
  end(){},// ...
 
})
Copy the code

As we already know from the above flow, parseHtml will first extract the contents of the start tag, namely:

<div data-test='This is a test property'>
Copy the code

Then, based on the startTagMatch data, call the start method, which calls createASTElement to return astElement. Its structure is as follows:

{
  type: 1.tag: 'dev'.rawAttrsMap: {},
  parent: undefined.children: [].attrsMap: {
    "data-test": "This is a test property."
  },
  attrsList: [{start: 5.end: 23.name: 'data-test'.value: "This is a test property."}}]Copy the code
    1. After the start tag content is processed, the string after the start content is removed and looks like this:
html = "{{ a }}</div>"
Copy the code

Enter the next while loop, and the remaining string, as HTML field values, goes through the process again. This will enter:

if(textEnd >= 0) {}Copy the code

The text variable is recorded, that is:

text = "{{ a }}"
Copy the code

Call advance and move index to the end of the text string, truncating {{a}}

    1. Enter the next while loop, which is:
html = "</div>"
Copy the code

Repeat the above process to match the condition to the end tag and enter: endTagMatch, i.e. </div>. Call the advance method to move index to the end. Call the parseEndTag method to trigger the end hook.

Advance is a subscript calculator that automatically moves to the end of the parsed end for the next part of the parsing

The parseHTML method calls the corresponding hook function to generate the corresponding AST node while parsing the different content, and finally completes the transformation of the entire template string into the AST

In general, there are three types of AST.

  1. Normal tag node processing, created by createASTElement method, its structure is as follows:
{
  type: 1,
  tag,
  attrsList: [
    // ...].attrsMap: {
    // ...
  },
  rawAttrsMap: {},
  parent,
  children: []}Copy the code
  1. Matching to a character variable is associated with the parseText interpreter, which has the following structure:
{
  type: 2.text: "{{ a }}".expression: "_s(a)".tokens: [{'@binding': 'a'}].start: 24.end: 31
}
Copy the code
  1. Plain text, without variables, with the following structure:
{
  type: 3.text: "Textual content"
  isComment: true.start: xx,
  end: xx
}
Copy the code

Finally, the template string parses the ast structure as follows:

{
    "type": 1."tag": "div"."attrsList": [{"name": "data-test"."value": "This is a test property."."start": 5."end": 23}]."attrsMap": {
	"data-test": "This is a test property."
      },
       "rawAttrsMap": {
	  "data-test": {
	    "name": "data-test"."value": "This is a test property."."start": 5."end": 23}},"children": [{"type": 2."expression": "_s(a)"."tokens": [{"@binding": "a"}]."text": "{{ a }}"."start": 24."end": 31}]."start": 0."end": 37."plain": false."attrs": [{"name": "data-test"."value": "\" This is the test property \""."start": 5."end": 23}}]Copy the code

Just to clarify, we see that expression has an _s, what is this thing? In fact, this is defined in render helpers of Vue Instance. _s = toString

Its definition is as follows:

export function toString (val: any) :string {
  return val == null
    ? ' '
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null.2)
      : String(val)
}
Copy the code

Ok, at this point, the ast backbone is finished.

4. Optimize

After obtaining the AST tree, Vue performs a layer of static markup optimization. Mark some unchanged nodes to improve the performance of patch diff. For example, there are tags like this:

<div>
  <div>That's the same thing 1</div>
  <div>That's the same thing 2</div>
  <div>This is the same thing 3</div>
  <div>This is the same thing 4</div>
</div>
Copy the code

So, during diff, this tag does not need to be compared, it is purely static tag, will not change. The outermost div is called the static root node.

According to the AST generated above, we know that there are three types of AST, which are:

  1. Type == 1, common element node
  2. Type == 2, containing the text of the variable
  3. Type == 3, plain text, no variables

Static = true; static = false; static = 1; static = true;

function isStatic (node: ASTNode) :boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return!!!!! (node.pre || ( ! node.hasBindings &&// no dynamic bindings! node.if && ! node.for &&// not v-if or v-for or v-else! isBuiltInTag(node.tag) &&// not a built-in
    isPlatformReservedTag(node.tag) && // not a component! isDirectChildOfTemplateFor(node) &&Object.keys(node).every(isStaticKey)
  ))
}
Copy the code

That is:

    1. If the node uses the V-pre instruction, it is presumed to be static.
    1. Without pre, the following conditions must be met:
    • 2.1 Cannot have the attribute starting with v-,@, :
    • 2.2 Cannot be built-in slot, Component
    • 2.3 Must be browser reserved tags, not components
    • 2.4 Cannot be a V-for template tag
    • 2.5 Check whether each key on the AST exists only on static nodes

After the completion of the marking, each child was recursively marked on the AST.

On this basis, the root static node is calculated, so when diff, it is the root static node, so everything below the root node does not need to be compared.

After optimization, the structure of the ast looks like this :(2 more attributes)

{
    "type": 1."tag": "div".// Add static tags here
    "static": false.// Add the root static node flag here
    "staticRoot": false."attrsList": [{"name": "data-test"."value": "This is a test property."."start": 5."end": 23}]."attrsMap": {
	"data-test": "This is a test property."
      },
       "rawAttrsMap": {
	  "data-test": {
	    "name": "data-test"."value": "This is a test property."."start": 5."end": 23}},"children": [{// Add the tag here
            "static": false."type": 2."expression": "_s(a)"."tokens": [{"@binding": "a"}]."text": "{{ a }}"."start": 24."end": 31}]."start": 0."end": 37."plain": false."attrs": [{"name": "data-test"."value": "\" This is the test property \""."start": 5."end": 23}}]Copy the code

5. Generate

In the code generation stage, the AST will transform it into the render function, whose code is as follows:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) :CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}} `.staticRenderFns: state.staticRenderFns
  }
}
Copy the code

Generation code stage is relatively clear, genElement method, mainly to judge different types, call different generation methods. Essentially, one JSON is converted to another. It’s worth noting that when converting JSON, we’ll see the _m, _c, _O methods.

These are available in instance/render-helpers/index.js

The method name Corresponding helper method
_m renderStatic
_o markOnce
_l renderList
_e createEmptyVNode
_t renderSlot
_b bindObjectProps
_v createTextVNode
_s toString

The render function generated here looks like this:

with(this) {
    return _c('div', {
        attrs: {
            "data-test": "This is a test property."
        }
    },
    [_v(_s(a))])
}
Copy the code

At this point, the Compiler process is complete. At this point, the Render function from the Compiler will mount to the vm.$options. The virtual DOM is generated while waiting for the updateComponent method to execute.

Note: Compiler only returns the render function and does not execute it, so dependency collection of the Dep class has not yet been triggered at this stage

6. Summary

    1. Compiler is the process of converting the template string to the render function
    1. Call the parse method to generate the AST
    • 2.1 parseHTML dynamically matches the beginning content, the content inside the tag, and the content at the end of the tag through the re
    • 2.2 It is not recommended to have script and style tags in template
    • 2.3 Start from index = 0, match the start label content, call advance to move index to the end of the previous string, and return the corresponding data structure description label start content. Parse’s start life cycle function is also called to generate the corresponding AST
    • 2.4 Handle comment node, conditional comment, Doctype node respectively, call advance to move index to the end of the string of the particular node
    • 2.5 The while loop evaluates the next string type, matching the label content
    • 2.6 Tag Content The CHars method of the Parse lifecycle is invoked to generate the corresponding AST
    • 2.7 To match the end tag, call Advance to move the index to the end of the corresponding string. Call parse’s End lifecycle method to update the end identifier bit of the corresponding AST
    • 2.8 This is repeated until the end of the HTML string is parsed.
    1. Optimize AST, mark AST of each node with static mark and static root node, so as to remove unnecessary comparison and improve performance during diFF of patch process.
    1. The ast data structure is recursively traversed through each Childrens and converted into corresponding method calls.
    1. Return the render function that mounts the method to vm.$options and waits for the virtual DOM to be generated when updateComponent is later executed

Code word is not easy, many attention, Thanks (· ω ·) Blue