模板编译
模板编译的主要目的是将模板 (template) 转换为渲染函数 (render)
<div>
<h1 @click="handler">title</h1>
<p>some content</p>
</div>
渲染函数 render
render (h) {
return h('div', [
h('h1', { on: { click: this.handler} }, 'title'),
h('p', 'some content')
])
}
模板编译的作用
Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
用户只需要编写类似 HTML 的代码 - Vue 模板,通过编译器将模板转换为返回 VNode 的 render 函数
.vue 文件会被 webpack 在构建的过程中转换成 render 函数
带编译器版本的 Vue.js 中,使用 template 或 el 的方式设置模板
<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>
编译后 render 输出的结果
(function anonymous() {
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_m(0), // 处理静态内容
_v(" "), // 创建文本节点,换行会生成一个空格的文本节点
_c("p", [_v(_s(msg))]), // _s 是 toString()
_v(" "),
_c("comp", { on: { myclick: handler } }),
],
1
);
}
});
_c 是 createElement() 方法,定义的位置 instance/render.js 中
相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
把 template 转换成 render 的入口 src\platforms\web\entry-runtime-with-compiler.js
// instance/render-helps/index.js
target._v = createTextVNode
target._m = renderStatic
// core/vdom/vnode.js
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 在 instance/render-helps/render-static.js 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.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree
}
把 template 转换成 render 的入口 src\platforms\web\entry-runtime-with-compiler.js
Vue Template Explorer
vue-template-explorer
Vue 2.6 把模板编译成 render 函数的工具
vue-next-template-explorer
Vue 3.0 beta 把模板编译成 render 函数的工具
模板编译过程
解析、优化、生成
编译的入口
src\platforms\web\entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
......
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
......
)
compileToFunctions() 执行过程,生成渲染函数的过程
compileToFunctions: 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 = extend({}, options)
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 对象,如果有直接返回
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码
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. 把字符串形式的js代码转换成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. 缓存并返回res对象(render, staticRenderFns方法)
return (cache[key] = res)
}
}
complie(template, options): src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
// baseOptions 平台相关的options
// src\platforms\web\compiler\options.js 中定义
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (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
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
baseCompile(template.trim(), finalOptions): src\compiler\index.js
/* @flow */
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 {
// 把模板转换成 ast 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns
}
})
什么是抽象语法树
抽象语法树简称 AST(Abstract Syntax Tree)
使用对象的形式描述树形的代码结构
此处的抽象语法树是用来描述树形结构的 HTML 字符串
为什么要使用抽象语法树
模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
标记模板中的静态内容,在 patch 的时候直接跳过静态内容
在 patch 的过程中,静态内容不需要对比和重新渲染
解析 - parse
解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码
字符串。
src\compiler\index.js
const ast = parse(template.trim(), options)
//src\compiler\parser\index.js
parse()
查看得到的 AST tree
结构化指令的处理
v-if 最终生成单元表达式
// src\compiler\parser\index.js // structural directives
// 结构化的指令
// v-for
processFor(element)
processIf(element)
processOnce(element)
// src\compiler\codegen\index.js
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
// 最终调用 genIfConditions 生成三元表达式
v-if 最终编译的结果
ƒ anonymous( ){
with(this){
return _c('div',{attrs:{"id":"app"}},[
_m(0),
_v(" "),
(msg)?_c('p',[_v(_s(msg))]):_e(),_v(" "),
_c('comp',{on:{"myclick":onMyClick}})
],1) }
}
v-if/v-for 结构化指令只能在编译阶段处理,如果我们要在 render 函数处理条件或循环只能使用 js 中的 if 和 for
Vue.component('comp', {
data: () {
return {
msg: 'my comp'
} },
render (h) {
if (this.msg) {
return h('div', this.msg)
}
return h('div', 'bar')
}
})
优化 - optimize
优化抽象语法树,检测子节点中是否是纯静态节点
一旦检测到纯静态节点,例如:
hello整体是静态节点
永远不会更改的节点
提升为常量,重新渲染的时候不在重新创建节点
在 patch 的时候直接跳过静态子树
// src\compiler\index.js
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 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.
*
* 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;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记静态节点
markStatic(root)
// second pass: mark static roots.
// 标记静态根节点
markStaticRoots(root, false)
}
生成 - generate
// src\compiler\index.js
const code = generate(ast, options)
// src\compiler\codegen\index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
// 把字符串转换成函数
// src\compiler\to-function.js
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
} }
1