前言
熟悉 Vue 的同学都知道,从 Vue2 开始,在实际运行的时候,是将用户所写的 template 转换为 render 函数,得到 vnode 数据(虚拟 DOM),然后再继续执行,最终经过 patch 到真实的 DOM,而当有数据更新的时候,也是靠这个进行 vnode 数据的 diff,最终决定更新哪些真实的 DOM。
这个也是 Vue 的一大核心优势,尤大不止一次的讲过,因为用户自己写的是静态的模板,所以 Vue 就可以根据这个模板信息做很多标记,进而就可以做针对性的性能优化,这个在 Vue 3 中做了进一步的优化处理,block 相关设计。
所以,我们就来看一看,在 Vue 中,template 到 render 函数,到底经历了怎么样的过程,这里边有哪些是值得我们借鉴和学习的。
正文分析
What
template 到 render,在 Vue 中其实是对应的 compile 编译的部分,也就是术语编译器 cn.vuejs.org/v2/guide/in… 本质上来讲,这个也是很多框架所采用的的方案 AOT,就是将原本需要在运行时做的事情,放在编译时做好,以提升在运行时的性能。
关于 Vue 本身模板的语法这里就不详细介绍了,感兴趣的同学可以看 cn.vuejs.org/v2/guide/sy… ,大概就是形如下面的这些语法(插值和指令):
render 函数呢,这部分在 Vue 中也有着详细的介绍,大家可以参阅 cn.vuejs.org/v2/guide/re… ,简单来讲,大概就是这个样子:
那我们的核心目标就是这样:
如果你想体验,可以这里 template-explorer.vuejs.org
当然 Vue 3 的其实也是可以的 https://vue-next-template-explorer ,虽然这里我们接下来要分析的是 Vue 2 版本的。
How
要想了解是如何做到的,我们就要从源码入手,编译器相关的都在 github.com/vuejs/vue/t… 目录下,我们这里从入口文件 index.js 开始:
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 重点!
// 第1步 parse 模板 得到 ast
const ast = parse(template.trim(), options)
// 优化 可以先忽略
if (options.optimize !== false) {
optimize(ast, options)
}
// 第2步 根据 ast 生成代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
其实,你会发现,这是一个经典的编译器(Parsing、Transformation、Code Generation)实现的步骤(这里其实是简化):
- parse,得到 ast
- generate,得到目标代码
接下来我们就分别来看下对应的实现。
1. parse
parse 的实现在 github.com/vuejs/vue/b… 这里,由于代码比较长,我们一部分一部分的看,先来看暴露出来的 parse 函数:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// options 处理 这里已经忽略了
// 重要的栈 stack
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 根节点,只有一个,因为我们知道 Vue 2 的 template 中只能有一个根元素
// ast 是树状的结构,root 也就是这个树的根节点
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
// parseHTML 处理
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 注意后边的这些 options 函数 start end chars comment
// 约等于是 parseHTML 所暴露出来的钩子,以便于外界处理
// 所以纯粹的,parseHTML 只是负责 parse,但是并不会生成 ast 相关逻辑
// 这里的 ast 生成就是靠这里的钩子函数配合
// 直观理解也比较容易:
// start 就是每遇到一个开始标签的时候 调用
// end 就是结束标签的时候 调用
// 这里重点关注 start 和 end 中的逻辑就可以,重点!!
// chars comment 相对应的纯文本和注释
start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 创建一个 ASTElement,根据标签 属性
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
// 一些前置转换 可以忽略
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 处理 vue 指令 等
processFor(element)
processIf(element)
processOnce(element)
}
if (!root) {
// 如果还没有 root 即当前元素就是根元素
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
// 设置当前 parent 元素,处理 children 的时候需要
currentParent = element
// 因为我们知道 html 的结构是 <div><p></p></div> 这样的,所以会先 start 处理
// 然后继续 start 处理 然后 才是两次 end 处理
// 是一个经典的栈的处理,先进后出的方式
// 其实任意的编译器都是离不开栈的,处理方式也是类似
stack.push(element)
} else {
closeElement(element)
}
},
end (tag, start, end) {
// 当前处理的元素
const element = stack[stack.length - 1]
// 弹出最后一个
// pop stack
stack.length -= 1
// 最新的尾部 就是接下来要处理的元素的 parent
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars (text: string, start: number, end: number) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
})
// 返回根节点
return root
}
可以看出做的最核心的事情就是调用 parseHTML,且传的钩子中做的事情最多的还是在 start 开始标签这里最多。针对于在 Vue 的场景,利用钩子的处理,最终我们返回的 root 其实就是一个树的根节点,也就是我们的 ast,形如:
模板为:
<div id="app">{{ msg }}</div>
{
"type": 1,
"tag": "div",
"attrsList": [
{
"name": "id",
"value": "app"
}
],
"attrsMap": {
"id": "app"
},
"rawAttrsMap": {},
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [
{
"@binding": "msg"
}
],
"text": "{{ msg }}"
}
],
"plain": false,
"attrs": [
{
"name": "id",
"value": "app"
}
]
}
所以接下来才是parse最核心的部分 parseHTML,取核心部分(不全),一部分一部分来分析,源文件 github.com/vuejs/vue/b…
// parse的过程就是一个遍历 html 字符串的过程
export function parseHTML (html, options) {
// html 就是一个 HTML 字符串
// 再次出现栈,最佳数据结构,用于处理嵌套解析问题
// HTML 中就是处理 标签 嵌套
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 初始索引位置 index
let index = 0
let last, lastTag
// 暴力循环 目的为了遍历
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
if (!lastTag || !isPlainTextElement(lastTag)) {
// 没有 lastTag 即初始状态 或者说 lastTag 是 script style
// 这种需要当做纯文本处理的标签元素
// 正常状态下 都应进入这个分支
// 判断标签位置,其实也就是判断了非标签的end位置
let textEnd = html.indexOf('<')
// 在起始位置
if (textEnd === 0) {
// 注释,先忽略
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
// 条件注释,先忽略
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// Doctype 先忽略
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 结束标签,第一次先忽略,其他case会进入
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 处理结束标签
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 重点,一般场景下,开始标签
const startTagMatch = parseStartTag()
// 如果存在开始标签
if (startTagMatch) {
// 处理相关逻辑
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 剩余的 html 去掉文本之后的
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 纯文本内的 <
// < in plain text, be forgiving and treat it as text
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)
}
if (options.chars && text) {
// 钩子函数处理
options.chars(text, index - text.length, index)
}
} else {
// lastTag 存在 且是 script style 这样的 将其内容当做纯文本处理
let endTagLength = 0
// 存在栈中的tag名
const stackedTag = lastTag.toLowerCase()
// 指定 tag 的 匹配正则 注意 是到对应结束标签的 正则,例如 </script>
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
// 做替换
// 即把 <div>xxxx</div></script> 这样的替换掉
const rest = html.replace(reStackedTag, function (all, text, endTag) {
// 结束标签本身长度 即 </script>的长度
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
// 钩子函数处理
if (options.chars) {
options.chars(text)
}
// 替换为空
return ''
})
// 索引前进 注意没有用 advance 因为 html 其实是已经修正过的 即 rest
index += html.length - rest.length
html = rest
// 处理结束标签
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
}
break
}
}
}
这里边有几个重点的函数,他们都是定义在 parseHTML 整个函数上下文中的,所以他们可以直接访问上边定义的 index stack lastTag 等关键变量:
// 比较好理解,前进n个位置
function advance (n) {
index += n
html = html.substring(n)
}
// 开始标签
function parseStartTag () {
// 正则匹配开始 例如 <div
const start = html.match(startTagOpen)
if (start) {
// 匹配到的
const match = {
tagName: start[1],
attrs: [],
start: index
}
// 移到 <div 之后
advance(start[0].length)
let end, attr
// 到结束之前 即 > 之前
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
// 匹配属性们
attr.start = index
// 逐步移动
advance(attr[0].length)
attr.end = index
// 收集属性
match.attrs.push(attr)
}
// 遇到了 > 结束了
if (end) {
// 是否是 自闭合标签,例如 <xxxx />
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
// 当遇到开始标签的情况 去处理他们
// 因为开始标签的情况比较复杂 所以 单独了一个函数处理
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
// HTML 场景
// p 标签之内不能存在 isNonPhrasingTag 的tag
// 详细的看 https://github.com/vuejs/vue/blob/v2.6.14/src/platforms/web/compiler/util.js#L18
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
// 所以在浏览器环境 也是会自动容错处理的 直接闭合他们
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// 自闭和的场景 或者 可以省略结束标签的case
// 即 <xxx /> 或者 <br> <img> 这样的场景
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
if (!unary) {
// 如果不是自闭和case 也就意味着可以当做有 children 处理的
// 栈里 push 一个当前的
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
// 把 lastTag 设置为当前的
// 为了下次进入 children 做准备
lastTag = tagName
}
// start 钩子处理
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
// 结束标签处理
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
// 这里需要找到 最近的 相同类型的 未闭合标签
// 相对应的配对的那个元素
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}
if (pos >= 0) {
// 回到那个未闭合的标签,这中间里边所有的元素都需要闭合掉
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`,
{ start: stack[i].start, end: stack[i].end }
)
}
// end 钩子
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// 里边的元素也不需要处理了 直接修改栈的长度即可
// Remove the open elements from the stack
stack.length = pos
// 记得更新 lastTag
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// br 的情况 如果写的是 </br> 其实效果相当于 <br>
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// p 的情况 如果找不到 <p> 直接匹配到了 </p> 那么认为是 <p></p> 因为浏览器也是这样兼容
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
所以大概了解了上边三个函数的作用,再和 parseHTML 的主逻辑结合起来,我们可以大概整理下 parseHTML 的整个过程。
这里为了方便,以一个具体的示例来进行,例如
<div id="app">
<p :class="pClass">
<span>
This is dynamic msg:
<span>{{ msg }}</span>
</span>
</p>
</div>
那么首先直接进入 parseHTML,进入 while 循环,很明显会走入到对于开始标签的处理 parseStartTag
此时经过上边的一轮处理,html已经是这个样子了,因为每次都有 advance 前进:
也就是关于最开始的根标签 div 的开始部分 <div id="app">
已经处理完成了。
接着进入到 handleStartTag 的逻辑中
此时,stack 栈中已经 push 了一个元素,即我们的开始标签 div,也保存了相关的位置和属性信息,lastTag 指向的就是 div。
接着继续 while 循环处理
因为有空格和换行的关系,此时 textEnd 的值是 3,所以要进入到文本的处理逻辑(空格和换行本来就属于文本内容)
所以这轮循环会处理好文本,然后进入下一次循环操作,此时已经和我们第一轮循环的效果差不多:
再次lastTag变为了 p,然后进入到处理文本(空格、换行)的逻辑,这里直接省略,过程是一样的;
下面直接跳到第一次处理 span
其实还是重复和第一次的循环一样,处理普通元素,处理完成后的结果:
此时栈顶的元素是外部的这个 span。然后进入新一轮的处理文本:
接着再一次进入处理里层的 span 元素,一样的逻辑,处理完成后
然后处理最里层的文本,结束后,到达最里层的结束标签 </span>
,
这个时候我们重点看下这一轮的循环:
可以看到经过这一圈处理,最里层的 span 已经经过闭合处理,栈和lastTag已经更新为了外层的 span 了。
剩下的循环的流程,相信你已经能够大概猜到了,一直是处理文本内容(换行 空格)以及 parseEndTag 相关处理,一次次的出栈,直到 html 字符串处理完成,为空,即停止了循环处理。
十分类似的原理,我们的 parse 函数也是一样的,根据 parseHTML 的钩子函数,一次次的压榨,处理,然后出栈 处理,直至完成,这些钩子做的核心事情就是根据 parse HTML 的过程中,一步步构建自己的 ast,那么最终的 ast 结果
到这里 parse 的阶段已经彻底完成。
2. generate
接下来看看如何根据上述的 ast 得到我们想要的 render 函数。相关的代码在 github.com/vuejs/vue/b…
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
可以看出,generate 核心,第一步创建了一个 CodegenState 实例,没有很具体的功能,约等于是配置项的处理,然后进入核心逻辑 genElement,相关代码 github.com/vuejs/vue/b…
// 生成元素代码
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
基本上就是根据元素类型进行对应的处理,依旧是上边的示例的话,会进入到
接下来会是一个重要的 genChildren github.com/vuejs/vue/b…
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
可以看出,基本上是循环 children,然后 调用 genNode 生成 children 的代码,genNode github.com/vuejs/vue/b…
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
这里就是判断每一个节点类型,然后基本递归调用 genElement 或者 genComment、genText 来生成对应的代码。
最终生成的代码 code 如下:
可以理解为,遍历上述的 ast,分别生成他们的对应的代码,借助于递归,很容易的就处理了各种情况。当然,有很多细节这里其实被我们忽略掉了,主要还是看的正常情况下的核心的大概简要流程,便于理解。
到此,这就是在 Vue 中是如何处理编译模板到 render 函数的完整过程。
Why
要找到背后的原因,我们可以拆分为两个点:
- 为什么要引入 Virtual DOM
- 为什么推荐模板(将模板转换为render函数,得到 vnode 数据)
为什么要引入 Virtual DOM
这个问题其实尤大本人自己讲过,为什么在 Vue 2 中引入 Virtual DOM,是不是有必要的等等。
来自方应杭的聚合回答:
这里有一些文章和回答供参考(也包含了别人的总结部分):
- 官网的渲染函数部分 cn.vuejs.org/v2/guide/re…
- zhuanlan.zhihu.com/p/23752826
- zhuanlan.zhihu.com/p/108899766
- zhuanlan.zhihu.com/p/58335278
- www.zhihu.com/question/28…
- www.zhihu.com/question/31…
为什么推荐模板
这个在官网框架对比中有讲到,原文 cn.vuejs.org/v2/guide/co…
当然,除了上述原因之外,就是我们在前言中提到的,模板是静态的,Vue 可以做针对性的优化,进而利用 AOT 技术,将运行时性能进一步提升。
这个也是为什么 Vue 中有构建出来了不同的版本,详细参见 cn.vuejs.org/v2/guide/in…
总结
通过上边的分析,我们知道在 Vue 中,template到render函数的大概过程,最核心的还是:
- 解析 HTML 字符串,得到自己定义的 AST
- 根据 AST,生成最终的 render 函数代码
这个也是编译器做的最核心的事情。
那么我们可以从中学到什么呢?
编译器
编译器,听起来就很高大上了。通过我们上边的分析,也知道了在 Vue 中是如何处理的。
编译器的核心原理和相比较的标准化的过程基本上还是比较成熟的,不管说这里分析和研究的对于 HTML 的解析,然后生成最终的 render 函数代码,还是其他任何的语言,或者是你自己定义的”语言“都是可以的。
想要深入学习的话,最好的就是看编译原理。在社区中,也有一个很出名的项目 github.com/jamiebuilds… 里边有包含了一个”五脏俱全“的编译器,核心只有 200 行代码,里边除了代码之外,注释也是精华,甚至于注释比代码更有用,很值得我们去深入学习和研究,且易于理解。
树
树的这种数据结构,上述我们通过parse得到的 ast 其实就是一种树状结构,树的应用,基本上随处可见,只要你善于发现。利用他,可以很好的帮助我们进行逻辑抽象,统一处理。
栈
在上述的分析中,我们是多次看到了对于栈的运用,之前在响应式原理中也有提到过,但是在这里是一个十分典型的场景,也可以说是栈这个数据结构的最佳实践之一。
基本上你在社区中很多的框架或者优秀库中,都能看到栈的相关应用的影子,可以说是一个相当有用的一种数据结构。
钩子
我们在 parseHTML 的 options 中看到了钩子的应用,其实不止是这里有用到这种思想。通过 parseHTML 对外暴露的钩子函数 start、end、chars、comment 可以很方便的让使用者钩入到 parseHTML 的执行逻辑当中,相信你也感受到了,这是一种很有简单,但是确实很实用的思想。当然,这种思想本身,也常常和插件化设计方案或者叫微内核的架构设计一起出现;针对于不同的场景,可以有更复杂一些的实现,进而提供更加强大的功能,例如在 webpack 中,底层的 tapable 库,本质也是这种思想的应用。
正则表达式
在整个的parser过程中,我们遇到了很多种使用正则的场景,尤其是在 github.com/vuejs/vue/b… 这里:
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${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
这里边还是包含了很多种正则的使用,也有正则的动态生成。正则本身有简单的,有复杂的,如果你不能很好的理解这里的正则,推荐你去看精通正则表达式这本书,相信看过之后,你会收获很多。
其他小Tips
- 目录 模块拆分,依旧值得我们好好学习
- ast 优化操作,虽然上边没有详细分析,但是在源码中还是专门去做了 ast 优化相关的事情的
- 简单工厂模式的使用 Creator
- staticRenderFns,作用是啥,为啥会有它
- 缓存技术的再次利用,提升性能
- 避免重复处理,各种标记的运用
- 因为涉及到HTML解析,所以还是有必要了解下 HTML 规范的,以及常规的浏览器解析 HTML 的容错处理,源码中的一些工具也有体现 github.com/vuejs/vue/b…
- makeMap 的作用,在 Vue 中大量使用
滴滴前端技术团队的团队号已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。