Mind mapping

preface

The last article mainly wrote Vue’s data response principle is how to achieve, involving the object of data hijacking, array variation method rewrite, as well as data below the convenient value. Vue source exploration (a) responsive principle.

In this article, we will focus on the principle of Vue template compilation. First, we will review,

var vm = new Vue({
  el: '#app'.data: {
    message: 'Hello Vue! '.arr: [1.2.3]},template: '<div id="app">{{message}}</div>'
})
console.log(vm.$options.render)
Copy the code

Let’s take a look at the printout, which refers to the official Vue code.

So the template we wrote in options is turned into a render function by Vue, and we can render the page using this function. Vue needs to process the template code in order to make the function of native HTML more powerful. That’s where the template compilation process comes in.

The main purpose of this article is to figure out how Vue internally converts the Template string into a render function.

The body of the

1. Template compiler function entry

import { compileToFunction } from "./compiler"
import { initState } from "./state"

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    // This is an instance of new outside
    const vm = this
    // Put the user's options on the VM so that they are available in other methods
    vm.$options = options  // The $options option is available for subsequent methods
    // Options contains a number of options el data props
    initState(vm)

    // Template compiler entry
    if(vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

  Vue.prototype.$mount = function(el){
    // Get the current instance, or property, inside the constructor's prototype method, and add parameters to the property dynamically.
    const vm = this
    const options = vm.$options

    el = document.querySelector(el)  // Get the real element
    vm.$el = el
    // The user did not pass the render function
    if(! options.render){// The user did not pass the template string
      let template = options.template
      if(! template && el) { template = el.outerHTML }// Get the HTML of the user-passed EL element as template and compile it into the render function
      let render = compileToFunction(template)
      // Put the render function on top of options.
      options.render = render
    }
  }
}
Copy the code

To generate the render function, we first need to get the AST code generated by the template parsing.

2, template compilation core entrance

import { generate } from "./generate";
import { parserHTML } from "./parser";


export function compileToFunction(template) {

  // Make the template an AST
  let ast = parserHTML(template)

  // Code optimization, static node marking

  // Code generation
  let code = generate(ast)

  let render = new Function(`with(this){return ${code}} `);

  console.log(render.toString())
}
Copy the code

3. Convert template to AST

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // Match the aa-xxx of the label name
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `; // aa:aa-xxx
const startTagOpen = new RegExp(` ^ <${qnameCapture}`); // This re can match the first tag name that matches the result (index first) [1]
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `); // Match  at the end of the tag [1]
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /; // Match the attribute

/ / [1] attribute key [3] | | [4] | | [5] the value of the attribute a = 1 a = '1' a = ""
const startTagClose = /^\s*(\/?) >/; // Match tag end /> >
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g; // {{ xxx }}

// Vue3 compiles much better than vue2, with fewer regex's

export function parserHTML(html) {
  // You can continue to intercept templates until all templates are parsed
  let stack = [];
  let root = null;
  // I want to build a father-son relationship
  function createASTElement(tag, attrs, parent = null) {
    return {
      tag,
      type: 1./ / element
      children: [],
      parent,
      attrs
    }
  }
  function start(tag, attrs) { // [div,p]
    // The parent node is the last one in the stack
    let parent = stack[stack.length - 1];
    let element = createASTElement(tag, attrs, parent);
    if (root == null) { // The current node is the root node
      root = element
    }
    if (parent) {
      element.parent = parent; // point to parent with the parent property of new p
      parent.children.push(element);
    }
    stack.push(element)
  }
  function end(tagName) {
    // When the end tag matches, it is removed from the stack
    let endTag = stack.pop();
    if(endTag.tag ! = tagName) {console.log('Wrong label')}}function text(chars) {
    let parent = stack[stack.length - 1];
    chars = chars.replace(/\s/g."");
    if (chars) {
      parent.children.push({
        type: 2.text: chars
      })
    }
  }
  function advance(len) {
    html = html.substring(len);
  }
  function parseStartTag() {
    const start = html.match(startTagOpen);  / / 4.30 to continue
    if (start) {
      const match = {
        tagName: start[1].attrs: []
      }
      advance(start[0].length);
      let end;
      let attr;
      while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) {// 1 must have attribute 2, not the end tag of the beginning 
      
match.attrs.push({ name: attr[1].value: attr[3] || attr[4] || attr[5]}); advance(attr[0].length); } // <div id="app" a=1 b=2 > if (end) { advance(end[0].length); } return match; } return false; } while (html) { // Parse labels and text let index = html.indexOf('<'); if (index == 0) { const startTagMatch = parseStartTag() if (startTagMatch) { // Start tag start(startTagMatch.tagName, startTagMatch.attrs); continue; } let endTagMatch; if (endTagMatch = html.match(endTag)) { // End tag end(endTagMatch[1]); advance(endTagMatch[0].length); continue; }}/ / text if (index > 0) { / / text let chars = html.substring(0, index) //<div></div> text(chars); advance(chars.length) } } return root; } Copy the code

Summary: using regular expressions, match the start and end tags, and use the stack to record to match to the label of the ast structure, when meet start tag to the ast node into the stack, take the last element of the on the top of the stack as the parent node to join the element node, when the match to the end tag, the current label node is removed from the stack. Advance continuously intercepts matching strings until all strings are parsed.

All this code does is convert the template into a JS object

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

The transformation results are as follows.

{
    "tag": "div"."type": 1.// Element node
    "children": [{"type": 2.// Text node
            "text": "{{message}}"}]."parent": null."attrs": [{"name": "id"."value": "app"}}]Copy the code

For now, you can go to the # Vue Template Explorer to get an idea of what the final render function will look like.

Generate render function with AST

const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g; // {{ xxx }}

function genProps(attrs) {
  // {key:value,key:value,}
  let str = ' ';
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    if (attr.name === 'style') { // {name:id,value:'app'}
      let styles = {}
      attr.value.replace(/([^;:]+):([^;:]+)/g.function () {
        styles[arguments[1]] = arguments[2];
      })
      attr.value = styles
    }
    str += `${attr.name}:The ${JSON.stringify(attr.value)}, `
  }
  return ` {${str.slice(0, -1)}} `
}

function gen(el) {
  if (el.type == 1) {
    return generate(el); // If it is an element, it is generated recursively
  } else {
    let text = el.text; / / {{}}
    if(! defaultTagRE.test(text))return `_v('${text}') `; // The description is plain text

    / / that have expression I need to do an expression and average value of splicing [' aaaa '_s (name),' BBB ']. Join (' +)
    // _v('aaaa'+_s(name) + 'bbb')
    let lastIndex = defaultTagRE.lastIndex = 0;
    let tokens = []; // <div> aaa{{bbb}} aaa </div>
    let match;

    // lastIndex is automatically moved backwards every time a match is made
    while (match = defaultTagRE.exec(text)) { // If the re + g is used with exec, there will be a problem with lastIndex
      let index = match.index;
      if (index > lastIndex) {
        tokens.push(JSON.stringify(text.slice(lastIndex, index)));
      }
      tokens.push(`_s(${match[1].trim()}) `);
      lastIndex = index + match[0].length;
    }
    if (lastIndex < text.length) {
      tokens.push(JSON.stringify(text.slice(lastIndex)));
    }
    return `_v(${tokens.join('+')}) `; // webpack source code csS-loader image processing}}function genChildren(el) {
  let children = el.children;
  if (children) {
    return children.map(item= > gen(item)).join(', ')}return false;
}

// _c(div,{},c1,c2,c3,c4)
export function generate(ast) {
  let children = genChildren(ast)
  let code = `_c('${ast.tag}',${ast.attrs.length ? genProps(ast.attrs) : 'undefined'
    }${children ? `,${children}` : ' '
    }) `
  return code;
}
Copy the code

conclusion

In fact, in this video, just remember a few important steps, and as for the internal details, you can have time to look at the corresponding internal details.

  1. Template is converted to ATS.
  2. Ast compiles into the render function.

Links to a series of articles (constantly updated…)

Vue source exploration (a) responsive principle

Vue source exploration (two) template compilation

A preliminary study of Vue source code (3) Single component mount (rendering)

Vue source exploration (four) dependent (object) collection process

Object asynchronous update nextTick()