第9章 模板编译
模板 -> 模板编译 -> 渲染函数 -> vnode -> 视图
template中可以用{{}}加入变量和js表达式,可以使用一些v-指令,都由于模板编译赋予模板强大的功能,将这些功能都变成渲染函数的一部分,从而渲染视图。
将模板编译成渲染函数可以分为3个步骤:
1.将模板解析成AST => 解析器
2.遍历AST标记静态节点 => 优化器
3.使用AST生成渲染函数 => 代码生成器
解析器
通过一条主线将不同类型的解析器(文本解析器、HTML解析器、过滤器解析器)组装在一起,监听HTML解析到HTML标签的开始位置、结束位置、文本或注释时,触发钩子函数,生成一个对应的AST节点。文本解析器用于解析带变量的文本,过滤器解析器用于解析过滤器。
优化器
遍历AST,检测出所有静态子树,并给其打标记。当静态节点被标记后,每次重新渲染时,会跳过更新节点的过程,而是直接克隆已经存在的虚拟节点。静态节点除了首次渲染,后续不需要任何重新渲染操作,优化器可以避免无用渲染,从而提升性能。
代码生成器
把AST转换成渲染函数中的代码字符串。代码字符串最终导出到外界使用时,会被放到渲染函数中,当渲染函数被导出,模板编译的任务就完成了。代码字符串中的_c和_v函数调用都是创建vnode的方法,_c创建元素类型的,_v创建文本类型的。
第9章 解析器
模板 -> AST
运行原理
解析器分为HTML解析器、文本解析器、过滤器解析器,最主要的是HTML解析器。在解析HTML的过程中,不断触发开始标签、结束标签、文本及注释钩子函数,在这些钩子函数里,可以构建AST节点。
层级关系通过栈来实现。解析HTML时,从前往后解析,每次触发start钩子函数时,如果当前节点有子节点,就推入栈中;当触发end钩子函数时,就从栈中弹出一个节点。这样可以保证,每当触发钩子函数start时,栈的最底节点就是当前正在构建节点的父节点。
当HTML解析器不再触发钩子函数时,说明所有模板都已经解析完毕,AST构建完成。
parseHTML(template, {
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent)
}
chars (text) {
let element = { type: 3, text}
}
comment (text) {
let element = { type: 3, text, isComment: true}
}
end () {}
})
function createASTElement (tag, attrs, parent) {
return {
type: 1,
tag,
attrsList: attrs,
parent,
children: []
}
}
HTML解析器
export function parseHTML (html, options) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
// 父元素为正常元素
let textEnd = html.indexOf('<')
// 以<开头
if (textEnd === 0) {
const comment = /^<!--/
const conditionalComment = /^<!\]/
const doctype = /^<!DOCTYPE [^>]+>/i
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)' + |([^\s"'=<>`]+)))?/
// 注释
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
html = html.substring(commentEnd + 3)
continue
}
}
// 条件注释
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd > 0) {
html = html.substring(conditionalEnd + 2)
continue
}
}
// DOCTYPE
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
html = html.substring(doctypeMatch[0].length)
continue
}
// 结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
html = html.substring(endTagMatch[0].length)
options.end(endTagMatch[1])
continue
}
// 开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch) // 将tagName、attrs和unary取出来,然后调用start钩子将这些数据放到参数中
continue
}
function advance (n) {
html = html.substring(n)
}
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
// 解析标签属性
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
math.attrs.push(attr)
}
// 是否自闭和
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
return match
}
}
}
}
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 如果<在文本中,且不符合任何需要被解析的片段的类型,那就属于文本
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
html = html.substring(textEnd)
}
// 整个模板找不到<,说明全是文本
if (textEnd < 0) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
} else {
// 父元素为script、style、textarea,标签内所有内容当作文本处理
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stachedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function(all, text) {
if (options.chars) {
options.chars(text)
}
return ''
})
html = rest
options.end(stackedTag)
}
}
}
文本解析器
对HTML解析器解析出来的文本进行二次加工,纯文本不用进行任何处理,主要是用于进一步解析带变量的文本。
parseHTML(template, {
start (tag, attrs, unary) {},
end () {},
chars (text) {
text = text.trim()
if (text) {
const children = currentParent.children
let expression
if (expression = parseText(text)) {
children.push({
type: 2,
expression,
text
})
} else {
children.push({
type: 3,
text
})
}
}
}
})
// "Hello {{name}}" => '"Hello" +_s(name)'
parseText (text) {
const tagRE = /\{\{((?:.|\n)+?)\}\}/g
// 如果没匹配到{{}},则为纯文本,直接return
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
while ((match = tagRE.exec(text))) {
index = match.index // 匹配到{{}}的位置
// 把{{前面的文本添加到tokens中
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// 把匹配到的变量改成_s(x)形式,也添加到数组中
tokens.push(`_s(${match[1].trim()})`)
// 更新lastIndex,保证下一轮循环不会重复匹配已经解析过的文本
lastIndex = index + match[0].length
}
// 所有变量都处理完,如果右边还有文本,将文本添加到数组中
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
// 将数组中每一位拼回字符串
return tokens.join('+')
}
第10章 优化器
目的是找到AST中的静态子树,并打上标记。
静态子树: 在AST中永远不会发生变化的节点,eg:纯文本节点。
标记静态子树有两点好处:
- 每次重新渲染时,不需要为静态子树创建新的节点,而是直接克隆已存在的子树
- 在虚拟dom patching过程中,如果新旧节点都是静态子树,就无需进行patching,可以直接跳过
优化器内部实现过程
主要分为两个步骤:
- 找出AST中所有静态节点并打标记--static属性为true
- 找出AST中所有静态根节点并打标记--staticRoot为true(所有子节点都是静态节点,并且父级是动态节点)
优化的过程,其实是给ASTnode添加static和staticRoot属性。
export function optimize(root) {
if (!root) return
// 标记所有静态节点
markStatic()
// 标记所有静态根节点
markStaticRoots(root)
function isStatic (node) {
if (node.type === 2) return false // 带变量的文本节点
if (node.type === 3) return true // 不带变量的纯文本节点 type === 1为元素节点
return !!(node.pre || ( // v-pre说明是静态节点
!node.hasBindings && // 没有动态绑定: v-, @
!node.if && !node.for && // 没有使用v-if v-for
!isBuiltInTag(node.tag) && // 不能是内置标签,slot或component
isPlatformReservedTag(node.tag) && // 不是组件,标签名必须是保留标签(分为HTML保留标签和SVG保留标签)
!isDirectChildOfTemplateFor(node) && // 当前节点的父节点不能是带v-for的template标签
Object.keys(node).every(isStaticKey) // 节点中不存在动态节点才会有的属性,静态节点的属性范围是type\tag\attrsList\attrsMap\plain\parent\children\attrs\staticClass\staticStyle,如果有属性名不在这之内,就不是静态节点
))
}
function markStatic (node) {
node.static = isStatic(node)
if (node.type === 1) {
for (let i=0; i<node.children.length; i++) {
const child = node.children[i]
markStatic(child)
// 重新校对,如果子节点是动态,那此节点也是动态
if (!child.static) {
node.static = false
}
}
}
}
function markStaticRoots (node) {
if (node.type === 1) {
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) { // 第一个找到的静态节点,其子节点一定也为静态,所以标记为静态根节点,直接return不再递归判断子节点
node.staticRoot = true
return
} else { // 只有一个文本节点,即使是静态节点也不会被标记,因为其优化成本大于收益
node.staticRoot = false
}
if (node.children) { // 递归
for (let i=0; i<node.children.length; i++) {
markStaticRoots(node.children[i])
}
}
}
}
}
第11章 代码生成器
将AST转换成渲染函数中的代码字符串,渲染函数被执行后,生成vnode,虚拟dom通过vnode来渲染视图。
// <div id="el">Hello {{name}}</div>
// 生成的代码字符串如下
with(this) {
return _c( // _c其实是createElement,创建虚拟dome元素节点
"div",
{
attrs: {"id": "el"}
},
[
_v("Hello " + _s(name))
]
)
}
生成原理
递归,从顶向下依次处理每一个AST节点。每处理一个节点,就会生成一个与节点类型相对应的代码字符串。
// 元素节点对应的代码字符串
_c(<tagname>, <data>, <children>)
处理子节点时,创建出来的代码字符串会放在<children>位置。
<div id="el">
<div>
<p>Hello {{name}}</p>
</div>
</div>
// 创建出的代码字符串
_c('div',
{
attrs: { "id": "el"}
},
[
_c('div',
[
_c('p',
[_v("Hello"+_s(name))]
)
]
)
]
)
// 代码字符串会被包裹在with语句中的code中
`with(this){return ${code}}`
生成元素节点
_c
function genElement (el, state) {
const data = el.plain ? undefined : genData(el, state)
const children = genChildren(el, state)
code = `_c('${el.tag}'${
data ? `,${data}` : ''
}${
children ? `,${children}` : ''
})`
return code
}
function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
if (el.key) {
data += `key:${el.key},`
}
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.pre) {
data += `pre:${el.pre},`
}
// ...
data = data.replace(/,$/, '') + '}'
return data
}
function genChildren (el, state) {
const children = el.children
if (children.length) {
return `[${children.map(c => genNode(c, state)).join(',')}]`
}
}
function genNode (node, state) {
if (node.type === 1) return genElement(node, state)
if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
生成文本节点
function genText (text) {
return `_v(${text.type === 2
? text.expression // 带变量文本
: JSON.stringify(text.text) // 静态纯文本 Hello => "'Hello'"
})`
}
生成注释节点
function genComment (comment) {
return `_e(${JSON.stringify(comment.text)})`
}