preface

The content of this article is in the previous chapter – data hijacking on the basis of preparation, suggested that the boys go to see. This article mainly refers to vue2.0 source code and Ono Mori, because only around the core principle of the compilation, so easier to understand (not rigorous, please forgive). Finally, the case source code, so that we learn to understand.

The directory structure

The new directories and files are circled in red in the image below

Code parsing

Vue/index. Js entrance

Compared to the previous article on data hijacking, there are mainly two new functions: lifecycleMixin and renderMixin. More on what they do later. Let’s first look at how to compile templates.

import { initMixin } from './init';
import { lifecycleMixin } from './lifecycle';
import { renderMixin } from './vdom/index';

function Vue(options) {
  // When a Vue instance is created with the keyword new, the Vue prototype method _init is called to initialize the data
  this._init(options);
}

// Initialize the related operations, mainly mounting the _init() and $mount() methods on vue.prototype
initMixin(Vue);
// Lifecycle related operations, mainly mounting the _update method on vue. prototype
lifecycleMixin(Vue);
Prototype mounts _render(), _c(), _v() and _s() functions
renderMixin(Vue);

export default Vue;
Copy the code

Vue/init. Js initialization

As in the previous article, we don’t just call initState(VM) to process data during initialization. Mount (vm.mount(vm.mount(vm. mount(vm.options.el)) to mount and compile the template, generating the AST abstract syntax tree and the render function.

import { initState } from './state';
import { compileToFunctions } from './compiler';
import { mountComponent } from './lifecycle';

function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this; // Store this (Vue instance)
    vm.$options = options; // Mount options to the VM for later use

    // Data, props, methods, computed, and watch in Vue instances are all in the initState function
    // Initialize. Since we mainly explain: Vue data hijacking, only data will be processed.
    initState(vm);

    if (vm.$options.el) {
      // Vue. Prototype.$mount --
      vm.$mount(vm.$options.el);
    }
  }

  Vue.prototype.$mount = function (el) {
    const vm = this;
    const options = vm.$options;

    // If the render function is present in the Vue option, the Vue constructor will not function from
    // The template option or the HTML template extracted from the mount element specified by the EL option compiles the rendering function.

    // Render > template > HTML

    // If the render function does not exist, render is generated
    if(! options.render) {let template = options.template; // Get the template

      // el exists and template does not exist
      if(el && ! template) {// Mount el (HTML template) to be used in the _update method of the instance
        vm.$el = document.querySelector(el);
        template = vm.$el.outerHTML;
      }

      // Compile the template, generate the AST abstract syntax tree and render it into the render function
      const render = compileToFunctions(template);
      options.render = render; / / mount render
    }
    
    mountComponent(vm); // Mount the component}}export {
  initMixin
}
Copy the code

Vue/Compiler /index.js compiles the template to generate the AST and render functions

import { parseHtml } from './parser';
import { generate } from './generate';

// compile: template => AST => render
function compileToFunctions(html) {
  // Parse the HTML string into an AST abstract syntax tree
  const ast = parseHtml(html);
  // Convert AST to a string function
  const code = generate(ast);
  // Generate the render function (with statement, key to understanding this code)
  const render = new Function(`with(this){ return ${code}} `);

  return render;
}

export {
  compileToFunctions
}
Copy the code

Vue/Compiler /parser.js generates the AST abstract syntax tree

// Match attributes: id="app", id='app', or id=app
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /;

      
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;

      
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `; // Match the start tag: const startTagOpen = new RegExp(` ^ <${qnameCapture}`); // Match closed tags: > or /> const startTagClose = /^\s*(\/?) >/; // Match the end tag: const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `);
function string, {{tip}} < span class = "cla" > {{studentNum}} < / span > < / div > * /
// Parse template string to generate AST syntax tree function parseHtml(html) { const stack = []; // Initial AST objects for all start tags let root; // The AST object that is finally returned let text; / / plain text let currentParent; // The parent of the current element // Vue2.0 source code for the following several cases are handled respectively: comment tag, conditional comment tag, Doctype, // Start label, end label. Each time a case is processed, it blocks the code from continuing and opens a new one //, and resets the HTML string, that is, removing the match // to the HTML string, leaving the unmatched for processing in the next loop. // Tip: when interpreting the source code in the above cases, it will be easier to understand with the template samples. while (html) { // textEnd is 0, indicating a start tag. let textEnd = html.indexOf('<'); if (textEnd === 0) { // Parse the start tag and its attributes and store them in an object, for example: // { tagName: 'div', attrs: [{ name: 'id', value: 'app' }] } const startTagMatch = parseStartTag(); // console.log(' parse -- start label -- result ', startTagMatch); // Process the start tag if (startTagMatch) { start(startTagMatch.tagName, startTagMatch.attrs); continue; // Executing to continue will start a new loop, with no subsequent code execution } const endTagMatch = html.match(endTag); // Matches the end tag // Process the closing tag if (endTagMatch) { advance(endTagMatch[0].length); end(endTagMatch[1]); continue; }}// Intercepts the text in the HTML template string if (textEnd > 0) { text = html.substring(0, textEnd); } // Process text content if(text) { advance(text.length); chars(text); }}// Parse the start tag and its attributes, e.g.
function parseStartTag() { Match () returns null if no matching text is found. Otherwise, it returns an array, // It contains information about the matching text it finds. const start = html.match(startTagOpen); // Match the start tag let end, attr; if (start) { // Store the start tag name and attributes const match = { tagName: start[1].// The name of the start tag, for example, div attrs: [] // Start tag attributes, e.g. {name: 'id', value: 'app'} } // Delete matched HTML strings and leave unmatched ones. // For example:
Id ="app">< div> advance(start[0].length); // Enter the loop when the attribute (id='app') is matched, but the closing of the opening tag (>) is not matched while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) { match.attrs.push({name: attr[1].// Attribute name: id // If you provide the template option when creating a vue instance with the new keyword // In its string, some tag attributes are in single quotation marks or without quotation marks, // For example:
or
// The value of the attribute in the array that it returns may be at subscript 4 or 5 of the array value: attr[3] || attr[4] || attr[5] // Attribute value: app }); advance(attr[0].length); } // If the match matches the closing of the start tag (such as: >), the match object is returned if (end) { advance(end[0].length); returnmatch; }}}// Intercepts the HTML string to delete the matched character from the original character. function advance(n) { The substring() method is used to extract the character between two specified subscripts in a string. html = html.substring(n); } function start(tag, attrs) { // Create an AST object const element = createASTElement(tag, attrs); // If the root node does not exist, the current node is the top-level node of the template, which is the first node if(! root) { root = element; }// Save the current parent node (AST object) currentParent = element; // Push the AST object onto the stack, and when the corresponding end tag is parsed, // Pop the AST object corresponding to this label off the stack. // Cause: Parsing the start tag is clockwise; When parsing the closing tag, it is counterclockwise. With the template example,
=> =>... => =>
// Therefore, after the AST object generated by parsing the start tag is pushed on the stack, it should be resolved to its corresponding end tag // Pop is used. The entire operation process is easier to understand when viewed together with the start() and end() methods. stack.push(element); } function end(tag) { // The pop() method removes the last element of the array, reduces the array length by one, and returns the value of the element it removed. // If the array is empty, pop() does not change the array and returns undefined. const element = stack.pop(); // Get the AST object of the current element label currentParent = stack[stack.length - 1]; // Gets the parent AST object of the current element label if (currentParent) { // mark the parent and child elements element.parent = currentParent; // The child element stores the parent element currentParent.children.push(element); // The parent element stores the child element}}function chars(text) { text = text.trim(); // Remove the leading and trailing Spaces // If the text exists, put it directly into the children of the parent if(text && text ! = =' ') { const element = { type: 3.// nodeType of text element: 3text }; currentParent.children.push(element); }}return root; } // Generate an AST object function createASTElement(tagName, attrs) { return { tag: tagName, / / tag name type: 1.// nodeType of the tag element: 1 children: [].// Label sublevel attrs, // Tag attributes parent // Label parent}}export { parseHtml } Copy the code

Vue/Compiler /generate.js generates string functions

The variable code returned by generate() is a string function that is key to generating the render function.

_v() => createTextNode() creates a text node. _s(value) => _s(tip) parses double curly braces, for example: {{tip}} AST => string function generate() {return '_c("div",{id: "App" style: {" color ":" # f66 ", "the font - size" : "20 px"}}, _v (string, "" + _s (tip), _c (" span" {" class ":" cla ", "style" : {" color ": "green" } }, _v(_s(studentNum))))`; } * /

const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g; // Match double braces => {{tip}}

// Generate a function string
function generate(el) {
  const children = genChildren(el);
  const code = `_c('${el.tag}', ${el.attrs.length > 0 ? `${jointAttrs(el.attrs)}` : 'undefined'}${children ? `,${children}` : ' '}) `;

  return code;
}

/ / the property joining together into a string, for example: ` style: {" color ":" # f66 ", "the font - size" : "20 px"} `
function jointAttrs(attrs) {
  let str = ' ';

  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    // Handle the style attribute
    if (attr.name === 'style') {
      let attrValue = {};

      attr.value.split('; ').map((itemArr) = > {
        let [key, value] = itemArr.split(':');
        if(key) { attrValue[key] = value; }}); attr.value = attrValue; }// Concatenate attributes (note: don't forget the comma)
    str += `${attr.name}:The ${JSON.stringify(attr.value)}, `
  }

  // str.slice(0, -1) removes the last comma of the string
  return ` {${str.slice(0, -1)}} `;
}

// Generate child nodes
function genChildren(el) {
  const children = el.children;
  // Whether there are child nodes
  if (children.length) {
    return children.map(c= > genNode(c)).join(', '); }}// Process accordingly according to the node type
function genNode(node) {
  if (node.type === 1) { // Element node

    return generate(node);
  } else if (node.type === 3) { // Text node

    let text = node.text;

    if (defaultTagRE.test(text)) { // Handle double braces
      let match,
      index,
      textArray = [],
      // lastIndex Where the next match starts. Each time through the loop, it starts at zero, in case you're dealing with other text,
      // Fetching lastIndex is the value retained after the end of the previous loop, causing an error.
      lastIndex = defaultTagRE.lastIndex = 0;

      {{tip}} haha 

      // Handle double braces and plain text before them: function string, {{tip}}
      while (match = defaultTagRE.exec(text)) {

        index = match.index; // The subscript position of the double brace

        if (index > lastIndex) { // Intercepts plain text before the double braces
          textArray.push(JSON.stringify(text.slice(lastIndex, index)));
        }

        textArray.push(`_s(${match[1].trim()}) `); // Double braces
        lastIndex = index + match[0].length; // marks where the next match starts
      }

      // Handle stored text after double braces: haha
      if (lastIndex < text.length) {
        textArray.push(JSON.stringify(text.slice(lastIndex)));
      }

      return `_v(${textArray.join('+')}) `; // Concatenate entire lines of text

    } else { // Handle plain text

      return `_v(The ${JSON.stringify(text)}) `; }}}export {
  generate
}
Copy the code

Vue /lifecycle. Js updates the component

Remember? In the vue/index.js entry file, we implement the lifecycleMixin(vue) function, which does much more than that in the vue source code: it updates the component, which is our template. This is why we can update components by executing the mountComponent() function in vue/init.js.

import { patch } from './vdom/patch';

function mountComponent(vm) {
  // vm._render() returns the virtual node vnode
  vm._update(vm._render()); // Update the component
}

function lifecycleMixin(Vue) {
  // Mount the _update() function
  Vue.prototype._update = function (vnode) {
    const vm = this;
    patch(vm.$el, vnode); // Generate the corresponding HTML element for the vNode virtual node}}export {
  lifecycleMixin,
  mountComponent
}
Copy the code

Vue /vdom/index.js mounts the _render(), _c(), _v(), and _s() functions

The renderMixin() function called in the vue/index.js entry file is exported from the current file module. Its purpose is indicated in the code comments below.

import { createElement, createTextNode } from './vnode';

function renderMixin(Vue) {
  // Create a virtual element node object
  Vue.prototype._c = function () {
    returncreateElement(... arguments); }// Create a virtual text node object
  Vue.prototype._v = function (text) {
    return createTextNode(text);
  }

   // Handle double braces, for example: {{tip}}
   Vue.prototype._s = function (value) {
    if (value === null) return;
    return typeof value === 'object' ? JSON.stringify(value) : value;
  }

  // Call vm.$options.render to generate a virtual node
  Vue.prototype._render = function () {
    const vm = this;
    const vnode = vm.$options.render.call(vm); // Generate a virtual node object and return

    returnvnode; }}export {
  renderMixin
}
Copy the code

Vue /vdom/vnode.js generates virtual node objects

The createElement() and createTextNode() functions createElement and text virtual nodes, respectively.

/ / element vnode
function createElement (tag, attrs = {}, ... children) {
  return vnode(tag, attrs, children);
}
/ / text vnode
function createTextNode (text) {
  return vnode(undefined.undefined.undefined, text);
}

// VNode object
function vnode (tag, props, children, text) {
  return {
    tag,
    props,
    children,
    text
  }
}

export {
  createElement,
  createTextNode
}
Copy the code

Vue/vDOM /patch.js converts virtual nodes into HTML elements

/** * Example: *  * 
      
* * */
/** * Generate the corresponding HTML element * for the vNode virtual node@param { HTMLDivElement } template => html * @param { Object } VNode => Virtual node object */ function patch(template, vNode) { const el = createElement(vNode); // template.parentNode => body const parentElement = template.parentNode; // Insert the newly generated element into the body. After template, before script tag. parentElement.insertBefore(el, template.nextSibling); parentElement.removeChild(template); // Remove the original node } // Create a node (for simplicity, it's not logically rigorous, but it runs!) function createElement(vnode) { const { tag, props, children, text } = vnode; if (typeof tag === 'string') { vnode.el = document.createElement(tag); // Create the element updateProps(vnode); // Set attributes for the element children.map((child) = > { // Add a child element to the parent elementvnode.el.appendChild(createElement(child)); })}else { // Create a plain text node vnode.el = document.createTextNode(text); } return vnode.el; } // Set attributes for the element, dealing mainly with style and class function updateProps(vnode) { const el = vnode.el; const nodeAttrs = vnode.props || {}; for (let key in nodeAttrs) { if (key === 'style') { // Set the style attribute for (let sKey innodeAttrs.style) { el.style[sKey] = nodeAttrs.style[sKey]; }}else if (key === 'class') { // Set the class attribute el.className = el.class; } else { // Set custom attributes, no special handlingel.setAttribute(key, nodeAttrs[key]); }}}export { patch } Copy the code

conclusion

Article can not understand the place, do not always stare tilt head to think, you can download the case source code, while watching while debugging. Ok, quick to master the secret to tell you, come on, SAO year!