文章目录
一、模板编译简介
- 模板编译的主要目的是将模板 (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.js 模板,通过编译器将模板转换为返回 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对象的成员可省略this
with (this) {
return _c(
"div", // tag标签,对应<div>
{ attrs: { id: "app" } }, // data描述tag,对应id="app"
[ // children设置tag子节点
_m(0), // 处理静态内容做优化处理,对应<h1>
_v(" "), // 创建空白的文本节点,对应<h1>和<p>之间的空白位置(换行)
// 创建<p>对应的vnode 第二个位置(数组包裹的文本的vnode节点)
_c("p", [_v(_s(msg))]), // 把用户输入数据转化为字符串(_s)
_v(" "),
_c("comp", { on: { myclick: handler } }), // 创建自定义组件对应的vnode
],
1 // 后续如何对children处理,将children拍平为一维数组
);
}
});
- _c 是 createElement() 方法,定义的位置 instance/render.js 中
- 相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
// instance/render-helps/index.js
target._v = createTextVNode
target._s = toString
target._m = renderStatic
// core/vdom/vnode.js
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// shared/util
// 将一个值转换为实际渲染的字符串
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: 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.
// 如果已经渲染了静态树,并且不在v-for里面,我们可以重用同样的树。
if (tree && !isInFor) {
return tree
}
// otherwise, render a fresh tree.
// 如果没有,从staticRenderFns这个数组中获取静态根节点对应的render函数调用
// 此时就生成vnode节点,把结果缓存
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
// 把当前返回的vnode节点标记为静态的
// 将来调用patch函数的时候,内部会判断如果当前vnode为静态,则不再对比节点差异
markStatic(tree, `__static__${index}`, false)
return tree
}
- 把 template 转换成 render 的入口 src\platforms\web\entry-runtime-with-compiler.js
三、Vue Template Explorer
把 html 模版转换成 render 函数的工具
- vue-template-explorer
- Vue 2.6 把模板编译成 render 函数的工具
- 在使用vue2.x 的模板时,标签内的文本内容尽量不要添加多余的空白
模板
<div id="app">
<select>
<option>
{{ msg }}
</option>
</select>
<div>
hello
</div>
</div>
转换结果
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('select', [_c('option', [_v("\n " + _s(msg) + "\n ")])]),
_c('div', [_v("\n hello\n ")])
])
}
}
- vue-next-template-explorer
- Vue 3.0 beta 把模板编译成 render 函数的工具
- vue3 编译后的 render 函数已经去除了标签内多余的空白
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", { id: "app" }, [
_createElementVNode("select", null, [
_createElementVNode("option", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]),
_createElementVNode("div", null, " hello ")
]))
}
// Check the console for the AST
四、编译的入口函数
- 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
...
)
五、模板编译过程
5.1 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 {
// 防止污染 vue 的 options 所以克隆一份
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
...
// 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)
...
// 3. 把字符串形式的js代码转换成js方法
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
...
// 4. 缓存并返回res对象(render, staticRenderFns方法)
return (cache[key] = res)
}
}
5.2 compile
- 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 {
// 合并 baseOptions 和 complice函数传递过来的options
const finalOptions = Object.create(baseOptions)
// 存贮编译过程中出现的错误和信息
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
...
}
finalOptions.warn = warn
// 通过 baseCompile 把模板编译成 render函数
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)
}
}
}
5.3 baseCompile
- src/compiler/index.js
// `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.
// `createCompilerCreator`允许创建使用替代解析器/优化器/代码生成的编译器,
// 例如SSR优化编译器。在这里,我们只是使用默认的部分导出一个默认的编译器。
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
}
})
5.3.1 baseCompile-AST
什么是抽象语法树
- 抽象语法树简称 AST (Abstract Syntax Tree)
- 使用对象的形式描述树形的代码结构
- 此处的抽象语法树是用来描述树形结构的 HTML 字符串
为什么要使用抽象语法树
- 模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
- 标记模板中的静态内容,在 patch 的时候直接跳过静态内容
- 在 patch 的过程中静态内容不需要对比和重新渲染
获取 AST
- 使用工具 AST explorer
5.3.2 baseCompile-parse
- 解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码字符串
- parse 函数内部处理过程中会依次去遍历 html 模板字符串,把其转换成 AST 对象,html 中的属性和指令(v-if、v-for 等)都会记录在 AST 对象的相应属性上
- src/compiler/index.js
// 把模板转换成 AST 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
- 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");
},
});
5.3.3 baseCompile-optimize
- src/compiler/index.js
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
- src/compiler/optimizer.js
- 优化抽象语法树,检测子节点中是否是纯静态节点(对应的 DOM 子树永远不会发生变化)
- 一旦检测到纯静态节点
- 提升为常量,重新渲染的时候不在重新创建节点
- 在 patch 的时候直接跳过静态子树
/**
- 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.
*/
// 优化的目的:标记抽象语法树的静态节点,即DOM中永远不需要改变的部分
// 当标记完静态子树后,将来就不需要进行渲染,在patch的时候直接跳过静态子树
// 一旦我们检测到这些子树,我们就可以做到:
// 1. 将它们提升为常量,这样我们就不再需要在每次重新渲染时为它们创建新的节点;
// 2. 在修补过程中完全跳过它们。
export function optimize (root: ?ASTElement, options: CompilerOptions) {
// 判断root,是否传递 AST 对象
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)
}
5.3.4 baseCompile-generate
- src/compiler/index.js
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
- src/compiler/codegen/index.js
- 把抽象语法树转换成字符串形式的 js 代码,生成 render 表达式
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 代码生成过程中使用到的状态对象
const state = new CodegenState(options)
// AST存在,调用genElement生成代码
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
}
}