熟悉 Vue 的同窗都知道,从 Vue2 开始,在实际运行的时候,是将用户所写的 template 转换为 render 函数,获得 vnode 数据(虚拟 DOM),而后再继续执行,最终通过 patch 到真实的 DOM,而当有数据更新的时候,也是靠这个进行 vnode 数据的 diff,最终决定更新哪些真实的 DOM。html
这个也是 Vue 的一大核心优点,尤大不止一次的讲过,由于用户本身写的是静态的模板,因此 Vue 就能够根据这个模板信息作不少标记,进而就能够作针对性的性能优化,这个在 Vue 3 中作了进一步的优化处理,block 相关设计。前端
因此,咱们就来看一看,在 Vue 中,template 到 render 函数,到底经历了怎么样的过程,这里边有哪些是值得咱们借鉴和学习的。vue
template 到 render,在 Vue 中实际上是对应的 compile 编译的部分,也就是术语编译器 cn.vuejs.org/v2/guide/in… 本质上来说,这个也是不少框架所采用的的方案 AOT,就是将本来须要在运行时作的事情,放在编译时作好,以提高在运行时的性能。node
关于 Vue 自己模板的语法这里就不详细介绍了,感兴趣的同窗能够看 cn.vuejs.org/v2/guide/sy… ,大概就是形以下面的这些语法(插值和指令):webpack
render 函数呢,这部分在 Vue 中也有着详细的介绍,你们能够参阅 cn.vuejs.org/v2/guide/re… ,简单来说,大概就是这个样子:git
那咱们的核心目标就是这样:github
若是你想体验,能够这里 template-explorer.vuejs.orgweb
固然 Vue 3 的其实也是能够的 https://vue-next-template-explorer ,虽然这里咱们接下来要分析的是 Vue 2 版本的。正则表达式
要想了解是如何作到的,咱们就要从源码入手,编译器相关的都在 github.com/vuejs/vue/t… 目录下,咱们这里从入口文件 index.js 开始:express
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 的实如今 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 的阶段已经完全完成。
接下来看看如何根据上述的 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 函数的完整过程。
要找到背后的缘由,咱们能够拆分为两个点:
这个问题其实尤大本人本身讲过,为何在 Vue 2 中引入 Virtual DOM,是否是有必要的等等。
来自方应杭的聚合回答:
这里有一些文章和回答供参考(也包含了别人的总结部分):
这个在官网框架对比中有讲到,原文 cn.vuejs.org/v2/guide/co…
固然,除了上述缘由以外,就是咱们在前言中提到的,模板是静态的,Vue 能够作针对性的优化,进而利用 AOT 技术,将运行时性能进一步提高。
这个也是为何 Vue 中有构建出来了不一样的版本,详细参见 cn.vuejs.org/v2/guide/in…
经过上边的分析,咱们知道在 Vue 中,template到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
复制代码
这里边仍是包含了不少种正则的使用,也有正则的动态生成。正则自己有简单的,有复杂的,若是你不能很好的理解这里的正则,推荐你去看精通正则表达式这本书,相信看过以后,你会收获不少。
滴滴前端技术团队的团队号已经上线,咱们也同步了必定的招聘信息,咱们也会持续增长更多职位,有兴趣的同窗能够一块儿聊聊。