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!