When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.
The new video and articles will be sent to WeChat official account for the first time. Li Yongning
The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.
Special instructions
Due to the limited length of the article, the interpretation of Vue source code (8) - the analysis of compiler is divided into two articles Interpretation of Vue source code (8) -- Analysis of compiler (I) A supplement to, so please open it at the same time when reading Interpretation of Vue source code (8) -- Analysis of compiler (I) Read together.
processAttrs
/src/compiler/parser/index.js
/** * Process all attributes on the element: * v-bind The command becomes: El Attrs or EL dynamicAttrs = [{ name, value, start, end, dynamic }, ...], * Or the attribute of props must be used, which becomes el props = [{ name, value, start, end, dynamic }, ...] * v-on The command becomes: El Events or EL nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] } * Other instructions: El directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...] * Native attribute: El Attrs = [{name, value, start, end}], or some attributes that must use props, become: * el.props = [{ name, value: true, start, end, dynamic }] */ function processAttrs(el) { // list = [{ name, value, start, end }, ...] const list = el.attrsList let i, l, name, rawName, value, modifiers, syncGen, isDynamic for (i = 0, l = list.length; i < l; i++) { // Attribute name name = rawName = list[i].name // Attribute value value = list[i].value if (dirRE.test(name)) { // Indicates that the attribute is an instruction // There are instructions on the element, marking the element as a dynamic element // mark element as dynamic el.hasBindings = true // Modifiers: resolve modifiers on attribute names, such as XX lazy modifiers = parseModifiers(name.replace(dirRE, '')) // support .foo shorthand syntax for the .prop modifier if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) { // For props modifier support foo shorthand (modifiers || (modifiers = {})).prop = true name = `.` + name.slice(1).replace(modifierRE, '') } else if (modifiers) { // Remove the modifier in the attribute to get a clean attribute name name = name.replace(modifierRE, '') } if (bindRE.test(name)) { // v-bind, <div :id="test"></div> // Handle the attributes of the v-bind instruction, and finally get el Attrs or EL dynamicAttrs = [{ name, value, start, end, dynamic }, ...] // Attribute name, such as id name = name.replace(bindRE, '') // Attribute value, such as: test value = parseFilters(value) // Whether it is a dynamic attribute < div: [ID] = "test" > < / div > isDynamic = dynamicArgRE.test(name) if (isDynamic) { // If it is a dynamic attribute, remove the square brackets [] on both sides of the attribute name = name.slice(1, -1) } // Tip: the dynamic attribute value cannot be an empty string if ( process.env.NODE_ENV !== 'production' && value.trim().length === 0 ) { warn( `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"` ) } // Modifier present if (modifiers) { if (modifiers.prop && !isDynamic) { name = camelize(name) if (name === 'innerHtml') name = 'innerHTML' } if (modifiers.camel && !isDynamic) { name = camelize(name) } // Handle sync modifier if (modifiers.sync) { syncGen = genAssignmentCode(value, `$event`) if (!isDynamic) { addHandler( el, `update:${camelize(name)}`, syncGen, null, false, warn, list[i] ) if (hyphenate(name) !== camelize(name)) { addHandler( el, `update:${hyphenate(name)}`, syncGen, null, false, warn, list[i] ) } } else { // handler w/ dynamic event name addHandler( el, `"update:"+(${name})`, syncGen, null, false, warn, list[i], true // dynamic ) } } } if ((modifiers && modifiers.prop) || ( !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) { // Add attribute object to El Props array indicates that these properties must be set through props // el.props = [{ name, value, start, end, dynamic }, ...] addProp(el, name, value, list[i], isDynamic) } else { // Add attribute to El Attrs array or EL Dynamicattrs array addAttr(el, name, value, list[i], isDynamic) } } else if (onRE.test(name)) { // Event handling, < - div >, < div > div // Property name, i.e. event name name = name.replace(onRE, '') // Is it a dynamic attribute isDynamic = dynamicArgRE.test(name) if (isDynamic) { // Dynamic attribute, get the attribute name in [] name = name.slice(1, -1) } // Handle the event attribute and add the information of the attribute to El Events or EL On the nativeevents object, format: // el.events = [{ value, start, end, modifiers, dynamic }, ...] addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic) } else { // normal directives // Get el directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...] name = name.replace(dirRE, '') // parse arg const argMatch = name.match(argRE) let arg = argMatch && argMatch[1] isDynamic = false if (arg) { name = name.slice(0, -(arg.length + 1)) if (dynamicArgRE.test(arg)) { arg = arg.slice(1, -1) isDynamic = true } } addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]) if (process.env.NODE_ENV !== 'production' && name === 'model') { checkForAliasModel(el, value) } } } else { // The current property is not an instruction // literal attribute if (process.env.NODE_ENV !== 'production') { const res = parseText(value, delimiters) if (res) { warn( `${name}="${value}": ` + 'Interpolation inside attributes has been removed. ' + 'Use v-bind or the colon shorthand instead. For example, ' + 'instead of <div id="{{ val }}">, use <div :id="val">.', list[i] ) } } // Put the attribute object in El In attrs array, El attrs = [{ name, value, start, end }] addAttr(el, name, JSON.stringify(value), list[i]) // #6887 firefox doesn't update muted state if set via attribute // even immediately after element creation if (!el.component && name === 'muted' && platformMustUseProp(el.tag, el.attrsMap.type, name)) { addProp(el, name, 'true', list[i]) } } } }
addHandler
/src/compiler/helpers.js
/** * Process event attributes and add event attributes to El Events object or EL In the nativeevents object, format: * el.events[name] = [{ value, start, end, modifiers, dynamic }, ...] * A lot of space is spent dealing with the case where the name attribute has a modifier * @param {*} el ast object * @param {*} name Property name, i.e. event name * @param {*} value Property value, that is, the event callback function name * @param {*} modifiers Modifier * @param {*} important * @param {*} warn journal * @param {*} range * @param {*} dynamic Is the property name a dynamic property */ export function addHandler ( el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: ?Function, range?: Range, dynamic?: boolean ) { // modifiers is an object. If the passed parameter is empty, it will be given to a frozen empty object modifiers = modifiers || emptyObject // Tip: the prevent and passive modifiers cannot be used together // warn prevent and passive modifier /* istanbul ignore if */ if ( process.env.NODE_ENV !== 'production' && warn && modifiers.prevent && modifiers.passive ) { warn( 'passive and prevent can\'t be used together. ' + 'Passive handler can\'t prevent default event.', range ) } // Standardized click Right and click Middle, they are not actually triggered. Technically, they are them // It is browser specific, but at least at the current location, only the browser has right-click and middle click // normalize click.right and click.middle since they don't actually fire // this is technically browser-specific, but at least for now browsers are // the only target envs that have right/middle clicks. if (modifiers.right) { // Right click if (dynamic) { // Dynamic properties name = `(${name})==='click'?'contextmenu':(${name})` } else if (name === 'click') { // Non dynamic attribute, name = contextmenu name = 'contextmenu' // Delete the right attribute in the modifier delete modifiers.right } } else if (modifiers.middle) { // Intermediate key if (dynamic) { // Dynamic attribute, name = > mouseup or ${name} name = `(${name})==='click'?'mouseup':(${name})` } else if (name === 'click') { // Non dynamic attribute, mouseup name = 'mouseup' } } /** * Handle the three modifiers capture, once and passive, and mark these modifiers by adding different tags to name */ // check capture modifier if (modifiers.capture) { delete modifiers.capture // Add to the attribute with the capture modifier! sign name = prependModifierMarker('!', name, dynamic) } if (modifiers.once) { delete modifiers.once // The once modifier is marked with ~ name = prependModifierMarker('~', name, dynamic) } /* istanbul ignore if */ if (modifiers.passive) { delete modifiers.passive // passive modifier with & Mark name = prependModifierMarker('&', name, dynamic) } let events if (modifiers.native) { // The native modifier listens to the native event of the component root element and stores the event information in El In the nativeevents object delete modifiers.native events = el.nativeEvents || (el.nativeEvents = {}) } else { events = el.events || (el.events = {}) } const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range) if (modifiers !== emptyObject) { // It indicates that there is a modifier. Put the modifier object on the newHandler object // { value, dynamic, start, end, modifiers } newHandler.modifiers = modifiers } // Put the configuration object into events [name] = [newhandler, handler,...] const handlers = events[name] /* istanbul ignore if */ if (Array.isArray(handlers)) { important ? handlers.unshift(newHandler) : handlers.push(newHandler) } else if (handlers) { events[name] = important ? [newHandler, handlers] : [handlers, newHandler] } else { events[name] = newHandler } el.plain = false }
addIfCondition
/src/compiler/parser/index.js
/** * Put the passed in condition object into el In ifconditions array */ export function addIfCondition(el: ASTElement, condition: ASTIfCondition) { if (!el.ifConditions) { el.ifConditions = [] } el.ifConditions.push(condition) }
processPre
/src/compiler/parser/index.js
/** * If there is a v-pre instruction on the element, set el pre = true */ function processPre(el) { if (getAndRemoveAttr(el, 'v-pre') != null) { el.pre = true } }
processRawAttrs
/src/compiler/parser/index.js
/** * Set el Attrs array object. Each element is an attribute object {name: attrName, value: attrVal, start, end} */ function processRawAttrs(el) { const list = el.attrsList const len = list.length if (len) { const attrs: Array<ASTAttr> = el.attrs = new Array(len) for (let i = 0; i < len; i++) { attrs[i] = { name: list[i].name, value: JSON.stringify(list[i].value) } if (list[i].start != null) { attrs[i].start = list[i].start attrs[i].end = list[i].end } } } else if (!el.pre) { // non root node in pre blocks with no attributes el.plain = true } }
processIf
/src/compiler/parser/index.js
/** * Handle v-if, v-else-if, v-else * Get el if = "exp",el.elseif = exp, el.else = true * v-if Attribute will be added in El Add {exp, block} object to ifconditions array */ function processIf(el) { // Get the value of the v-if attribute, such as < div v-if = "test" > < / div > const exp = getAndRemoveAttr(el, 'v-if') if (exp) { // el.if = "test" el.if = exp // In El Add {exp, block} to ifconditions array addIfCondition(el, { exp: exp, block: el }) } else { // Deal with v-else and get el else = true if (getAndRemoveAttr(el, 'v-else') != null) { el.else = true } // If-el-se, get elseif = exp const elseif = getAndRemoveAttr(el, 'v-else-if') if (elseif) { el.elseif = elseif } } }
processOnce
/src/compiler/parser/index.js
/** * Process the v-once instruction to get el once = true * @param {*} el */ function processOnce(el) { const once = getAndRemoveAttr(el, 'v-once') if (once != null) { el.once = true } }
checkRootConstraints
/src/compiler/parser/index.js
/** * Check the root element: * You cannot use slot and template tags as the root element of a component * You cannot use the v-for directive on the root element of a stateful component because it renders multiple elements * @param {*} el */ function checkRootConstraints(el) { // You cannot use slot and template tags as the root element of a component if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.', { start: el.start } ) } // You cannot use v-for on the root element of a stateful component because it renders multiple elements if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.', el.rawAttrsMap['v-for'] ) } }
closeElement
/src/compiler/parser/index.js
/** * Three main things have been done: * 1,If the element has not been processed, that is, El If processed is false, the processElement method is called to process many attributes on the node * 2,Let yourself have a relationship with the parent element, put yourself in the children array of the parent element, and set your parent attribute to currentParent * 3,Set your own child elements and put all your non slot child elements into your own children array */ function closeElement(element) { // Remove the space at the end of the node, except for the elements in the current pre tag trimEndingWhitespace(element) // The current element is no longer in the pre node and has not been processed if (!inVPre && !element.processed) { // Handle the key, ref, slot, closed slot tag, dynamic component, class, style, v-bind, v-on, other instructions and some native attributes of the element node respectively element = processElement(element, options) } // Handle the case where there are v-if, v-else-if and v-else instructions on the root node // If the root node has a v-if instruction, you must also provide a node of the same level with v-else-if or v-else to prevent the root element from not existing // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { if (process.env.NODE_ENV !== 'production') { // Check root element checkRootConstraints(element) } // Set ifconditions attribute on root element, root ifConditions = [{ exp: element.elseif, block: element }, ...] addIfCondition(root, { exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { // The prompt indicates that you should not only use v-if on the root element, but use v-if and v-else-if together to ensure that the component has only one root element warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.`, { start: element.start } ) } } // Make yourself related to the parent element // Put yourself in the children array of the parent element, and then set your parent attribute to currentParent if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else { if (element.slotScope) { // scoped slot // keep it in the children list so that v-else(-if) conditions can // find it as the prev node. const name = element.slotTarget || '"default"' ; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } currentParent.children.push(element) element.parent = currentParent } } // Set your own child elements // Set all of your non slot child elements to element In the children array // final children cleanup // filter out scoped slots element.children = element.children.filter(c => !(c: any).slotScope) // remove trailing whitespace node again trimEndingWhitespace(element) // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // Execute the postTransform methods of model, class and style for element // However, the web platform does not provide this method // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } }
trimEndingWhitespace
/src/compiler/parser/index.js
/** * Delete the blank text node in the element, for example: < div > < / div >, delete the blank node in the div element and move it out of the children attribute of the element */ function trimEndingWhitespace(el) { if (!inPre) { let lastNode while ( (lastNode = el.children[el.children.length - 1]) && lastNode.type === 3 && lastNode.text === ' ' ) { el.children.pop() } } }
processIfConditions
/src/compiler/parser/index.js
function processIfConditions(el, parent) { // Find parent The last element node in children const prev = findPrevElement(parent.children) if (prev && prev.if) { addIfCondition(prev, { exp: el.elseif, block: el }) } else if (process.env.NODE_ENV !== 'production') { warn( `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` + `used on element <${el.tag}> without corresponding v-if.`, el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else'] ) } }
findPrevElement
/src/compiler/parser/index.js
/** * Find the last element node in children */ function findPrevElement(children: Array<any>): ASTElement | void { let i = children.length while (i--) { if (children[i].type === 1) { return children[i] } else { if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') { warn( `text "${children[i].text.trim()}" between v-if and v-else(-if) ` + `will be ignored.`, children[i] ) } children.pop() } } }
help
Here, the parsing part of the compiler is over. I believe many people see it in the clouds. Even if they see it several times, it may not be so clear.
Don't worry. This is normal. The amount of code in the compiler is really large. However, the content itself is not complex. The complex thing is that it has to deal with too many things, which leads to a huge amount of code in this part. Correspondingly, it will feel more difficult. It's really not simple. At least I think it's the most complex and difficult part of the whole framework.
You can read the videos and articles several times. If you don't understand, write some sample code to assist debugging and write detailed comments. Still that sentence, the meaning of reading a book a hundred times is self-evident.
In the process of reading, you need to grasp the essence of the compiler parsing part: parsing the HTML like string template into AST objects.
So one thing that so many codes are doing is parsing the string template and representing and recording the whole template with AST objects. Therefore, when you read, you can record the AST objects generated in the parsing process to help you read and understand, so that you will not be so confused after reading and help you understand.
This is a simple record of my reading:
const element = { type: 1, tag, attrsList: [{ name: attrName, value: attrVal, start, end }], attrsMap: { attrName: attrVal, }, rawAttrsMap: { attrName: attrVal, type: checkbox }, // v-if ifConditions: [{ exp, block }], // v-for for: iterator, alias: alias, // :key key: xx, // ref ref: xx, refInFor: boolean, // slot slotTarget: slotName, slotTargetDynamic: boolean, slotScope: Expression for the scope slot, scopeSlot: { name: { slotTarget: slotName, slotTargetDynamic: boolean, children: { parent: container, otherProperty, } }, slotScope: Expression for the scope slot, }, slotName: xx, // Dynamic component component: compName, inlineTemplate: boolean, // class staticClass: className, classBinding: xx, // style staticStyle: xx, styleBinding: xx, // attr hasBindings: boolean, nativeEvents: {with evetns}, events: { name: [{ value, dynamic, start, end, modifiers }] }, props: [{ name, value, dynamic, start, end }], dynamicAttrs: [with attrs], attrs: [{ name, value, dynamic, start, end }], directives: [{ name, rawName, value, arg, isDynamicArg, modifiers, start, end }], // v-pre pre: true, // v-once once: true, parent, children: [], plain: boolean, }
summary
The interviewer asked: briefly, what did Vue's compiler do?
Answer:
Vue's compiler does three things:
- Parse the html template of the component into an AST object
- Optimize, traverse the AST, make static marks for each node, mark whether it is a static node, and then further mark the static root node, so that these static nodes can be skipped in the process of subsequent updates; The marked static root is used to generate the render function stage and generate the render function of the static root node
- Generate runtime rendering functions from AST, that is, what we call render. In fact, there is another one, the staticRenderFns array, which stores the rendering functions of all static nodes
The interviewer asked: let's talk about the parsing process of the compiler in detail. How does it turn the html string template into an AST object?
Answer:
- Traverse the HTML template string and match "<" through regular expression
Skip some tags that do not need to be processed, such as annotation tag, conditional annotation tag and Doctype.
Note: the core of the whole parsing process is to deal with the start tag and end tag
Parse start tag
- Get an object, including tag name, all attributes and the index position of the tag in the html template string
- Further process the attrs attribute obtained in the previous step and change it into [{name: attrName, value: attrVal, start: xx, end: xx},...] Form of
- The AST object generated from the tag name, attribute object and parent element of the current element is actually an ordinary JS object, which records some information of the element in the form of key and value
- Next, further process some instructions on the start tag, such as v-pre, v-for, v-if and v-once, and put the processing results on the AST object
- After processing, store the ast object into the stack array
- After processing, the html string will be truncated and the processed string will be truncated
Resolve closed label
- If the end tag is matched, the last element is taken from the stack array, which is a pair of the currently matched end tag.
Process the attributes on the start tag again, which are different from the previous processing, such as key, ref, scopedSlot, style, etc., and put the processing results on the AST object of the element
Note: the video said there was an error in this area. After looking back, there was no problem and there was no need to change it. That's true
- Then connect the current element with the parent element, set the parent attribute to the ast object of the current element, and then put yourself in the children array of the ast object of the parent element
- Finally, after traversing the entire html template string, the ast object is returned
link
- Supporting video, WeChat official account reply Get "proficient in Vue technology stack source code principle video version":
- Proficient in Vue technology stack source code principle column
- github warehouse liyongning/Vue Welcome, Star
Thank you for your attention, likes, collections and comments. See you next time.
When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.
The new video and articles will be sent to WeChat official account for the first time. Li Yongning
The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.