Front-end Compilation principles

Refer to the super – a tiny – the compiler.

Parse => transform => generate. Parse converts code or template strings into an AST tree, transform processes the AST, and generate generates code

Vue2.6 compilation

Take a look at the source code for vue2.6 to compile the core code

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

Parse: Template optimize into code the AST optimize is a static node that generates the render function, which generates the VNode tree

File directory

To better clarify the code structure, let’s take a look at how VUE gets here. First of all,

// scripts/config.js
'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd'.env: 'development'.alias: { he: './entity-decoder' },
    banner
  },
Copy the code

You can see that the entry is a web/entry-runtime-with-compiler.js file.

// src/platforms/wev/entry-runtime-with-compiler.js
import Vue from './runtime/index'
// Put Vue. Prototype.$mount on the prototype
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)
  const options = this.$options
  if(! options.render) {// Get the template string
    let template = options.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}}else if (el) {
      template = getOuterHTML(el)
    }

    // Get the render function
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}
Copy the code

The mount function above actually ends up executing mountComponent

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code

The above function fetch Render comes from compileToFunctions

// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// createCompiler is the core function originally mentioned above
const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
Copy the code

Parse

Parse generates an AST tree from template

var ast = parse(template.trim(), options);
Copy the code
Get the template string

From the above analysis, you can see that template comes from this

// If template is passed when creating Vue instance,
let template = options.template
if (template) {
    // If template is #templateId, return innerHTML of templateId
    if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
            template = idToTemplate(template)
            /* istanbul ignore if */
            if(process.env.NODE_ENV ! = ='production' && !template) {
            warn(
                `Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
        // If template is dom, return its innerHTML directly
        template = template.innerHTML
    } else {
        if(process.env.NODE_ENV ! = ='production') {
            warn('invalid template option:' + template, this)}return this}}else if (el) {
    // If el is not passed in, the outerHTML of EL is fetched
    template = getOuterHTML(el)
}
Copy the code
parse

Look at the parse method and see that some variables are defined

  1. Stack ==> This stack stores all the nodes with open tabs, which we will later call the outterStack
  2. CurrentParent ==> This refers to the nearest parent element of the currently open tag
  3. Root ==> Finally returned root node
/** * Convert HTML string to AST. */
  function parse (template, options) {
    var stack = [];
    var root;
    var currentParent;
    function closeElement (element) {}function trimEndingWhitespace (el) {
    }

    parseHTML(template, {
      warn: warn$2.expectHTML: options.expectHTML,
      isUnaryTag: options.isUnaryTag,
      canBeLeftOpenTag: options.canBeLeftOpenTag,
      shouldDecodeNewlines: options.shouldDecodeNewlines,
      shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
      shouldKeepComment: options.comments,
      outputSourceRange: options.outputSourceRange,
      start: function start (tag, attrs, unary, start$1, end) {
        // ...
      },
      end: function end (tag, start, end$1) {
        // ...
      },
      chars: function chars (text, start, end) {
        // ...
      },
      comment: function comment (text, start, end) {
        // ...}});return root
  }
Copy the code

From the following code, you can see that the parse function does two main things.

  1. Defines its own store variables and functions,
  2. Perform the parseHTML

If you look at the input parameter to parseHTML, in addition to the options passed in, there are four functions that act as hooks that change the outterStack and currentParent values under certain circumstances.

parseHTML

ParseHTML can be understood as a walk function that iterates through all the Template strings

  1. Different tags are identified
  2. Handle corresponding tag
  3. Advance (n) Advances n characters
  4. Change outer’s variable by calling the corresponding hook function

The following code will be briefly looked at, followed by a step-by-step look at examples

function parseHTML (html, options) {
    var stack = [];
    var expectHTML = options.expectHTML;
    var isUnaryTag$$1 = options.isUnaryTag || no;
    var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
    var index = 0;
    var last, lastTag;
    while (html) {
      last = html;
      if(! lastTag || ! isPlainTextElement(lastTag)) {var textEnd = html.indexOf('<');
        if (textEnd === 0) {
          / /... Other tags

          // End tag:
          var endTagMatch = html.match(endTag);
          if (endTagMatch) {
            var curIndex = index;
            advance(endTagMatch[0].length);
            parseEndTag(endTagMatch[1], curIndex, index);
            continue
          }

          // Start tag:
          var startTagMatch = parseStartTag();
          if (startTagMatch) {
            handleStartTag(startTagMatch);
            continue}}var text = (void 0), rest = (void 0), next = (void 0);
        if (textEnd >= 0) {
          rest = html.slice(textEnd);
          while(! endTag.test(rest) && ! startTagOpen.test(rest) && ! comment.test(rest) && ! conditionalComment.test(rest) ) { next = rest.indexOf('<'.1);
            if (next < 0) { break }
            textEnd += next;
            rest = html.slice(textEnd);
          }
          text = html.substring(0, textEnd);
        }

        if (textEnd < 0) {
          text = html;
        }

        if (text) {
          advance(text.length);
        }

        if(options.chars && text) { options.chars(text, index - text.length, index); }}else {
        var endTagLength = 0;
        var stackedTag = lastTag.toLowerCase();
        var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?) (< / ' + stackedTag + '[^ >] * >)'.'i'));
        var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
          endTagLength = endTag.length;
          if(! isPlainTextElement(stackedTag) && stackedTag ! = ='noscript') {
            text = text
              .replace(/ 
      /g.'$1') / / # 7298
              .replace(/ 
      /g.'$1');
          }
          if (shouldIgnoreFirstNewline(stackedTag, text)) {
            text = text.slice(1);
          }
          if (options.chars) {
            options.chars(text);
          }
          return ' '
        });
        index += html.length - rest$1.length;
        html = rest$1;
        parseEndTag(stackedTag, index - endTagLength, index);
      }

      if (html === last) {
        options.chars && options.chars(html);
        break
      }
    }

    parseEndTag();
  }
Copy the code

Watch parseHTML:

  1. First you’ll see some variables defined in function as well

Stack: Holds an element that is not closed. Since the stack has the same name as the one above, we call the stack innerStack to distinguish it from the other. Index: the index pointer that the current string rotates to

  1. Loop HTML
The example analysis

Take a look at how to generate ast for multi-layer DOM. You can combine the flow chart and source code debug to see.

Assuming that the template is

    <div id="demo"><div id="child">childValue</div></div>
    new Vue({
        el: '#demo',
    })
Copy the code

Because the first string is <, the start tag is included

    // Start tag:
    var startTagMatch = parseStartTag();
    if (startTagMatch) {
        handleStartTag(startTagMatch);
        continue
    }
Copy the code

Here, two methods are executed, parseStartTag:

var startTagOpen = new RegExp(("^" " + qnameCapture));
function parseStartTag () {
    var start = html.match(startTagOpen);
    // start = ["<div","div"]
    if (start) {
        var match = {
            tagName: start[1].attrs: [].start: index
        };
        advance(start[0].length);
        var end, attr;
        // The actual code is written like this, I have done the decomposition, convenient to see
        // while (! (end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {

        // verify that it is not followed by > or />
        end = html.match(startTagClose);
        // Verify that there are no attributes
        attr = html.match(dynamicArgAttribute);
        var isAttrMatch = (attr || html.match(attribute));
        // If it is not followed by > or /> and has attributes,
        // Loop to get attributes and push them into attrs of match
        while(! end && isAttrMatch) { attr.start = index; advance(attr[0].length);
            attr.end = index;
            match.attrs.push(attr);
        }
        // match ====> 
        // "tagName": "div",
        // "attrs": [
        / / /
        // " id=\"demo\"",
        // "id",
        / / "=",
        // "demo",
        // null,
        // null
        / /]
        / /,
        // "start": 0
        // }

        // If it is > or />
        if (end) {
            match.unarySlash = end[1];
            advance(end[0].length);
            match.end = index;
            return match
        }
        // match ====>
        / / {
        // "tagName": "div",
        // "attrs": [
        / / /
        // " id=\"demo\"",
        // "id",
        / / "=",
        // "demo",
        // null,
        // null
        / /]
        / /,
        // "start": 0,
        // "unarySlash": "",
        // "end": 15
        // }}}Copy the code

In the code, you can see that parseStartTag is actually

  1. will<div id="demo">Convert to object, this object contains
{
    tagName,
    attrs // The property array needs to be parsed
    start // Start index
    end // End index
}
Copy the code
  1. And parseStartTag points the index pointer to the end

I get startTagMatch, so what do I do

if (startTagMatch) {
    handleStartTag(startTagMatch);
    continue
}
Copy the code

HandleStartTag:

  1. Processing attrs
  2. The current node is pushed into the innerStack
  3. Execute the hook function start
function handleStartTag (match) {
    var tagName = match.tagName;
    var unarySlash = match.unarySlash;

    var l = match.attrs.length;
    var attrs = new Array(l);
    for (var i = 0; i < l; i++) {
    / / processing attrs
    }
    // attrs ===>
    / / /
    / / {
    // "name": "id",
    // "value": "demo",
    // "start": 5,
    // "end": 14
    / /}
    // ]

    if(! unary) { stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
    // Push to innerStack ==>
    / / /
        / / {
        // "tag": "div",
        // "lowerCasedTag": "div",
        // "attrs": [
        / / {
        // "name": "id",
        // "value": "demo",
        // "start": 5,
        // "end": 14
        / /}
        / /,
        // "start": 0,
        // "end": 15
        // }
    // ]
    lastTag = tagName;
    }
    
    // execute the start hook
    if(options.start) { options.start(tagName, attrs, unary, match.start, match.end); }}Copy the code

And what does the start function do

  1. Create an element with createASTElement
  2. Element processing (instruction parsing, etc.)
  3. Current elment assigned to cuurentParent
  4. Elment pushes to the outerStack
function start (tag, attrs, unary, start$1, end) {

    var element = createASTElement(tag, attrs, currentParent);
    // createASTElement will be based on
    AttrsList generates a map and assigns it to attrsMap
    // 2. Attach currentParent to the parent of the current element based on the currentParent passed in
    // 3. Type type 1 to indicate the label node
    // 4. Generate the format of the final required child node

    // element ==>
    / / {
    // "type": 1,
    // "tag": "div",
    // "attrsList": [
    / / {
    // "name": "id",
    // "value": "demo",
    // "start": 5,
    // "end": 14
    / /}
    / /,
    // "attrsMap": {
    // "id": "demo"
    / /},
    // "rawAttrsMap": {},
    // "children": []
    // }

    {
        if (options.outputSourceRange) {
        element.start = start$1;
        element.end = end;
        element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {
            cumulated[attr.name] = attr;
            returncumulated }, {}); }}// 这里追加start, end, rawAttrsMap
    / / {
    // "id": {
    // "name": "id",
    // "value": "demo",
    // "start": 5,
    // "end": 14
    / /}
    // }

    // apply pre-transforms
    // preTransforms does some parsing and binding of VUE directives
    for (var i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element;
    }

    // structural directives
    // TODO:Generate an execution method for for if once
    processFor(element);
    processIf(element);
    processOnce(element);

    // If there is no root, it is now the first node. Assign elment to root
    if(! root) { root = element; }// Assign the current elment to currentParent;
    currentParent = element;
    // Push current elment onto the stack
    stack.push(element);
}
Copy the code

So, so far,

has been parsed, and we’re ignoring whitespace, and we’re going to parse

, and we’re going to do exactly the same thing, and we’re going to look at the innerStack once we’ve done that

[{"tag": "div"."lowerCasedTag": "div"."attrs": [{"name": "id"."value": "demo"."start": 5."end": 14}]."start": 0."end": 15
  },
  {
    "tag": "div"."lowerCasedTag": "div"."attrs": [{"name": "id"."value": "child"."start": 20."end": 30}]."start": 15."end": 31}]Copy the code

See outerStack again

[{"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
      "id": "demo"
    },
    "rawAttrsMap": {... },"children": []."start": 0."end": 15
  },
  {
    "type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
      "id": "child"
    },
    "rawAttrsMap": {... },"parent": {
      "type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
        "id": "demo"
      },
      "rawAttrsMap": {... },"children": []."start": 0."end": 15
    },
    "children": []."start": 15."end": 31}]Copy the code

CurrentParent points to the child elment.

Then you parse the childValue parseHTML

  1. Index pointer tochildValueAt the end
  2. Execute the chars hook function
    var textEnd = html.indexOf('<');
    if (textEnd >= 0) {
        // ...
        text = html.substring(0, textEnd);
    }
    // text == 'childValue'
    if (text) {
        advance(text.length);
    }
    // Execute the hook function chars
    if (options.chars && text) {
        options.chars(text, index - text.length, index);
    }
Copy the code

Implementation of chars:

  1. Generate a text node
  2. Push textNode into the current CurrentParent.Children
    function chars (text, start, end) {
        if(! currentParent) {return
        }
        var children = currentParent.children;
        // ... 
        if (text) {
          var res;
          var child;
          if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
            child = {
              type: 2.expression: res.expression,
              tokens: res.tokens,
              text: text
            };
          } else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') {
              // type indicates a text node
            child = {
              type: 3.text: text
            };
          }
          if(child) { child.start = start; child.end = end; children.push(child); }}}Copy the code

After completing into execution currentParent. Children. Push (textNode), currentParent this time is:

{
  "type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
    "id": "child"
  },
  "rawAttrsMap": {... },"parent": {
    "type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
      "id": "demo"
    },
    "rawAttrsMap": {... },"children": []."start": 0."end": 15
  },
  // Text is pushed in children
  "children": [{"type": 3."text": "childValue"."start": 31."end": 41}]."start": 15."end": 31
}
Copy the code

  1. Index pointer to</div>At the end
  2. Perform parseEndTag
// End tag:
    var endTagMatch = html.match(endTag);
    // endTagMatch ==>
    / / /
    // "",
    // "div"
    // ]
    if (endTagMatch) {
        var curIndex = index;
        // The index pointer moves to the end
        advance(endTagMatch[0].length);
        parseEndTag(endTagMatch[1], curIndex, index);
        continue
    }
Copy the code

parseEndTag

  1. Find the innerStack element closest to the top of the stack with the same name as the current tag
  2. I’m going to put this element at the top of the stack and I’m going to call the end hook function for all the elements
  3. Push these elements out of the innerStack
function parseEndTag (tagName, start, end) {
      var pos, lowerCasedTagName;
      // Find the innerStack node object pos whose lowerCasedTagName is equal to the innerStack nearest the top
      if (tagName) {
        lowerCasedTagName = tagName.toLowerCase();
        for (pos = stack.length - 1; pos >= 0; pos--) {
          if (stack[pos].lowerCasedTag === lowerCasedTagName) {
            break}}}else {
        pos = 0;
      }

      if (pos >= 0) {
        for (var i = stack.length - 1; i >= pos; i--) {
            // Then execute the end hook function
          if(options.end) { options.end(stack[i].tag, start, end); }}// Push the closed tag out of the innerStack, and the processing is complete
        stack.length = pos;
        lastTag = pos && stack[pos - 1].tag; }}Copy the code

End hook function:

  1. Push element out of outterStack
  2. CurrentParent points to the element’s parent node
  3. Rewrite the derived element. End
  4. Perform closeElement (element)
function end (tag, start, end$1) {
    // Find the top elment in the outerStack
    var element = stack[stack.length - 1];
    // Push this elment out of the stack
    stack.length -= 1;
    // currentParent points to the new top of the stack
    currentParent = stack[stack.length - 1];
    if (options.outputSourceRange) {
        
      
element.end = end$1; } closeElement(element); }, Copy the code

CloseElement function

  1. Instructions for processing vue, etc., mounted on element (ignored)
  2. If you have currentParent currentParent. Children. Push (element)
function closeElement (element) {
      if(! inVPre && ! element.processed) { element = processElement(element, options); }if(currentParent && ! element.forbidden) {if (element.elseif || element.else) {
            Elseif / / processing
            processIfConditions(element, currentParent);
        } else {
            / / handle slot
            if (element.slotScope) {
                var name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; }// Push the current element into the children of currentParent
            currentParent.children.push(element);
            // Assign currentParent to element.parent;element.parent = currentParent; }}}Copy the code

The last also performs the closing procedure described above. And finally root, the first element, is

{
    attrs: [{...}]attrsList: [{...}]attrsMap: {id: "demo"}
    children: [{
        attrs: [{...}]attrsList: [{...}]attrsMap: {id: "child"}
        children: [{
            end: 41
            start: 31
            text: "childValue"
            type: 3
        }]
        end: 47
        parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... }plain: false
        rawAttrsMap: {id: {... }}start: 15
        tag: "div"
        type: 1
    }]
    end: 53
    parent: undefined
    plain: false
    rawAttrsMap: {id: {... }}start: 0
    tag: "div"
    type: 1
}
Copy the code
Vue Parse basic process

Variable bindings

We discussed how the parent node generates the AST. We know that all the information in the template tag will eventually be converted to a key in the AST. What will the final AST of the variable in the VUE look like if there is a DOM

    <div id="demo" :class="calssName"><div v-if="c">{{a + b}}</div><div v-else>{{a-b}}</div><div v-for="item in branches">{{item}}</div></div>

Copy the code

The resulting AST looks like this. You can focus on the parts I pointed out, where _s = toString();

{
    attrs: [{...}]attrsList: [{...}]attrsMap: {id: "demo"To:class:"calssName"}
    children: (3) [
        {
            attrsList: []
            attrsMap: {v-if: "c"}
            children: [{
                end: 57
                / / 👇
                expression: "_s(a + b)"
                start: 48
                text: "{{a + b}}"
                / / 👇
                tokens: [{@binding: "a + b"}]
                type: 2
            }]
            end: 63
            / / 👇
            if: "c"
            / / 👇
            ifConditions: [{
                // <div v-if="c">{{a + b}}</div>
                block: {type: 1.tag: "div".attrsList: Array(0), attrsMap: {... },rawAttrsMap: {... },... }exp: "c"
            }, {
                // <div v-else>{{a-b}}</div>
                block: {type: 1.tag: "div".attrsList: Array(0), attrsMap: {... },rawAttrsMap: {... },... }exp: undefined
            }]
            parent: {... }plain: true
            rawAttrsMap: {v-if: {... }}start: 34
            tag: "div"
            type: 1}, {alias: "item"
            attrsList: []
            attrsMap: {v-for: "item in branches"}
            children: [{
                end: 101
                / / 👇
                expression: "_s(item)"
                start: 93
                text: "{{item}}"
                / / 👇
                tokens: [{@binding: "item"}]
                type: 2
            }]
            end: 107
            / / 👇
            for: "branches"
            parent: {... }plain: true
            rawAttrsMap: {v-for: {... }}start: 63
            tag: "div"
            type: 1}]/ / 👇
    classBinding: "calssName"
    end: 158
    parent: undefined
    plain: false
    rawAttrsMap: {:class: {name: ":class".value: "calssName".start: 15.end: 33}
        id: {name: "id".value: "demo".start: 5.end: 14}}start: 0
    tag: "div"
    type: 1
}
Copy the code