文章目录
- 一、模板编译简介
- 二、体验模板编译的结果
- 三、Vue Template Explorer
- 四、编译的入口函数
- 五、模板编译过程
- 5.1 compileToFunctions
- 5.2 compile
- 5.3 baseCompile
- 5.3.1 baseCompile-AST
- 5.3.2 baseCompile-parse
- 5.3.3 baseCompile-optimize
- 5.3.4 baseCompile-generate
- 5.4 模板编译过程总结
一、模板编译简介
- 模板编译的主要目的是将模板 (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对象的成员可省略thiswith (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 = renderoptions.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 || baseWarndelete options.warn...// check cache// 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回const key = options.delimiters? String(options.delimiters) + template: templateif (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函数传递过来的optionsconst 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 = errorscompiled.tips = tipsreturn 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) returnisStaticKey = 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}
}