How Vue templates are compiled

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

.vueEveryone is familiar with this, calling render will get the virtual DOM corresponding to the incoming template ( file), so where does this render come from? How does it convert  .vue files into browser-recognizable code?

There are two ways how the render function comes from

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

This article will introduce these two types, as well as the detailed principles of the compilation process.

Understanding Template Compilation

We know that  <template></template> this is a template, not real HTML. Browsers don't recognize templates, so we need to compile it into native HTML that browsers recognize.

The main process of this piece is

  1. Extract the native HTML and non-native HTML in the template, such as bound attributes, events, instructions, etc.
  2. Generate a render function after some processing
  3. The render function generates the corresponding vnode from the template content
  4. Then go through the patch process ( Diff ) to get the vnode to be rendered into the view
  5. Finally, create a real DOM node based on vnode, that is, insert native HTML into the view to complete the rendering

Items 1, 2, and 3 above are the process of template compilation

So how does it compile and finally generate the render function?

Detailed explanation of template compilation - source code

baseCompile()

This is the entry function of template compilation, which receives two parameters

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

There are three main steps in the compilation process:

  1. Template parsing: extract  <template></template> the tag elements, attributes, variables and other information in the template through regularization and other methods, and parse it into an abstract syntax tree AST
  2. Optimization: traverse  AST to find out the static nodes and static root nodes, and add tags
  3. Code Generation: According to  AST the generated rendering function render

These three steps correspond to three functions respectively, which will be introduced one by one later, first look at  baseCompile where they are called in the source code

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string, // 就是要转换的模板字符串
  options: CompilerOptions //就是转换时需要的参数
): CompiledResult {
  // 1. 进行模板解析,并将结果保存为 AST
  const ast = parse(template.trim(), options)

  // 没有禁用静态优化的话
  if (options.optimize !== false) {
    // 2. 就遍历 AST,并找出静态节点并标记
    optimize(ast, options)
  }
  // 3. 生成渲染函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render, // 返回渲染函数 render
    staticRenderFns: code.staticRenderFns
  }
})

Just a few lines of code, three steps, calling three methods is very clear

Let's take a look at what is returned at the end, and then delve into the source code of the methods called in the above three steps, so that we can know more clearly what the three steps are to do

compile result

For example, there is such a template

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

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

{
  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: []
}

It doesn't matter if you don't understand, just pay attention to what the three steps mentioned above have done

  • ast Field, which is generated in the first step
  • static The field, that is, the tag, is added  in the second step according to  ast the type
  • render Field, which is generated in the third step

I have a general impression, and then look at the source code

1. parse()

Source address:src/complier/parser/index.js - 79行

This method is the main function of the parser, which extracts  <template></template> all the  tag, props, and children information in the template string through regularization and other methods, and generates an ast object with a corresponding structure

parse takes two parameters

  • template : is the template string to be converted
  • options: It is the parameter required for conversion. It contains four hook functions, which are used to  parseHTML extract the parsed string and generate the corresponding AST

The core steps are as follows:

Call  parseHTML the function to parse the template string

  • Parse to the start tag, end tag, text, and comments for different processing
  • When encountering text information during parsing, call the text parser  parseText function for text parsing
  • When an include filter is encountered during the parsing process, the filter parser  parseFilters function is called for parsing

The results of each step of parsing are merged into an object (that is, the final AST)

The source code of this place is really too long, there are hundreds of lines of code, so I will just post a rough outline. If you are interested, you can take a look at the above. When parsing the text, the call will be added according to different types of nodes to mark  chars() the  typenodes  AST . Type, this attribute will be used in the next step of marking

type AST node type
1 element node
2 dynamic text node containing variables
3 Plain text nodes without variables

2. optimize()

This function is  AST to find the static node and the static root node in it, and add a mark, so that the comparison of the static node will be skipped in the later  patch process, and a copy of the past will be directly cloned, thereby optimizing  patch the performance

The external function called in the function will not paste the code, the general process is like this

  • **mark static node(markStatic)**. It is to judge the type. The three types with values ​​of 1, 2, and 3 are introduced above.

    • The type value is 1: it is a node that contains child elements, set static to false and mark child nodes recursively until all child nodes are marked
    • type value is 2: set static to false
    • The type value is 3: it is a plain text node that does not contain child nodes and dynamic attributes. Set it to static = true, and this will be skipped when patching, and a copy will be cloned directly
  • **Mark static root nodes (markStaticRoots)**, the principle here is basically the same as that of marking static nodes, only nodes that meet the following conditions can be counted as static root nodes

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

Source address:src/complier/optimizer.js - 21行

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

3. generate()

This is the function that generates render, which means that it will eventually return something like the following

// 比如有这么个模板
<template>
    <div id="app">{
   
   {name}}</div>
</template>

// 上面模板编译后返回的 render 字段 就是这样的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`

// 把内容格式化一下,容易理解一点
with(this){
  return _c(
    'div',
    { attrs:{"id":"app"} },
    [  _v(_s(name))  ]
  )
}

Does this structure look familiar?

If you understand the virtual DOM, you can see that the above render is exactly the structure of the virtual DOM, which is to divide a label into  tag, props, children, there is nothing wrong

Before looking at  generate the source code, we need to understand  render what the last returned field above means, and then look at  generate the source code, it will be much easier. Otherwise, we don’t even know what the function returns. How can we understand this function? Woolen cloth

render

Let's translate the compiled above render

This  is introduced in the first volume of with " JavaScript You Don't Know ", which is a keyword used to cheat lexical scope, which allows us to refer to multiple properties on an object faster

see an example

const name = '掘金'
const obj = { name:'沐华', age: 18 }
with(obj){
    console.log(name) // 沐华  不需要写 obj.name 了
    console.log(age) // 18   不需要写 obj.age 了
}

with(this){} The above  is  this the current component instance. Because by  with changing the pointer of the attribute in the lexical scope, it is  name necessary to use it directly in the label, instead of  this.name this

 What is that  _c_v and  ?_s

This is defined in the source code, the format is: ** _c(abbreviation) =  createElement(function name)**

Source address:src/core/instance/render-helpers/index.js - 15行

// 其实不止这几个,由于本文例子中没有用到就没都复制过来占位了
export function installRenderHelpers (target: any) {
  target._s = toString // 转字符串函数
  target._l = renderList // 生成列表函数
  target._v = createTextVNode // 创建文本节点函数
  target._e = createEmptyVNode // 创建空节点函数
}
// 补充
_c = createElement // 创建虚拟节点函数

Let's see if it becomes clearer

with(this){ // 欺骗词法作用域,将该作用域里所有属姓和方法都指向当前组件
  return _c( // 创建一个虚拟节点
    'div', // 标签为 div
    { attrs:{"id":"app"} }, // 有一个属性 id 为 'app'
    [  _v(_s(name))  ] // 是一个文本节点,所以把获取到的动态属性 name 转成字符串
  )
}

Next, let's look at  generate() the source code

generate

Source address:src/complier/codegen/index.js - 43行

This process is very simple, only a few lines of code, is to first judge  AST whether it is empty, if not empty, create a vnode according to the AST, otherwise create a vnode with an empty div

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 就是先判断 AST 是不是为空,不为空就根据 AST 创建 vnode,否则就创建一个空div的 vnode
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'

  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

It can be seen that it is mainly  genElement() created through methods  vnode , so let's take a look at its source code to see how it was created

genElement()

Source address:src/complier/codegen/index.js - 56行

The logic here is still very clear, that is, a bunch of  if/else judgments on the attributes of the AST element nodes passed in to execute different generation functions

It can also be found here that another knowledge point, v-for, has a higher priority than v-if, because the 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)

    // template 节点 && 没有插槽 && 没有 pre 标签
  } 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 (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      // 获取元素属性 props
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 获取元素子节点
      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)
    }
    // 返回上面作为 with 作用域执行的内容
    return code
  }
}

The generation functions called by each type are not listed one by one. In general, there are only three types of vnode nodes created at last, element nodes, text nodes, and comment nodes.

custom render

Let me give you an example, the three situations are as follows

// 1. test.vue
<template>
    <h1>我是沐华</h1>
</template>
<script>
  export default {}
</script>
// 2. test.vue
<script>
  export default {
    render(h){
      return h('h1',{},'我是沐华')
    }
  }
</script>
// 3. test.js
export default {
  render(h){
    return h('h1',{},'我是沐华')
  }
}

The above three, the final rendering is exactly the same , because this  h is the one after the above template is compiled _c

At this time, someone may ask, why write it yourself, isn't it automatically generated by template compilation?

Good question! It is definitely beneficial to write your own

  1. If you write the vnode yourself, you will skip the template compilation directly, and you don’t need to parse the dynamic attributes, events, instructions, etc. in the template, so the performance will be slightly improved. This is reflected in the priority of the rendering below
  2. There are also some situations that can make our code writing more flexible, more convenient and concise, and will not be redundant

For example,  Element-UI there are a large number of directly written render functions in the source code of the components inside.

Next, let's see how these two points are reflected.

1. Rendering priority

First look at the part about template compilation in the life cycle of the official website

As you can see from the picture, if there is  template, it will not be taken care  el of, so  template has a higher priority than el , for example

So what if we wrote our own render?

<div id='app'>
    <p>{
   
   { name }}</p>
</div>
<script>
    new Vue({
        el:'#app',
        data:{ name:'沐华' },
        template:'<div>掘金</div>',
        render(h){
            return h('div', {}, '好好学习,天天向上')
        }
    })
</script>

After this code is executed, the page is rendered with only <div>好好学习,天天向上</div>

It can be concluded that  the render function has a higher priority

Because no matter it is  el mounted,  template it will be compiled into  render a function in the end, and if there is already  render a function, the previous compilation will be skipped

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;
    // 如果没有 render 
    if (!options.render) {
      var template = options.template;
      // 再判断,如果有 template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          return this
        }
      // 再判断,如果有 el
      } else if (el) {
        template = getOuterHTML(el);
      }
    }
    return mount.call(this, el, hydrating)
  };

2. More flexible writing

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>

I don't know if you have written code similar to the above?

Let's write the same code as above in another way, just write render

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

Done! That's it! That's it?

That's right, that's it!

Or the following is very convenient when calling multiple times

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

Guess you like

Origin blog.csdn.net/asfasfaf122/article/details/128784664