“This is the 8th day of my participation in the First Challenge 2022. For details: First Challenge 2022”
preface
In the previous article, we looked at part of the parse code during compilation, which is the implementation of parseHTML. The template is initially parsed in parseHTML by word-for-word matching. Now let’s move on to analyzing how the output in parseHTML is used by Parse. This completes the analysis of the complete parse process.
parse
We still start with the import file
const ast = parse(template.trim(), options)
Copy the code
Let’s look at the implementation of Parse
const stack = []
let root
let currentParent
parseHTML(template, {
expectHTML: options.expectHTML,
// ...
start (tag, attrs, unary, start, end) {
// Parse the hook called by the start tag
},
end (tag, start, end) {
// Parse the hook called by the closing tag
},
chars (text: string, start: number, end: number) {
// Parse the hook called by the text node
},
comment (text: string, start, end) {
// Parse the hook called by the annotation node}})return root
Copy the code
As you can see, parse’s implementation basically initializes some hook functions and passes them as arguments to parseHTML. We looked at the implementation of parseHTML, which extracts the teamplate’s information through regex, extracts its tag attributes, and then calls the hooks in Parse. So the focus of this article is on further processing in hook functions.
Let’s do it by example
<div>
<div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
<div v-for="item in 10"></div>
</div>
Copy the code
start
start (tag, attrs, unary, start, end) {
/ / 1
let element: ASTElement = createASTElement(tag, attrs, currentParent)
/ / 2
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// ...
/ / 3
if (inVPre) {
processRawAttrs(element)
} else if(! element.processed) {// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
// ...
/ / 4
if(! unary) { currentParent = element stack.push(element) }else {
closeElement(element)
}
}
Copy the code
The simplified SATRT is not complicated. Let’s sort out its implementation
- Create a node AST from label and attribute data
{
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []}Copy the code
-
Execute functions exposed in preTransforms, which are collections of functions associated with modules in baseOptions, similar to the node updates in vUE rendering analyzed earlier.
-
Different process functions are executed, and as you can see from the function name, process is used to further process instructions such as for, if, and so on.
-
We defined the stack to hold the stack of nodes currently created, push it in after creation, and point currentParent to the node.
For a single node, let’s look at the data comparison before and after start
end
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
Copy the code
The main code for end is simply to push out the node just pushed in start and update currentParent to indicate that the current node label is closed and processing is complete. CloseElement does some extra checks and calls and so on, so no analysis here.
chars
chars (text: string, start: number, end: number) {
const children = currentParent.children
/ /...
if (text) {
let res
letchild: ? ASTNodeif(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text
}
}
if (child) {
children.push(child)
}
}
}
Copy the code
Chars is used to process text information, mainly by calling parseText to parse the string in the text and extract the variables. Let’s look at the implementation
export function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if(! tagRE.test(text)) {return
}
/ / 1
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
/ / 2
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp}) `)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
/ / 3
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
/ / 4
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
Copy the code
The implementation of parseText is also not complicated, a bit like parseHTML, in that it actually tokenizes text nodes further
-
Defines output variables as well as temporary variables needed to traverse text
-
The loop matches the text, extracting the traversal by matching {{}}(of course, passing other delimiters will be different), and determining whether there are strings in front of it by matching the position, extracting all together. Of course matching variables add _s(). Don’t think twice that _s() is actually the function that will be executed later when used for rendering.
-
Do the closing, that is, the XXX text in the case of {{name}} XXXX.
-
Return the extracted expression and token, and let’s see the difference between the input and output values.
comment
Finally, let’s take a look at the annotation node processing, which is very simple, using the corresponding node variable to store text
comment (text: string, start, end) {
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
currentParent.children.push(child)
}
}
Copy the code
other
We looked at the main processes of Parse earlier, and it didn’t feel complicated. The actual parse contains a lot of content because we skip a lot of instruction processing logic such as V-if, V-for, V-pre, V-slot, V-ESle, V-elseif, V-Model, etc. Their processing logic is mainly in their respective process functions. We take V-for as an example to analyze their processing
processFor
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
}
}
}
Copy the code
The primary logic of processFor is to determine the presence of a V-for instruction by the attrsMap of the node and, if so, to parse its value. Merge the parsing results into element.
Let’s look at parseFor
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
if(! inMatch)return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, ' ')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, ' ').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
Copy the code
The logic of parseFor actually parses the developer-defined items such as item in List, splits it up and returns its object, which is stored in properties such as alias and for.
Let’s look at the nodes before and after the processing
AST
Let’s take a look at the AST code that our template eventually generated
<div>
<div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
<div v-for="item in 10"></div>
</div>
Copy the code
const ast = parse(template.trim(), options)
Copy the code
conclusion
This article examines the first step in vUE compilation, compiling the Template into an AST. Compared with Babel, the AST generation of JS code is actually much simpler. The reason is that the parsing of templates is mainly to match tags and attributes in order, while the parsing of code has to consider a lot of things, especially to consider the processing of syntax logic. We will continue to examine the AST transformation in the second step of compilation.