preface
Vue template compilation continues research
Template compilation
Rendering templates into functions can be done in two steps, parsing templates into an AST (abstract syntax tree) and then using the AST to generate rendering functions. But because static nodes do not change when data changes, they can be marked and updates skipped. So, in general logic, template compilation consists of three parts:
1. Compile the template into the AST
2. Traverse the AST to mark static nodes
3. Use AST to generate rendering functions
The parser
We convert the string passed in by template into an AST object. The data structure of stack stack is used to confirm the parent-child relationship between DOM and construct abstract syntax tree structure.
Regular parsing string
- Check if the string starts with “<“, if so, match the start tag or end tag
- If you start tag processing, the tag attributes will be processed along the way, and when the “>” tag is matched, you can put the tag on the stack and call the options.start method passed in.
- If it is an end tag, call the options.end method passed in. Label out of stack.
- If the string begins with either “<” or text, intercept the desired text and call the options.chars method passed in.
/ / property
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
// Dynamic attributes
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
// Tag names match
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
//
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)
Div > match start tag close XXXX >
const startTagClose = /^\s*(\/?) >/
// Match the end tag
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)
export function parseHTML(html: string, options: any) {
// Stack structure to parse DOM structure
const stack: any = []
let index = 0
// Last last HTML content, lastTag Last tag
let last, lastTag: any
while (html) {
last = html
// start with <
let textEnd = html.indexOf('<')
// May be the start tag, or the end tag
if (textEnd === 0) {
// Is the end tag
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// Call options.end, label off the stack, and enter the next loop
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
const startTagMatch = parseStartTag()
we can further process the start tag
if (startTagMatch) {
// call options.start, push the label, and enter the next loop
handleStartTag(startTagMatch)
continue}}let text, rest, next
// It is not a label, the description is text
if (textEnd >= 0) {
// In the loop, find the text before the < < to see if it meets the label requirements, can intercept the whole text "123
rest = html.slice(textEnd)
while(! endTag.test(rest) && ! startTagOpen.test(rest) ) { next = rest.indexOf('<'.1)
if (next < 0) break
// Cut back again
textEnd += next
rest = html.slice(textEnd)
}
// Intercepts the text before the tag
text = html.substring(0, textEnd)
}
// No <, all text
if (textEnd < 0) text = html
// Truncate the text
if (text) advance(text.length)
// Call options.chars to process the text content
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
// Error text, syntax error
if (html === last) {
options.chars && options.chars(html)
break}}// Clean up any remaining tags
parseEndTag()
// Intercepts a string
function advance(n: number) {
index += n
html = html.substring(n)
}
function parseStartTag() {
// Is the start label
const start = html.match(startTagOpen)
if (start) {
const match: any = {
tagName: start[1].attrs: [].start: index
}
advance(start[0].length)
let end = html.match(startTagClose)
// ...
if (end) {
// ...
return match
}
}
}
function handleStartTag(match: any) {
const tagName = match.tagName
// Label attributes are pushed onto the stack as objects
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
// Set the last label
lastTag = tagName
// Call the start method. Create the ast
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
function parseEndTag(tagName? : any, start? : any, end? : any) {
let pos, lowerCasedTagName
// Call the closing tag method
if (options.end) {
options.end(stack[i].tag, start, end)
}
// Label out of stack
/ /...
stack.pop()
lastTag = stack.length && stack[pos - 1].tag
}
}
}
Call the parseHTML method.Cosnt stack = [] cosnt stack = []let currentParent: any
let root: any
let stack: any = []
parseHTML(html, {
start(tag: any, attrs: any, unary: any, start: any, end: any) {
// Create a stack syntax tree
let element: any = createASTElement(tag, attrs, currentParent)
processFor(element) // v-for
processIf(element) // v-if
processOnce(element) // v-once
if(! root) root = element currentParent = element stack.push(element) },end(tag: any, start: any, end: any) {
const element = stack[stack.length - 1]
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
},
chars(text: string, start: number, end: number) {
if(! currentParent) {return
}
const children = currentParent.children
text = text.trim()
if (text) {
let res
let child: any
if ((res = parseText(text))) {
// Text with {{}}, type 2
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text
}
} else {
// Plain text with type 3
child = {
type: 3,
text
}
}
if (child) {
children.push(child)
}
}
}
})
// Create the AST syntax tree
function createASTElement(
tag: string,
attrs: Array<any>,
parent: any | void
) :any {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []}}// Parse text content with {{}}
export function parseText(text: string) :any | void {
const tagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g
if(! tagRE.test(text)) {return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// Add {{text to tokens first
if (index > lastIndex) {
tokenValue = text.slice(lastIndex, index)
tokens.push(JSON.stringify(tokenValue))
}
// Pass the contents of the curly braces as arguments to the _s method, which will be executed as an expression
const exp = match[1].trim()
tokens.push(`_s(${exp}) `)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokenValue = text.slice(lastIndex)
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+')}}Copy the code
The ultimate root is an abstract syntax tree
{
attrsList: [{...}]attrsMap: {class: 'box'}
children: (2) / {...}, {...}.parent: undefined
plain: false
rawAttrsMap: {}
tag: "div"
type: 1
}
Copy the code
The optimizer
The internal implementation of the optimizer consists of two main steps:
- Find and label all static nodes in the AST
- Find and label all static root nodes in the AST
Static node: Nodes that never change are static nodes static root node: A node is a static root node if all of its children are static nodes and its parent is a dynamic node
export function optimize (root: ? ASTElement, options: CompilerOptions) {
if(! root)return
markStatic(root)
markStaticRoots(root, false)}// Mark the static node
function markStatic (node: any) {
node.static = isStatic(node)
if (node.type === 1) {
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
// The child node is a dynamic node, change the parent node to dynamic node
if(! child.static) { node.static =false}}}}// Mark the static root node
function markStaticRoots (node: any) {
if (node.type === 1) {
To qualify a node as a static root node, it must have child nodes
// This node cannot be a child node with only one static text, otherwise the optimization costs will outweigh the benefits
if(node.static && node.children.length && ! ( node.children.length ===1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
// Find the static root node and exit
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i])
}
}
}
}
// Check whether the node is static
function isStatic (node: any) :boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return!!!!! (node.pre || ( ! node.hasBindings &&// Dynamic binding syntax cannot be used! node.if && ! node.for &&// Do not use v-if or v-for or v-else! isBuiltInTag(node.tag) &&// The tag name is not slot or compoennt
isPlatformReservedTag(node.tag) && // Cannot be a component! isDirectChildOfTemplateFor(node) &&// The parent node cannot be a template with v-for
Object.keys(node).every(isStaticKey) // There are no attributes for dynamic nodes))}Copy the code
Code generator
The code generator is the final step in template compilation and is used to convert the AST into the content of the rendering function, which can become a code string. The code string can be executed wrapped in a function, which is commonly referred to as a render function. After the render function is executed, a VNode can be generated.
Suppose we have a simple template like this:
Hello {{name}}
Processed code:
_c: the alias for creaElement, which creates a virtual node, is the h function in our handwritten render function.
with(this) {
return _c(
"div",
{
attrs: {"id": "el"}
},
[
_v("Hello " + _s(name))
]
)
}
Copy the code
The source code to achieve
- Based on each label information of the AST, the concatenation _c function, the children node, is concatenated into an array
- Use different methods to concatenate nodes _c(element node method), _e(text node method), and _v(text node method), depending on the type not used.
- Finally, the code string is spelled with and returned to the caller.
// Pass in the AST abstract syntax tree to generate the render function code
export function generate(ast: any) :any {
const code = ast ? genElement(ast) : '_c("div")'
return {
render: `with(this){return ${code}} `}}// The method to generate nodes, in the third argument to recursively generate each child element
export function genElement(el: any) :string {
let code
const data = el.plain ? undefined : genData(el)
const children = genChildren(el)
code = `_c('${el.tag}'${data ? `,${data}` : ' ' // data
}${children ? `,${children}` : ' ' // children
}) `
return code
}
// Generate child nodes recursively to form a virtual DOM tree
export function genChildren(el: any,) :string | void {
const children = el.children
if (children.length) {
return ` [${children.map((c: any) => genNode(c)).join(', ')}] `}}// Generate different nodes according to type
function genNode(node: any) :string {
if (node.type === 1) {
return genElement(node)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
// Generate a comment node
function genComment(comment: any) :string {
return `_e(The ${JSON.stringify(comment.text)}) `
}
// Generate a text node
function genText(text: any) :string {
return `_v(${text.type === 2 ? text.expression : JSON.stringify(text.text)}) `
}
// Generate tag attributes
export function genData(el: any) :string {
let data = '{'
// attributes
if (el.attrsList) {
data += `attrs:${genProps(el.attrsList)}, `
}
// ...
data = data.replace($/ /,.' ') + '} '
return data
}
function genProps(props: Array<any>) :string {
let staticProps = ` `
for (let i = 0; i < props.length; i++) {
const prop = props[i]
const value = JSON.stringify(prop.value)
staticProps += `"${prop.name}":${value}, `
}
staticProps = ` {${staticProps.slice(0, -1)}} `
return staticProps
}
Copy the code
The last
This article seven seven eight eight took a day to write, although, the reference to the “simple Vue. Js” the content of the third article, but still quite spend time, involving a lot of complex code processing is deleted, interested friends or need to see the source code. The year of 2021 is coming to an end. I hope you can cheer up and give me a lot of thumbs up. Merry Christmas
Code address: github.com/zhuye1993/m…