模板编译介绍
模板编译的主要目的是将模板(template)转换为渲染函数(render)。
模板编译的作用
- Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode (直接写render,调用h函数)比较复杂
- 用户只需要编写类似 HTML 的代码 - Vue.js 模板,通过编译器将模板转换为范围 VNode 的render函数
- .vue文件(SFC)会被webpack在构建的过程中转换成render函数
- 内部通过 vue-loader 实现
体验模板编译的结果
<div id="app">
<h1>Vue<span>模板编译过程</span></h1>
<p>{{ msg }}</p>
<comp @myclick="handler"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: "#app",
data: {
msg: 'Hello compiler'
},
methods: {
handler() {
console.log('test')
}
}
});
console.log(vm.$options.render)
</script>
找到模板
入口文件 entry-runtime-with-compiler.js 中会先判断用户是否定义了render(当前没有)。
然后判断是否定义了 template 选项(当前没有)。
然后判断是否定义了 el 选项(当前有)。
然后获取 el 的 outerHTML 作为模板(template)。
然后通过 compileToFunctions把 template 转换为 render 函数。
编译生成的render函数
格式化后的打印结果:
(function () {
// with:在代码块中使用成员时可以省略this
with (this) {
return _c(
"div", // tag
{ attrs: { id: "app" } }, // data
[
_m(0),
_v(" "),
_c("p", [_v(_s(msg))]),
_v(" "),
_c("comp", { on: { myclick: handler } }),
], // children
1 // children处理方式
);
}
});
- _m用于渲染静态内容,在处理模板的过程中,会对静态的内容做优化的处理。
- 当前处理的h1标签
- _v 创建空白的文本节点
- 当前处理的p标签前后的换行
- _c 创建vnode
- p标签只有文本内容,所以传两个参数,第二个参数是文本内容(会被包裹成数组形式的children)
- comp组件有事件没有内容,所以传两个参数,第二个参数是数据属性(data)
- _s 把用户输入的数据转换成字符串
- 它对几种特殊情况做了判断处理,不是JS原生的toString方法
- 如果是纯文本,不会调用_s
编译生成的下划线开头的函数的位置
- _c createElement 函数
- src\core\instance\render.js
// 对编译生成的 render 进行渲染的方法
// _c是在 template 选项转换成的 render 函数中调用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
- _m/_v/_s
- src\core\instance\render.js -> src\core\instance\render-helpers\index.js
// 这里的方法都和渲染相关,将来在编译的时候使用
// 在把模板编译成render函数时,render函数会调用这些方法
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString // 将属性的值转化为字符串类型
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic // 渲染静态内容
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode // 创建文本虚拟节点
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
Vue Template Explorer
Vue Template Explorer(Vue 2.x) 是一个在线工具。
可以把HTML模板转换成render函数。
可以通过它来学习render函数。
tempalte模板:
<div id="app">
{{ msg }}
换行
空 格
</div>
转换结果:
function render() {
with (this) {
return _c(
"div",
{
attrs: {
id: "app",
},
},
[_v("\n " + _s(msg) + "\n \n \n 换行\n \n \n 空 格\n")]
);
}
}
Vue 2 的render会原封不动的保留模板中的空格和换行(\n)
- 尽管对展示来说没有任何意义,只会占用内存
- 开发时可以把这些无意义的空格和换行去掉,从而提高性能
Vue 3 Template Explorer 的 render 已经移除了空白,不用考虑这个问题,Vue 3 转换结果:
import {
toDisplayString as _toDisplayString,
createVNode as _createVNode,
openBlock as _openBlock,
createBlock as _createBlock,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createBlock(
"div",
{ id: "app" },
_toDisplayString(_ctx.msg) + " 换行 空 格 ",
1 /* TEXT */
)
);
}
// Check the console for the AST
模板编译的入口
入口文件 entry-runtime-with-compiler.js 中通过compileToFunctions把模板编译成render,并返回。
compileToFunctions 是由 createCompiler 生成,传入了和web平台相关的选项。
// src\platforms\web\compiler\index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
// src\platforms\web\compiler\options.js
export const baseOptions: CompilerOptions = {
expectHTML: true, // 期望的是HTML的内容
modules, // 模块
directives, // 指令
isPreTag, // 是否是pre标签
isUnaryTag, // 是否是自闭合标签
mustUseProp,
canBeLeftOpenTag,
isReservedTag, // 是否是HTML的保留标签
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
模块:处理 类样式、行内样式 以及 处理和v-if一起使用的v-model
// src\platforms\web\compiler\modules\index.js
import klass from './class'
import style from './style'
import model from './model'
export default [
klass,
style,
model
]
指令:处理 v-model、v-text、v-html 指令
// src\platforms\web\compiler\directives\index.js
import model from './model'
import text from './text'
import html from './html'
export default {
model,
text,
html
}
createCompiler
createCompiler在 src\compiler\index.js
中定义,是和平台无关的代码。
它又是通过一个函数(createCompilerCreator)返回,传入了一个核心的函数(baseCompile)。
baseCompile 接收两个参数:
- template 模板
- options 合并后的选项
baseCompile 内部做了3件事情:
- 把模板编译成AST(抽象语法树)
- 优化抽象语法树
- 把抽象语法树转换成字符串形式的JS代码
稍后再来看这个方法
createCompilerCreator
createCompilerCreator 中返回了 createCompiler 函数。
createCompiler 函数中定义了 compile 函数。
compile 接收两个参数:
- template 模板
- options 用户传入的选项
createCompilerCreator 内部会将 和平台相关的选项(baseOptions) 与 用户传入的选项 进行合并。
然后调用 baseCompile 并传递合并后的选项。
这是通过函数返回一个函数的目的:
- 把两种选项都准备好。
- 最后调用 baseCompile 去编译模板。
createCompiler 最终返回并创建了 compileToFunctions 函数。
compileToFunctions 就是模板编译的入口,也是通过一个函数(createCompileToFunctionFn)创建的。
稍后来看。
总结过程
- 完整版的入口中调用了 compileToFunctions 把模板编译成render函数
- compileToFunctions(template, {} ,this) 是由 createCompiler 生成的
- createCompiler(baseOptions) 是由 createCompilerCreator 生成的
- 接收和平台相关的选项参数 baseOptions
- 定义了 compile 函数
- compile(template, options)
- 接收两个参数
- template 模板
- options 用户传入的选项
- 定义:
- 内部首先会把平台相关的选项(baseOptions) 和 用户传入的选项(options ) 合并:finalOptions
- 然后调用 baseCompile 把 finalOptions 传入
- 最终返回这个 baseCompile 返回的对象(compiled)
- 最终返回 compile 和 compileToFunctions 函数
- compileToFunctions 是整个模板编译的入口,它是由 createCompileToFunctionFn 生成
- createCompileToFunctionFn(compile)
- 接收 createCompiler 定义的 compile 函数作为参数
- 内部定义了 compileToFunctions 函数并返回。
- createCompilerCreator(function baseCompile(){})
- 接收一个 baseCompile 函数作为参数
- baseCompile(template, finalOptions)
- 接收两个参数
- template 模板
- finalOptions 合并之后的选项
- 它是模板编译的核心函数,内部主要做了3件事情:
- parse:把模板解析成AST(抽象语法树)
- optimize:优化抽象语法树
- generate:把优化后的抽象语法树转换成字符串形式的JS代码
- 最终返回一个对象(compiled)
- 最后返回 createCompiler
模板编译的过程
compileToFunctions 入口函数
src\compiler\create-compiler.js
中 createCompilerCreator 返回的 createCompiler 最终返回了 compileToFunctions。
compileToFunctions 是通过 createCompileToFunctionFn(compile) 生成的。
所以 compileToFunctions 是在 src\compiler\to-function.js
的 createCompileToFunctionFn 中定义并返回的。
compileToFunctions 的核心就是:
- 在缓存中找编译结果,如果有就直接返回
- 没有的话,开始编译,并且把编译的字符串形式的JS代码转化成函数形式
- 最后缓存并且返回
// src\compiler\to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
// 创建一个不带原型的对象
// 目的是通过闭包缓存编译之后的结果
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 克隆 options(Vue中的options选项)
// 目的是防止污染 Vue 中的 options
options = extend({}, options)
// 获取 warn 函数:开发环境中用于在控制台发送警告
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// check cache
// 1. 判断缓存中是否有编译的结果(读取缓存中的 CompiledFunctionResult 对象)
// 如果有,直接把编译的结果返回,不需要编译
// key 是模板
// options.delimiters
// 只有完整版的Vue才有,只有编译的时候才会使用到
// 它的作用是改变插值表达式的符号(详细查看官方文档)
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 2. 调用 compile 把模板编译成编译对象{render, staticRenderFns, errors, tips}
// render 存储的是字符串形式的js代码
// errors 和 tips 是辅助性属性,在编译模板过程中收集遇到的错误和信息,在这里把这些信息打印出来
const compiled = compile(template, options)
// check compilation errors/tips
// 打印错误和信息
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}
// turn code into functions
const res = {}
const fnGenErrors = []
// 3. 调用 createFunction 把字符串形式的js代码转换成函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
// 打印错误和信息
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}
// 4. 把编译的结果缓存并返回
return (cache[key] = res)
}
}
createFunction
// src\compiler\to-function.js
function createFunction (code, errors) {
try {
// 通过 new Function 把字符串转换成函数
return new Function(code)
} catch (err) {
// 如果失败还会收集错误信息,并返回一个空函数
errors.push({ err, code })
return noop
}
}
compile
它的核心就是合并选项(baseOptions 和 options),调用 baseCompile 进行编译,最后记录错误和信息,返回编译好的对象。
// src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
// baseOptions: 和平台相关的选项
return function createCompiler (baseOptions: CompilerOptions) {
/**
* 把模板编译成字符串形式的JS代码
* @param {*} template 模板
* @param {*} options 用户传入的选项(调用compileToFunctions时传入的)
*/
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// 以 baseOptions 为原型创建 finalOptions
// finalOptions 的作用是用来合并 baseOptions 和 options
const finalOptions = Object.create(baseOptions)
// errors 和 tips 用于存储编译过程中出现的错误和信息
const errors = []
const tips = []
// 定义warn函数:用于把消息放入对应的数组中
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// 如果 options 存在,就合并 baseOptions 和 options
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length
warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
// 调用 baseCompile 返回对象 {render, staticRenderFns}
// render 存储的是字符串形式的js代码
// baseCompile 是模板编译的核心函数
// baseCompile 内部还会把编译遇到的错误和信息记录下来
// 调用 finalOptions.warn 收集 errors 和 tips
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
// 将 errors 和 tips 记录到 compiled 对应的属性
compiled.errors = errors
compiled.tips = tips
// 返回 compiled 对象
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
baseCompile - AST
在 compile 中合并完选项,开始调用 baseCompile 编译模板。
baseCompile 是模板编译的核心函数。
内部代码非常清晰,把不同功能的代码,拆分到不同的函数中进行处理,让代码的结构更清晰。
内部就做了3件事情:
- parse:把模板解析成AST(抽象语法树)
- optimize:优化抽象语法树
- generate:把优化后的抽象语法树转换成字符串形式的JS代码
// src\compiler\index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 调用 parse 函数把模板字符串转换成抽象语法树 AST
// 抽象语法树(AST):用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2. 调用 optimize 优化抽象语法树
optimize(ast, options)
}
// 3. 调用 generate 把抽象语法树转换成字符串形式的JS代码
const code = generate(ast, options)
// 最终返回一个对象
return {
ast,
// 渲染函数(这里是字符串形式的render,不是最终调用的render,最终还要通过 createFunction 转换成函数的形式)
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns
}
})
AST 抽象语法树
什么是抽象语法树
- AST:Abstract Syntax Tree
- 使用对象的形式描述树形的代码结构
- 对象中记录父子节点,形成树的结构
- 此处的抽象语法树是用来描述树形结构的 HTML 字符串
- 先把HTML转化成字符串,然后记录标签的必要属性,以及解析Vue中的一些指令并记录到 AST
为什么使用抽象语法树
- 模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
- 标记模板中的静态内容
- 静态内容:内容是纯文本的标签
- 标记模板中的静态内容,在 patch 的时候直接跳过静态内容
- 在 patch 的过程中静态内容不需要对比和重新渲染,从而优化性能
babel 对代码进行降级处理的时候,也是会把代码转化成 AST ,再把 AST 转化成降级之后的 JS 代码。
查看 AST 的工具
astexplorer 可以查看各种解析器生成的 AST。
可以选择语言(Vue) 和 解析器。
- @vue/compiler-core Vue 3 中的解析器
- vue-template-compiler Vue 2 中的解析器
属性介绍:
- type 记录节点的类型
- 1 - 标签
- 3 - 文本
- tag 标签名
- attrsList、attrsMap、rawAttrsMap 记录标签中的属性
- children 记录子节点
- parent 记录父节点(Vue中会生成,这里没显示)
- AST通过记录父子节点形成树的形式
- static 标签当前节点是静态的
parse 生成AST的过程
parse的作用是把模板字符串转换成AST对象。
这个过程比较复杂,Vue内部还借鉴了一个开源的库去解析HTML。
深入研究parse的过程所花费的收获和时间不成正比,所以这里只关注整体的执行流程。
parse 接收两个参数:
- template 模板字符串
- options 合并后的选项
// src\compiler\parser\index.js
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson (MPL-1.1 OR Apache-2.0 OR GPL-2.0-or-later)
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
// 这段注释介绍,parseHTML 借鉴了开源库 simplehtmlparser
// import ...
// 定义了一些匹配模板字符串中内容的正则表达式
// 匹配标签中的属性,包括指令
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 = /^<!\[/
// ...
/**
* Convert HTML string to AST.
*/
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 1. 解析 options
// ...
// 定义了一些变量和函数
// ...
// 2. 调用 parseHTML 对模板解析
// 接收两个参数:
// template 模板字符串
// 一个包含选项中成员的对象 和 4个方法
// 这4个方法是解析过程中的回调函数
parseHTML(template, {
// 选项中的成员
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 解析过程中的回调函数
// 解析到开始标签时调用
start (tag, attrs, unary, start, end) {
// ...
// createASTElement 创建 AST 对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// 给 AST 的各种属性赋值
//...
if (!inVPre) {
// processPre 处理 v-pre 指令
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 处理结构化的指令
// v-for
processFor(element)
// v-if
processIf(element)
// v-once
processOnce(element)
}
// ...
},
// 解析到结束标签时调用
end (tag, start, end) {
// ...
},
// 解析到文本内容时调用
chars (text: string, start: number, end: number) {
// ...
},
// 解析到注释标签时调用
comment (text: string, start, end) {
// ...
}
})
// 最后返回root,root内存储的就是解析好的AST对象
return root
}
advance
// src\compiler\parser\html-parser.js
function advance (n) {
// 记录当前的位置
index += n
// 截取剩余的内容
html = html.substring(n)
}
handleStartTag
// src\compiler\parser\html-parser.js
// 解析标签和属性,并调用 start 方法
function handleStartTag (match) {
//... 解析标签和属性
if (options.start) {
// 当对标签处理完毕之后
// 最终调用了 options.start 方法
// 并传递解析好的标签名、属性、是否是一元(unary)标签(自闭合标签)、起止位置
// start 内部调用 createASTElement 创建 AST 对象
options.start(tagName, attrs, unary, match.start, match.end)
}
}
createASTElement
// src\compiler\parser\index.js
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
// 返回一个 AST 对象
return {
type: 1,
tag,
// 标签的属性数组
attrsList: attrs,
// makeAttrsMap 把 attrs 转换成对象的形式
// 方便后续使用
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
processPre
// src\compiler\parser\index.js
function processPre (el) {
// getAndRemoveAttr 获取 v-pre 指令,然后从 AST 中移除对应的属性
if (getAndRemoveAttr(el, 'v-pre') != null) {
// 如果有v-pre,通过 pre 属性记录下来
el.pre = true
}
}
getAndRemoveAttr
// src\compiler\helpers.js
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
let val
// 获取标签上的属性
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
if (removeFromMap) {
// 移除标签上的属性
delete el.attrsMap[name]
}
// 返回属性的值
return val
}
processIf
// src\compiler\parser\index.js
// 处理 v-if 指令
function processIf (el) {
// 获取 v-if 指令的值(表达式),并移除 v-if 属性
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// 如果有值(表达式),存储到 if 属性上
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// 否则处理 v-else 和 v-else-if
// 都是相似的处理过程,都是记录指令相关的数据
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
addIfCondition
// src\compiler\parser\index.js
// 把 v-if 中的表达式和对应的 AST 对象,存储到 ifConditions 数组中
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
总结
parse 函数处理的过程中,会依次遍历 HTML 模板字符串,把 HTML 模板字符串转换成 AST 对象(一个普通的对象)。
HTML 中的属性和指令都会记录在 AST 对象的相应属性上。
optimize 优化 AST
// src\compiler\optimizer.js
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
* 优化器的目的:遍历编译模板生成的AST并检测纯静态的子树,即DOM中不需要更改的部分
*
* Once we detect these sub-trees, we can:
* 一旦检测到这些子树,我们可以
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 1. 将它们提升为常量,这样我们就不再需要在每次重新渲染时为它们创建新的节点
* 2. Completely skip them in the patching process.
* 2. 在 patch 的过程中完全跳过它们
*/
// 优化的目的是为了标记 AST 中的静态节点
// 静态节点:对应的DOM子树永远不会发生变化(如纯文本的标签)
export function optimize (root: ?ASTElement, options: CompilerOptions) {
// 判断是否传递了 AST 对象
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记 root 中的所有静态节点
markStatic(root)
// second pass: mark static roots.
// 标记 root 中的静态根节点
markStaticRoots(root, false)
}
markStatic
标记静态节点
function markStatic (node: ASTNode) {
// 判断当前 astNode 是否是静态节点
node.static = isStatic(node)
if (node.type === 1) {
// 如果节点是元素,处理元素中的子节点
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
// 处理之前进行一些判断:
// 判断元素标签是否是保留标签(判断当前是否是组件)
// 如果是组件,不去把组件中的插槽内容标记成静态节点,避免:
// 1. 组件无法改变插槽节点
// 2. 静态插槽内容无法进行热重新加载
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
// 遍历 AST 对象的所有子节点
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
// 递归调用 markStatic 标记静态
markStatic(child)
if (!child.static) {
// 如果有一个 child 不是 static,当前 node 就不是 staic
node.static = false
}
}
// 处理条件渲染中的 AST 对象,处理类似遍历 children
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
isStatic
判断 astNode 是否是静态节点
function isStatic (node: ASTNode): boolean {
// 判断 AST 节点的类型
// 2:表达式(如插值表达式)
// 它的内容会发生变化,所以不是静态节点,直接返回false
if (node.type === 2) { // expression
return false
}
// 3:静态的文本内容,返回true
if (node.type === 3) { // text
return true
}
// 最后判断下面条件都满足就表示是一个静态节点
return !!(node.pre || ( // 如果是 pre
!node.hasBindings && // no dynamic bindings 没有动态绑定
!node.if && !node.for && // not v-if or v-for or v-else 不是这些指令
!isBuiltInTag(node.tag) && // not a built-in 不是内置组件
isPlatformReservedTag(node.tag) && // not a component 不是组件,是平台保留的标签
!isDirectChildOfTemplateFor(node) && // 不是v-for中的直接子节点
Object.keys(node).every(isStaticKey)
))
}
markStaticRoots
标记静态根节点。
静态根节点:
- 标签中包含子标签,并且没有动态内容(都是纯文本内容)。
- 如果标签中只包含纯文本内容,Vue中不会对它作优化(不会标记为静态根节点)。
- 因为这样优化的成本大于收益
function markStaticRoots (node: ASTNode, isInFor: boolean) {
// 判断 AST 描述的是否是 元素
if (node.type === 1) {
// 判断该节点是否是 静态的 或者 只渲染一次
if (node.static || node.once) {
// 标记该节点在 for 循环中是否是静态的
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 如果一个元素只有文本子节点,那这个元素不是静态根节点
// Vue 认为这种优化会带来负面的影响(优化成本大于收益)
// 例如这个div就不算静态根节点:<div>纯文本</div>
// 如果一个节点是静态的
// 并且不是“只有一个文本节点”
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 设置为静态根节点
node.staticRoot = true
return
} else {
// 否则不是
node.staticRoot = false
}
// 下面同markStatic类似
// 遍历子节点和条件渲染中的AST对象,递归调用 markStaticRoots
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
generate 把优化好的 AST 对象转化成 JS 代码
generate 接收两个参数:
- ast - 优化好的 AST 对象
- options - 合并好的选项
最终返回一个对象:
- render - 使用 with 包裹 AST 对象转化成的 JS 代码
- staticRenderFns - 存储静态根节点生成的字符串形式的代码
// src\compiler\codegen\index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 创建 CodegenState 对象:
// 代码生成过程中使用到的状态对象
const state = new CodegenState(options)
// 如果ast存在,调用 genElement 开始生成代码
// 否则 生成一个创建div的代码
const code = ast ? genElement(ast, state) : '_c("div")'
// 最后返回一个对象
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
CodegenState
export class CodegenState {
options: CompilerOptions;
warn: Function;
transforms: Array<TransformFunction>;
dataGenFns: Array<DataGenFunction>;
directives: { [key: string]: DirectiveFunction };
maybeComponent: (el: ASTElement) => boolean;
onceId: number;
staticRenderFns: Array<string>;
pre: boolean;
constructor (options: CompilerOptions) {
// CodegenState 存储了一些和代码生成相关的属性和方法
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
const isReservedTag = options.isReservedTag || no
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
this.onceId = 0
// 重点关注 staticRenderFns 和 pre
// staticRenderFns 用来存储静态根节点生成的字符串形式的代码
// 一个模板中可能有多个静态根节点,所以它是数组类型
this.staticRenderFns = []
// pre 记录当前处理的节点是否使用 v-pre 标记的
this.pre = false
}
}
render
genElement
generate 中最核心的就是 genElement,它是最终把 AST 转化成 代码的位置。
export function genElement (el: ASTElement, state: CodegenState): string {
// 判断是否有父节点
if (el.parent) {
// 记录pre,根据自身的pre和父节点的pre取值
// v-pre 标记的标签及子标签都是静态节点
el.pre = el.pre || el.parent.pre
}
// 如果当前是静态根节点,且没有被处理过(staticProcessed=false)
// genElement会被递归调用,这个判断用于防止重复处理这个节点
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
// 下面处理 once for if 指令,把它们转化成render函数中相应的代码
} 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)
// 如果是template标签并且不是slot和pre(也就是它不是静态的)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// 生成它内部的子节点以及对应的代码
// 如果没有子节点返回'void 0',也就是undefined
return genChildren(el, state) || 'void 0'
// 处理slot标签
} 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))) {
// 把AST对象中的相应属性,转换成 createElement 所需要的 data 对象的字符串形式
data = genData(el, state)
}
// 处理子节点
// 把el中的子节点,转化成 createElement中需要的数组形式
// 也就是第三个参数 children
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
}
}
genData
genData 最终拼接了一个普通对象的字符串形式
根据 el 中的属性,拼接 createElement 中使用的相应的 data
最后返回 data。
genChildren
把子节点数组中的每一个AST对象,通过调用 genNode(当前使用) 生成对应的代码形式。
把生成的代码数组,通过逗号拼接成字符串,包裹成一个字符串形式的数组。
最后拼接 createElement 的第四个参数(如何去拍平数组)并返回。
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
// 先判断 AST 对象是否有子节点
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}`
}
// 这里是这个函数核心的处理过程
// 首先获取如何去处理数组,也就是 createElement中的第四个参数(数组是否需要被拍平)
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
// map遍历数组中的每一个元素
// 使用gen函数对每一个元素进行处理并返回
// 然后用逗号把元素处理返回的数组拼接成一个字符串,包裹到一个数组中
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
genNode
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
// 当前 AST 节点是标签
// 继续调用 genElement 处理当前的子节点
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
// 如果是注释节点,生成注释节点对应的代码
return genComment(node)
} else {
// 处理文本节点
return genText(node)
}
}
genComment
export function genComment (comment: ASTText): string {
// _e:createEmptyVNode
// JSON.stringify 用于给字符串加上引号 hello -> "hello"
return `_e(${JSON.stringify(comment.text)})`
}
genText
export function genText (text: ASTText | ASTExpression): string {
// _v:createTextVNode
// type=2 表达式,直接返回该表达式(已经使用_s转化成了字符串)
// transformSpecialNewlines 用于把代码中特殊的换行(unicode形式的)进行修正,防止意外情况
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
staticRenderFns 生成静态根节点
staticRenderFns 存储的生成的静态根节点的渲染函数。
staticRenderFns 数组是在 genElement 中的 genStatic 方法中添加元素的。
staticRenderFns 定义为数组的原因:
- 一个模板中可能会有多个静态节点(子节点都是静态节点的静态根节点)
- staticRenderFns 先把每一个静态子树对应的代码进行存储
genStatic
// src\compiler\codegen\index.js
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
// 首先标记当前节点已经被处理过(防止重复处理)
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
// 把 state.pre 暂存到一个变量中
const originalPreState = state.pre
// 获取 AST 对象的pre属性,并赋值给 state.pre
if (el.pre) {
state.pre = el.pre
}
// 这里给 staticRenderFns 添加元素
// 把静态根节点,转化成生成vnode的对应JS代码
// staticProcessed 标记为 true
// genElement 会直接进入到 component or element 处理过程中
// staticRenderFns 定义为数组的原因:
// 一个模板中可能会有多个静态节点(子节点都是静态节点的静态根节点)
// 这里先把每一个静态子树对应的代码进行存储
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
// 当处理完当前节点后,再把原始状态中的 state.pre 还原
state.pre = originalPreState
// 最后返回当前节点对应的代码
// _m:renderStatic 渲染静态内容
// 传入的是当前节点在 staticRenderFns 中对应的索引
// _m 会获取 staticRenderFns 中存储的对应的代码
// 这里返回的是字符串类型的函数,但最终会被转化为函数调用
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
_m函数:renderStatic
_m 函数在 installRenderHelpers 函数中被定义为 renderStatic 函数。
// src\core\instance\render-helpers\render-static.js
/**
* Runtime helper for rendering static trees.
*/
export function renderStatic (
index: number,
isInFor: boolean
): VNode | Array<VNode> {
const cached = this._staticTrees || (this._staticTrees = [])
// 从缓存中获取生成的静态根节点对应的代码
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {
return tree
}
// otherwise, render a fresh tree.
// 如果没有,就从 staticRenderFns 获取对应的函数并调用
// 此时就生成了 vnode 节点,并把结果缓存
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
// 调用 markStatic 把当前返回的节点标记为静态的
markStatic(tree, `__static__${index}`, false)
return tree
}
markStatic
// src\core\instance\render-helpers\render-static.js
function markStatic (
tree: VNode | Array<VNode>,
key: string,
isOnce: boolean
) {
// 如果vnode是一个数组,递归调用markStaticNode
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
if (tree[i] && typeof tree[i] !== 'string') {
markStaticNode(tree[i], `${key}_${i}`, isOnce)
}
}
} else {
// 否者直接调用 markStaticNode,把vnode设置为静态的
markStaticNode(tree, key, isOnce)
}
}
function markStaticNode (node, key, isOnce) {
// 设置 isStatic 为true,表示是静态的
// patch函数会判断 isStatic 为 true,不再对比差异,直接返回
// 因为静态节点不会发生变化,不需要被处理,这是对静态节点的优化。
// 如果静态节点已经被渲染到文档,那它不需要重新被渲染
node.isStatic = true
// 记录 key 和 isOnce
node.key = key
node.isOnce = isOnce
}
把字符串转化成函数的过程
baseCompile 方法只是返回了 AST 转化的 JS 字符串。
baseCompile 在 createCompilerCreator 中被调用。
createCompilerCreator 中定义的 createCompiler 最后返回了一个包含 compile 和 compileToFunctions(模板编译的入口函数) 的对象。
compileToFunctions 是 createCompileToFunctionFn 生成的,并接收了 compile 函数作为参数,并在内部定义。
compile 函数定义的内部调用了 baseCompile ,最终返回的是baseCompile 返回的结果(compiled)。
所以 createCompileToFunctionFn 内部使用了 baseCompile 返回的结果。
createCompileToFunctionFn
createCompileToFunctionFn 定义并返回的 compileToFunctions。
compileToFunctions 内部通过调用 createFunction 把 JS 字符串转化成函数。
接着遍历 staticRenderFns,把静态根节点中对应的每一个JS字符串转化为函数。
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// compile
// 2. 调用 compile 把模板编译成编译对象{render, staticRenderFns, errors, tips}
// render 存储的是字符串形式的js代码
// errors 和 tips 是辅助性属性,在编译模板过程中收集遇到的错误和信息,在这里把这些信息打印出来
const compiled = compile(template, options)
// ...
// 3. 调用 createFunction 把字符串形式的js代码转换成函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// ...
// 4. 把编译的结果缓存并返回
return (cache[key] = res)
}
至此模板编译的过程就讲完了。
模板编译过程-调试
通过查看源码了解:
- 模板编译是把模板字符串首先转换成 AST 对象
- 然后优化 AST 对象
- 优化的过程就是标记静态根节点
- 然后把优化好的 AST 对象转换成字符串形式的JS代码
- 最终把字符串形式的JS代码,通过 new Function 转换成匿名函数
- 这个匿名函数,就是最后生成的 render 函数。
模板编译最终就是把模板转换成 render 函数
<div id="app">
<h1>Vue<span>模板编译过程</span></h1>
<p>{{ msg }}</p>
<div>该div不是静态根节点</div>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: "#app",
data: {
msg: 'Hello compiler'
}
});
</script>
断点位置:
src\platforms\web\entry-runtime-with-compiler.js
入口文件调用 compileToFunctions 的位置。
- 这是模板编译的入口函数
src\compiler\create-compiler.js
调用 baseCompile 的位置
- 把模板字符串转换成 AST 、优化、生成 JS 代码的位置
src\compiler\to-function.js
调用 createFunction 的位置
- 把JS代码转化成函数的位置
查看生成的AST对象,优化后查看结果:
- h1 标签的 static 和 staticRoot 都为 true ,表示是静态节点,且是静态根节点
- duv 标签的 static 为true,staticRoot为false,表示它是静态节点,但不是静态根节点
查看 generator 生成的 JS 代码(render,staticRenderFns)
- 查看 h1 标签,它的 staticProcessed 属性为 true,被标记为处理完毕。
模板编译过程-总结
- compileToFunctions(template, options, vm) 是模板编译的入口函数
- 内部先从缓存中加载编译好的 render 函数
- 缓存中没有,调用 compile 开始编译
- compile(template, options)
- 首先合并选项
- 这是compile的核心
- 然后调用 baseCompile 编译模板
- 把模板和合并好的选项传递进去
- baseCompile(template.tirm(), finalOptions)
- 先完成模板编译核心的三件事情
- parse 把 template 字符串转换成 AST 对象
- 把 template 转换成 AST
- optimize 优化 AST,标记 AST 中的静态根节点
- 检测到静态根节点,设置为静态,不需要再每次重新渲染的时候重新生成节点(重绘)
- patch 阶段跳过静态根节点
- generator 把优化过后的 AST 转换成字符串形式 JS 代码
- compile 执行完毕,再次回到入口函数 compileToFunctions
- 继续把上一步中生成的字符串形式 JS 代码转换成函数
- 通过 createFunction,内部使用 new Function
- 当render 和 staticRenderFns 初始化完毕,挂载到 Vue 实例的 options 对应的属性中。
通过查看源码了解:
- Vue 模板编译的过程中会标记静态根节点,对静态根节点进行优化处理。
- 重新渲染的时候不需要处理静态根节点。因为它的内容不会改变。
- 另外在模板中不要写过多的无意义的空白和换行
- 否则生成的AST对象会保留这些空白和换行
- 它们都会被存储到内存中。
- 这些空白和换行对浏览器渲染来说是没有任何意义的。
- 代码规范中也有响应的约定。
知识点小记
- 模板编译的入口函数 compileToFunctions() 中的 parse 函数的作用是把模板解析成 AST 对象
- AST 对象称为抽象语法树,通过 AST 抽象语法树来描述 DOM 的树形结构,目的是基于 AST 优化生成的代码
- 模板编译的入口函数 compileToFunctions() 中的 optimize 函数的作用是标记 AST 中的静态根节点
- 静态根节点是标签中除了文本内容以外,还需要包含其它标签
- 静态根节点不会被重新渲染,patch 的过程中会跳过静态根节点
- 模板和插值表达式在编译的过程中都会被转换成对应的代码形式,不会出现在 render 函数中