Small knowledge, big challenge! This paper is participating in theEssentials for programmers”Creative activities

new Vue({
  render: h= > h(App)
})
Copy the code

Call render to render the virtual DOM corresponding to the template (.vue file) that was passed in. How does it turn a.vue file into browser-aware code?

How does the render function come about in two ways

  • The first is to generate the render function through template compilation
  • The second is that we define the render function ourselves in the component, which skips the template compilation process

This article will introduce you to each of these and the detailed compilation process principles

Understanding template compilation

We know that
is a template, not real HTML. Browsers don’t know templates, so we need to compile it into native HTML that browsers know

The main flow of this piece is

  1. Extract native and non-native HTML from templates, such as bound properties, events, directives, and so on
  2. The render function is generated after some processing
  3. The render function then generates the template content into the corresponding VNode
  4. The patch process (Diff) is followed to get the VNodes to be rendered into the view
  5. Finally, create a real DOM node from the VNode, that is, native HTML, and insert it into the view to complete the rendering

Steps 1, 2, and 3 above are the template compilation process

How does it compile to produce the render function?

Template compilation details – source code

baseCompile()

This is the entry function for template compilation, and it takes two arguments

  • template: is the template string to be converted
  • options: is the parameter required for conversion

There are three main steps in the compilation process:

  1. Template resolution: extracted by means of re<template></template>Tag elements, attributes, variables, and other information in templates are parsed into abstract syntax treesAST
  2. Optimization: TraversalASTFind the static node and the static root node, and add the tag
  3. Code generation: based onASTGenerate render functionrender

These three steps correspond to three functions, which will be introduced later. First look at baseCompile source code. Where is the call

SRC /complier/ index.js-11 line

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string, // is the template string to convert
  options: CompilerOptions // This is the parameter needed for conversion
) :CompiledResult {
  // 1. Parse the template and save the result as the AST
  const ast = parse(template.trim(), options)
  
  // Static optimization is not disabled
  if(options.optimize ! = =false) {
    // 2. Go through the AST and find the static node and mark it
    optimize(ast, options)
  }
  // 3. Generate the render function
  const code = generate(ast, options)
  return {
    ast,
    render: code.render, // 返回渲染函数 render
    staticRenderFns: code.staticRenderFns
  }
})
Copy the code

It’s just a few lines of code, three steps, three methods called and it’s pretty clear

Let’s take a look at what the last return is, and then go into the source code of the above three steps respectively, so as to have a clearer idea of what the three steps are respectively to do

Compile the results

Let’s say I have a template like this

<template>
    <div id="app">{{name}}</div>
</template>
Copy the code

Print the compiled result, which is the result of the source code return above, and see what it looks like

{
  ast: {
    type: 1.tag: 'div'.attrsList: [{name: 'id'.value: 'app'}].attrsMap: { id: 'app' },
    rawAttrsMap: {},
    parent: undefined.children: [{type: 2.expression: '_s(name)'.tokens: [{'@binding': 'name'}].text: '{{name}}'.static: false}].plain: false.attrs: [{name: 'id'.value: '"app"'.dynamic: undefined}].static: false.staticRoot: false
  },
  render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`.staticRenderFns: [].errors: [].tips: []}Copy the code

It doesn’t matter if you don’t understand, notice what the three steps above do

  • astThe fields are generated in the first step
  • staticThe field, which is the tag, is based on the second stepastIn thetypeadded
  • renderField, which is generated in step 3

Have a general impression, and then look at the source code

1. parse()

Source address: SRC/complier/parser/index. The js – 79 line

This method is the main function of the parser, which extracts all the tags, props, and children in the
template string using the re method to generate an AST object with the corresponding structure

Parse accepts two arguments

  • template: is the template string to be converted
  • options: is the parameter required for conversion. It contains four hook functions, which are used to putparseHTMLThe parsed string is extracted and the correspondingAST

The core steps are as follows:

The parseHTML function is called to parse the template string

  • Parsing to start tag, end tag, text, comment for different processing
  • The text parser is invoked when text information is encountered during parsingparseTextFunction for text parsing
  • When a contain filter is encountered during parsing, the filter parser is invokedparseFiltersFunction parsing

The results of each step of parsing are merged into one object (the final AST)

The source code of this place is too long, there are hundreds of lines of code, I will only post about it, interested in their own to have a look

export function parse (
  template: string, // The template string to convert
  options: CompilerOptions // Parameters needed for conversion
) :ASTElement | void {
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // called when parsing to the start tag, such as 
      
start (tag, attrs, unary, start, end) { // unary is a self-closing tag, such as . },// called when parsing to end tags, such as end (tag, start, end) { ... }, // called when text is parsed chars (text: string, start: number, end: number) { // A lot of things are checked here to see if it's dynamic text with variables // Then create AST nodes for dynamic or static text. },// called when parsing to a comment comment (text: string, start, end) { // This is the way to find the comment const comment = / ^ if (comment.test(html)) { // If it is a comment, continue to look for '-->' const commentEnd = html.indexOf('-->')... }})// Return the AST return root } Copy the code

The above call to chars(), which parses the text above, marks the AST node type with a different type depending on the node type, which will be used in the next tag

type AST Node Type
1 Element nodes
2 A dynamic text node containing variables
3 Plain text node with no variables

2. optimize()

This function is to find out static nodes and static root nodes in the AST and add labels. In order to skip the comparison of static nodes in the subsequent patch process, a copy of the past will be directly cloned, thus optimizing the patch performance

The external function called inside the function is not attached code, the general process is like this

  • Mark a static node (markStatic). The value is 1, 2, and 3

    • A value of type 1: the node that contains child elements, set static to false and recursively mark the child nodes until all of them are marked
    • Set type to 2: static to false
    • Static = true, patch; static = true, patch; static = true; static = true
  • Marking static root nodes (markStaticRoots), here the principle is basically the same as marking static nodes, except that nodes that meet the following criteria can be counted as static root nodes

    • The node itself must be static
    • There must be child nodes
    • Child nodes cannot have only one text node

Source address: SRC/complier/optimizer. Js – 21 rows

export function optimize (root: ? ASTElement, options: CompilerOptions) {
  if(! root)return
  isStaticKey = genStaticKeysCached(options.staticKeys || ' ')
  isPlatformReservedTag = options.isReservedTag || no
  // Mark the static node
  markStatic(root)
  // Mark the static root node
  markStaticRoots(root, false)}Copy the code

3. generate()

This is the function that generates render, which ultimately returns something like the following

// There is a template like this
<template>
    <div id="app">{{name}}</div>
</template>

// This is what the render field looks like when the template is compiled
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`

// Format the content to make it easier to understand
with(this){
  return _c(
    'div',
    { attrs: {"id":"app"} },
    [  _v(_s(name))  ]
  )
}
Copy the code

Does this structure look familiar?

Render (props, children, props, props, props, props, props, props, props

Before we look at the generate source code, we need to look at the render field returned above. It is much easier to look at the generate source code without knowing what the function returns

render

Let’s translate the render compiled above

The with keyword, introduced in JavaScript you Don’t Know, is used to cheat lexical scopes, allowing us to reference multiple properties on an object more quickly

Look at an example

const name = 'the nuggets'
const obj = { name:'MuHua'.age: 18 }
with(obj){
    console.log(name) // Muhua does not need to write obj.name
    console.log(age) // 18 does not need to write obj.age
}
Copy the code

The this in with(this){} above is the current component instance. Because with changes the direction of the lexical attribute, you can use name in the tag instead of this.name

What are _c, _V, and _s?

_c(abbreviation) = createElement(function name)

SRC /core/instance/render-helpers/ index.js-15

// There are more than just a few of them, because they are not used in this example
export function installRenderHelpers (target: any) {
  target._s = toString // Go to the string function
  target._l = renderList // Generate list functions
  target._v = createTextVNode // Create a text node function
  target._e = createEmptyVNode // Create an empty node function
}
/ / added
_c = createElement // Create a virtual node function
Copy the code

If we look at it again, it will be much clearer

with(this){ // Trick the lexical scope by referring all generic names and methods in the scope to the current component
  return _c( // Create a virtual node
    'div'.// The tag is div
    { attrs: {"id":"app"}},// Have an attribute id 'app'
    [  _v(_s(name))  ] // is a text node, so the acquired dynamic attribute name is converted to a string)}Copy the code

Next, look at the generate() source code

generate

Source address: SRC/complier codegen/index, js – 43

The process is simple, just a few lines of code, to determine whether the AST is empty, and then create a VNode based on the AST, otherwise create an empty DIV VNode

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) :CodegenResult {
  const state = new CodegenState(options)
  If the AST is not empty, create a vnode based on the AST. If it is not empty, create a vnode with an empty div
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  
  return {
    render: `with(this){return ${code}} `.staticRenderFns: state.staticRenderFns
  }
}
Copy the code

The genElement() method is used to create a vNode, so let’s take a look at the source code

genElement()

Source address: SRC/complier codegen/index, js – 56

The logic here is pretty clear, just a bunch of if/else attributes of the AST element node passed in to perform different generators

V-for has a higher priority than V-if, because for is judged first

export function genElement (el: ASTElement, state: CodegenState) :string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if(el.staticRoot && ! el.staticProcessed) {return genStatic(el, state)
  } else if(el.once && ! el.onceProcessed) {// v-once
    return genOnce(el, state)
  } else if(el.for && ! el.forProcessed) {// v-for
    return genFor(el, state)
  } else if(el.if && ! el.ifProcessed) {// v-if
    return genIf(el, state)
     
    // The template node && has no slot && has no pre tag
  } else if (el.tag === 'template'&&! el.slotTarget && ! state.pre) {return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') { // v-slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    // If there are child components
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      // Get the element attribute props
      if(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) }// Get the element child node
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : ' ' // data
      }${
        children ? `,${children}` : ' ' // children
      }) `
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    // Returns what was executed above as the with scope
    return code
  }
}
Copy the code

The generating functions called by each type are not listed. In general, there are only three types of VNodes created: element node, text node, and comment node

Custom render

So, to give you an example, there are three cases

// 1. test.vue
<template>
    <h1>I am a MuHua</h1>
</template>
<script>
  export default {}
</script>
Copy the code
// 2. test.vue
<script>
  export default {
    render(h){
      return h('h1', {},'I am Mu Hua')
    }
  }
</script>
Copy the code
// 3. test.js
export default {
  render(h){
    return h('h1', {},'I am Mu Hua')}}Copy the code

The final rendering of the above three types is exactly the same, because the h is the _C that was compiled from the above template

At this point, one might ask, why do you write it yourself, when templates are compiled automatically?

That’s a good question! There must be an advantage to writing your own

  1. When you write vNode, you skip template compilation and don’t have to parse dynamic properties, events, instructions, etc., so you get a bit of a performance boost. This is reflected in the priority of the rendering below
  2. There are also cases where we can write code in a way that makes it more flexible, more convenient, less redundant

For example, the Element UI component source code has a lot of direct render function

Now let’s see how these two things are reflected

1. Render priority

Take a look at the template compilation section of the life cycle on the official website

As you can see, if you have template, you don’t care about EL, so template takes precedence over EL, for example

What if we wrote render ourselves?

<div id='app'>
    <p>{{ name }}</p>
</div>
<script>
    new Vue({
        el:'#app'.data: {name:'MuHua' },
        template:'< div > Denver < / div >'.render(h){
            return h('div', {}, 'Good good study, day day up')}})</script>
Copy the code

It follows that the render function has a higher priority

Either the EL or emplate will be compiled into the render function, and if you already have the render function, skip the previous compilation

This is also reflected in the source code

Find the answer in the source code: dist/ vue.js-11927

  Vue.prototype.$mount = function ( el, hydrating ) {
    el = el && query(el);
    var options = this.$options;
    // If there is no render
    if(! options.render) {var template = options.template;
      // check if there is a template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) = = =The '#') { template = idToTemplate(template); }}else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          return this
        }
      // If there is el
      } else if(el) { template = getOuterHTML(el); }}return mount.call(this, el, hydrating)
  };
Copy the code

2. Write more flexibly

For example, when we need to write a lot of if judgments

<template>
    <h1 v-if="level === 1">
      <a href="xxx">
        <slot></slot>
      </a>
    </h1>
    <h2 v-else-if="level === 2">
      <a href="xxx">
        <slot></slot>
      </a>
    </h2>
    <h3 v-else-if="level === 3">
      <a href="xxx">
        <slot></slot>
      </a>
    </h3>
</template>
<script>
  export default {
    props: ['level']}</script>
Copy the code

Have you ever written code like the one above?

Let’s write the same code in a different way: render

<script>
  export default {
    props: ['level'].render(h){
      return h('h' + this.level, this.$slots.default())
    }
  }
</script>
Copy the code

Done! Just this! Is this it?

Yes, there it is!

Or the following, which is handy when you call it multiple times

<script>
  export default {
    props: ['level'].render(h){
      const tag = 'h' + this.level
      return (<tag>{this.$slots.default()}</tag>)
    }
  }
</script>
Copy the code

supplement

If you want to know more about what the template looks like when it compiles, you can do this. Okay

Vue2 template compilation can be installed with vuE-template-Compiler

Template compilation for Vue3 can be done here

And test it on your own.

In addition, there are some changes in Vue3 template compilation, you can click the link below to see the simple virtual DOM and Diff algorithm, which is introduced

Past wonderful

  • Simple virtual DOM and Diff algorithms, and Vue2 and Vue3 differences
  • Vue3 7 and Vue2 12 components communication, worth collecting
  • What are the latest updates to Vue3.2
  • JavaScript advanced knowledge
  • Front-end anomaly monitoring and DISASTER recovery
  • 20 minutes to help you learn HTTP and HTTPS, and consolidate your HTTP knowledge

conclusion

If this article is of any help to you, please give it a thumbs up. Thank you