前言
Vue的模板编译的继续研究
模板编译
将模板渲染成函数可以分为两个步骤,先将模板解析成AST(抽象语法树),然后再使用AST生成渲染函数。但是由于静态节点在数据变化时,也不会发生改变,所以可以标记静态节点,跳过更新。所以,在大体逻辑上,模板编译分三部分内容:
1.将模板编译成AST
2.遍历AST标记静态节点
3.使用AST生成渲染函数
解析器
主要将template传入的字符串,转化为AST对象。利用stack栈的数据结构来确认DOM之间的父子关系,构建抽象语法树结构。
正则解析字符串
- 判断字符串是不是"<"开头,如果是就可以匹配是开始标签或者结束标签
- 如果是开始标签处理时,会顺带处理标签属性,最后匹配">"标签之后,就可以将标签放入栈中,调用传入的options.start方法。
- 如果是结束标签时,调用传入的options.end方法。标签出栈。
- 字符串不是"<"开头,就是文本内容,截取需要的文本,调用传入的options.chars方法。
// 属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 动态属性
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 标签名匹配
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// <div 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// div> 匹配开始标签的关闭 xxxx>
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
export function parseHTML(html: string, options: any) {
// 栈结构来解析DOM结构
const stack: any = []
let index = 0
// last最后的html内容, lastTag上一次的tag标签
let last, lastTag: any
while (html) {
last = html
// 以<开头
let textEnd = html.indexOf('<')
// 可能是开始标签,或者结束标签
if (textEnd === 0) {
// 是结束标签 </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 调用options.end方法,标签出栈,进入下次循环
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 获取到 1.开始标签名 2.标签属性 3.开始标签结尾,<div class="box" 解析完成
const startTagMatch = parseStartTag()
// 开始标签组合成<div class="box">,我们才能进一步处理开始标签
if (startTagMatch) {
// 调用options.start方法,标签入栈,进入下次循环
handleStartTag(startTagMatch)
continue
}
}
let text, rest, next
// 不是标签,说明是文本
if (textEnd >= 0) {
// 如果有"123<d</span>"这种文本,我们去掉文本123,剩余"<d</span>",取的文本不完整
// 在循环中,每次找<之前的文本,看是否符合标签要求,可以截取的完整的文本"123<d"
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest)
) {
next = rest.indexOf('<', 1)
if (next < 0) break
// 再往后截取
textEnd += next
rest = html.slice(textEnd)
}
// 截取标签之前的文本
text = html.substring(0, textEnd)
}
// 不包含<,内容全是文本
if (textEnd < 0) text = html
// 截掉文本内容
if (text) advance(text.length)
// 调用options.chars,处理文本内容
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
// 错误文本,语法错误
if (html === last) {
options.chars && options.chars(html)
break
}
}
// Clean up any remaining tags
parseEndTag()
// 截取字符串
function advance(n: number) {
index += n
html = html.substring(n)
}
function parseStartTag() {
// 是否是开始标签
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
// 标签属性当做对象,push到stack栈中
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
// 设置上一次标签
lastTag = tagName
// 调用start方法。创建ast
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
function parseEndTag(tagName?: any, start?: any, end?: any) {
let pos, lowerCasedTagName
// 调用结束标签的方法
if (options.end) {
options.end(stack[i].tag, start, end)
}
// 标签出栈
//...
stack.pop()
lastTag = stack.length && stack[pos - 1].tag
}
}
}
// 调用parseHTML方法。
这个stack是维护AST语法树的,与上面的不是同一个
cosnt stack = []
let currentParent: any
let root: any
let stack: any = []
parseHTML(html, {
start(tag: any, attrs: any, unary: any, start: any, end: any) {
// 创建stack语法树
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))) {
// 带{{}}的文本,type为2
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else {
// 纯文本,type为3
child = {
type: 3,
text
}
}
if (child) {
children.push(child)
}
}
}
})
// 创建AST语法树
function createASTElement(
tag: string,
attrs: Array<any>,
parent: any | void
): any {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
// 解析带{{}}文本内容
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
// 先将{{前边文本添加到tokens中
if (index > lastIndex) {
tokenValue = text.slice(lastIndex, index)
tokens.push(JSON.stringify(tokenValue))
}
// 将花括号里面的内容当做参数,传入_s方法中,最后将作为一个表达式执行
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('+')
}
}
最终的root就是一颗抽象语法树
{
attrsList: [{…}]
attrsMap: {class: 'box'}
children: (2) [{…}, {…}]
parent: undefined
plain: false
rawAttrsMap: {}
tag: "div"
type: 1
}
优化器
优化器的内部实现主要分为两个步骤:
- 在AST中找出所有静态节点并打上标记
- 在AST中找出所有静态根节点并打上标记 静态节点:永远都不会发生变化的节点属于静态节点 静态根节点:如果一个节点下面的所有子节点都是静态节点,并且它的父级是动态节点,那么它就是静态根节点
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
markStatic(root)
markStaticRoots(root, false)
}
// 标记静态节点
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)
// 子节点是动态节点,将父节点改为动态节点
if (!child.static) {
node.static = false
}
}
}
}
// 标记静态根节点
function markStaticRoots (node: any) {
if (node.type === 1) {
// 要使节点符合静态根节点的要求,它必须有子节点
// 这个节点不能是只有一个静态文本的子节点,否则优化成本将超过收益
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
// 找到静态根节点,直接退出
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i])
}
}
}
}
// 判断是否静态节点
function isStatic (node: any): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // 不能使用动态绑定语法
!node.if && !node.for && // 不能使用 v-if or v-for or v-else
!isBuiltInTag(node.tag) && // 标签名不是slot或compoennt
isPlatformReservedTag(node.tag) && // 不能是组件
!isDirectChildOfTemplateFor(node) && // 父节点不能是带v-for的template
Object.keys(node).every(isStaticKey) // 不存在动态节点才有的属性
))
}
代码生成器
代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以成为代码字符串。 代码字符串可以被包装在函数中执行,这个函数就是我们通常所说的渲染函数。 渲染函数被执行之后,可以生成一份VNode。
假设现有这样一个简单的模板:
Hello {{name}}
被处理完成的代码:
_c:其实就是creaElement的别名,它的作用是创建虚拟节点,其实就是我们手写render函数中的h函数。
with(this) {
return _c(
"div",
{
attrs: {"id": "el"}
},
[
_v("Hello " + _s(name))
]
)
}
源码实现
- 根据ast的每一个标签信息,拼接_c函数,children的子节点,拼接在数组中
- 根据不用type,使用不同的方法拼接节点_c(元素节点方法)、_e(文本节点方法)、_v(文本节点方法)
- 最后将代码字符串拼在with中返回给调用者。
// 传入ast抽象语法树,生成render函数代码
export function generate(ast: any): any {
const code = ast ? genElement(ast) : '_c("div")'
return {
render: `with(this){return ${code}}`
}
}
// 生成节点的方法,第三个参数中递归生成每一个子元素
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
}
// 递归生成子节点,形成虚拟dom树
export function genChildren(el: any,): string | void {
const children = el.children
if (children.length) {
return `[${children.map((c: any) => genNode(c)).join(',')}]`
}
}
// 根据类型生成不同的节点
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)
}
}
// 生成注释节点
function genComment(comment: any): string {
return `_e(${JSON.stringify(comment.text)})`
}
// 生成文本节点
function genText(text: any): string {
return `_v(${text.type === 2 ? text.expression : JSON.stringify(text.text)})`
}
// 生成标签属性
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
}
最后
这篇文章七七八八花了一天的时间写,虽然,参考了《深入浅出Vue.js》第三篇的内容,但是还是挺花时间的,涉及到的很多复杂的代码处理都是删掉了,感兴趣的朋友还是需要自己去看看源码。2021年接近尾声了,希望大家一起加油,多多点赞,圣诞快乐~