preface
Next will formally enter the handwriting Vue2 series. Instead of starting from scratch, the upgrade will be directly based on lyn-Vue, so if you haven’t read the written Vue series vu1.x, please start with this article and work through it in sequence.
The problem with Vue1 is that there are too many Watcher’s in large applications. If you don’t know how this works, check out the Handwritten Vue series vu1.x.
Therefore, VNode and DIff algorithms are introduced in Vue2 to solve this problem. By reducing the granularity of Watcher to one component for each Watcher (rendering Watcher), you don’t have the performance degradation problem of too many large page Watcher.
In Vue1, Watcher corresponds to responsive data in the page one by one. When responsive data changes, Dep informs Watcher to complete the corresponding DOM update. But in Vue2, each component corresponds to a Watcher. When the responsive data changes, Watcher does not know where the responsive data is in the component. How can the update be done?
As you probably know from reading the previous source code series, Vue2 introduces the VNode and DIff algorithms to compile components into VNodes. Each time the responsive data changes, a new VNode is generated, and the diff algorithm is used to compare the old and new VNodes to find the changes. Then perform the corresponding DOM operations to complete the update.
So, as you can see, Vue1 and Vue2 don’t really have any changes in the core data responsiveness. The main changes are in the compiler.
The target
Complete a simplified implementation of the Vue2 compiler, starting with string template parsing and ending with the render function.
The compiler
When writing Vue1, the compiler uses the DOM API to iterate over the TEMPLATE’s DOM structure. In Vue2, this method is no longer used. Instead, it directly compiles the template string of the component, generates the AST, and then generates the rendering function from the AST.
First, back up the COMPILER directory for Vue1, and then create a compiler directory as the compiler directory for Vue2
mv compiler compiler-vue1 && mkdir compiler
Copy the code
mount
/src/compiler/index.js
/** * compiler */
export default function mount(vm) {
if(! vm.$options.render) {// If no render option is provided, the render function is compiled
// Get the template
let template = ' '
if (vm.$options.template) {
// The template exists
template = vm.$options.template
} else if (vm.$options.el) {
// Mount point exists
template = document.querySelector(vm.$options.el).outerHTML
// Record mount points on the instance, as used in this._update
vm.$el = document.querySelector(vm.$options.el)
}
// Generate the render function
const render = compileToFunction(template)
// Mount the render function to $options
vm.$options.render = render
}
}
Copy the code
compileToFunction
/src/compiler/compileToFunction.js
/** * parse the template string to get the AST syntax tree@param { String } Template template string *@returns Render function */
export default function compileToFunction(template) {
// Parse the template to generate an AST
const ast = parse(template)
// The ast generates the rendering function
const render = generate(ast)
return render
}
Copy the code
parse
/src/compiler/parse.js
/** * parse the template string to generate the AST syntax tree *@param {*} Template template string *@returns {AST} Root ast Syntax tree */
export default function parse(template) {
// Store all unpaired start tag AST objects
const stack = []
// Final AST syntax tree
let root = null
let html = template
while (html.trim()) {
// Filter comment tags
if (html.indexOf('<! -- ') = = =0) {
// The start position is a comment tag, which is ignored
html = html.slice(html.indexOf('-->') + 3)
continue
}
// Match the start tag
const startIdx = html.indexOf('<')
if (startIdx === 0) {
if (html.indexOf('< /') = = =0) {
// Indicates a closed label
parseEnd()
} else {
// Process the start tag
parseStartTag()
}
} else if (startIdx > 0) {
Find the start position of the next tag in the HTML
const nextStartIdx = html.indexOf('<')
// If the stack is empty, the text does not belong to any element and is discarded
if (stack.length) {
// Process the text and place it in the belly of the top element of the stack
processChars(html.slice(0, nextStartIdx))
}
html = html.slice(nextStartIdx)
} else {
// The start tag is not matched, and the entire HTML is just a text}}return root
// Declaration of the parseStartTag function
// ...
// processElement declaration
}
// processVModel function declaration
// ...
// processVOn function declaration
Copy the code
parseStartTag
/src/compiler/parse.js
...
*/
function parseStartTag() {
// Find the end of the start tag >
const end = html.indexOf('>')
Div id="app"; div id="app";
const content = html.slice(1, end)
// Truncate the HTML to remove the content parsed above from the HTML string
html = html.slice(end + 1)
// Find the first space position
const firstSpaceIdx = content.indexOf(' ')
// Tag name and attribute string
let tagName = ' ', attrsStr = ' '
if (firstSpaceIdx === -1) {
content = h3
tagName = content
// No attributes
attrsStr = ' '
} else {
tagName = content.slice(0, firstSpaceIdx)
// The rest of the content will be attributes, such as id="app" xx=xx
attrsStr = content.slice(firstSpaceIdx + 1)}[id="app", xx=xx]
const attrs = attrsStr ? attrsStr.split(' ') : []
// Further parse the property array to get a Map object
const attrMap = parseAttrs(attrs)
// Generate an AST object
const elementAst = generateAST(tagName, attrMap)
// If the root node does not exist, the current node is the first node in the template
if(! root) { root = elementAst }// Push the AST object onto the stack and pop the ast object at the top of the stack when the end tag is encountered
stack.push(elementAst)
// Call the end method to truncate the closed tag
if (isUnaryTag(tagName)) {
processElement()
}
}
Copy the code
parseEnd
/src/compiler/parse.js
/** * handle closing tags, such as
...
*/
function parseEnd() {
// Truncate the closing tag from the HTML string
html = html.slice(html.indexOf('>') + 1)
// Handle the top of the stack element
processElement()
}
Copy the code
parseAttrs
/src/compiler/parse.js
/** * parses the attributes array to get a Map of attributes and values *@param {*} Attrs array, [id="app", xx="xx"] */
function parseAttrs(attrs) {
const attrMap = {}
for (let i = 0, len = attrs.length; i < len; i++) {
const attr = attrs[i]
const [attrName, attrValue] = attr.split('=')
attrMap[attrName] = attrValue.replace(/"/g.' ')}return attrMap
}
Copy the code
generateAST
/src/compiler/parse.js
/** * Generates the AST object *@param {*} TagName Label name *@param {*} The attrMap tag consists of the attribute map object */
function generateAST(tagName, attrMap) {
return {
// Element node
type: 1./ / label
tag: tagName,
// The original attribute map object, which needs further processing later
rawAttr: attrMap,
/ / child nodes
children: [].}}Copy the code
processChars
/src/compiler/parse.js
/** * process text *@param {string} text
*/
function processChars(text) {
// Remove null characters or newlines
if(! text.trim())return
// Construct the AST object of the text node
const textAst = {
type: 3,
text,
}
if (text.match(/ / {{(. *)}})) {
// The description is an expression
textAst.expression = RegExp.$1.trim()
}
// Put the AST in the stomach of the top element
stack[stack.length - 1].children.push(textAst)
}
Copy the code
processElement
/src/compiler/parse.js
/** * This method is called when the element's closing tag is processed * to further process the individual attributes on the element, placing the result on the attr attribute */
function processElement() {
// Pops the top element of the stack to further process that element
const curEle = stack.pop()
const stackLen = stack.length
// Further process the rawAttr in the AST object {attrName: attrValue,... }
const { tag, rawAttr } = curEle
// Put the results on the attr object and delete the corresponding attributes in the rawAttr object
curEle.attr = {}
// An array of keys for the property object
const propertyArr = Object.keys(rawAttr)
if (propertyArr.includes('v-model')) {
// Process the V-model directive
processVModel(curEle)
} else if (propertyArr.find(item= > item.match(/^v-bind:(.*)/))) {
processVBind(curEle, RegExp.$1, rawAttr[`v-bind:The ${RegExp. $1}`])}else if (propertyArr.find(item= > item.match(/^v-on:(.*)/))) {
processVOn(curEle, RegExp.$1, rawAttr[`v-on:The ${RegExp. $1}`])}// After the node is processed, make it relate to its parent node
if (stackLen) {
stack[stackLen - 1].children.push(curEle)
curEle.parent = stack[stackLen - 1]}}Copy the code
processVModel
/src/compiler/parse.js
/** * Process the V-model instruction and place the result directly on the curEle object *@param {*} curEle
*/
function processVModel(curEle) {
const { tag, rawAttr, attr } = curEle
const { type, 'v-model': vModelVal } = rawAttr
if (tag === 'input') {
if (/text/.test(type)) {
// <input type="text" v-model="inputVal" />
attr.vModel = { tag, type: 'text'.value: vModelVal }
} else if (/checkbox/.test(type)) {
// <input type="checkbox" v-model="isChecked" />
attr.vModel = { tag, type: 'checkbox'.value: vModelVal }
}
} else if (tag === 'textarea') {
// <textarea v-model="test" />
attr.vModel = { tag, value: vModelVal }
} else if (tag === 'select') {
//
attr.vModel = { tag, value: vModelVal }
}
}
Copy the code
processVBind
/src/compiler/parse.js
/** * Handle the V-bind directive *@param {*} The AST object * that curEle is currently processing@param {*} BindKey v-bind:key * in key@param {*} BindVal v-bind:key = val */ in val
function processVBind(curEle, bindKey, bindVal) {
curEle.attr.vBind = { [bindKey]: bindVal }
}
Copy the code
processVOn
/src/compiler/parse.js
/** * Handle the V-ON instruction *@param {*} CurEle The AST object currently being processed *@param {*} VOnKey V-ON :key * in key@param {*} VOnVal v-on:key= val */ in "val"
function processVOn(curEle, vOnKey, vOnVal) {
curEle.attr.vOn = { [vOnKey]: vOnVal }
}
Copy the code
isUnaryTag
/src/utils.js
/** * Indicates whether it is a self-closing label. Some self-closing labels are built in for simple processing
export function isUnaryTag(tagName) {
const unaryTag = ['input']
return unaryTag.includes(tagName)
}
Copy the code
generate
/src/compiler/generate.js
/** * Generate render function * from ast@param {*} Ast Ast Syntax tree *@returns Render function */
export default function generate(ast) {
// Render the function as a string
const renderStr = genElement(ast)
// Make a string Function executable with new Function and extend the scope chain for rendering functions with with
return new Function(`with(this) { return ${renderStr}} `)}Copy the code
genElement
/src/compiler/generate.js
/** * Parse ast to generate render function *@param {*} Ast Syntax tree *@returns {string} Render function in string form */
function genElement(ast) {
const { tag, rawAttr, attr } = ast
// Generate property Map object, static property + dynamic property
constattrs = { ... rawAttr, ... attr }// Process the child nodes to get an array of all the child node rendering functions
const children = genChildren(ast)
// Generate an executable method for VNode
return `_c('${tag}', The ${JSON.stringify(attrs)}[${children}]) `
}
Copy the code
genChildren
/src/compiler/generate.js
/** * Processes the children of the AST node, turning the children into rendering functions *@param {*} Ast object * of the AST node@returns [childNodeRender1, ....] * /
function genChildren(ast) {
const ret = [], { children } = ast
// Traverses all child nodes
for (let i = 0, len = children.length; i < len; i++) {
const child = children[i]
if (child.type === 3) {
// Text node
ret.push(`_v(The ${JSON.stringify(child)}) `)}else if (child.type === 1) {
// Element node
ret.push(genElement(child))
}
}
return ret
}
Copy the code
The results of
Add console.log(vm.$options.render) to the mount method, open the console, refresh the page, and see the following
The formal mount phase will then proceed to complete the initial rendering of the page.
Focus on
Welcome everyone to pay attention to my nuggets account and B station, if the content to help you, welcome everyone to like, collect + attention
link
-
Proficient in Vue stack source code principles
-
Form a complete set of video
-
Learning exchange group