模板编译
模板编译的主要目的是将模板(template)转换为渲染函数(render)
vue中使用模板编译的必要性
Vue 2.0需要用到VNode描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似HTML代码的Vue模板,通过编译器将模板转换为可返回VNode的render函数。
模板编译
带编译器的版本中,可以使用template或el的方式声明模板。
<div id="app">
<h1>Vue模板编译</h1>
<p>{{name}}</p>
<Com1></Com1>
</div>
<script>
// 1.声明Com1组件构造函数VueComponent
// 2.全局配置选项加上了一个components:{Com1}
Vue.component('Com1', { template: '<div>component</div>' })
const app = new Vue({ el: '#app', data: {name:'lau'} }); // 创建实例
console.log(app.$options.render); // 输出render函数
</script>
// 输出
ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[
_c('h1',[_v("Vue模板编译")]),_v(" "),
_c('p',[_v(_s(name))]),_v(" "),
_c('com1')],1)}
}
_c返回vnode,就是createElement
_v 创建文本节点
_s 格式化函数
其他的helpers:src/core/instance/render-helper/index.js
模板编译流程
compileToFunctions()
src/platforms/web/entry-runtime-with-compiler.js
在$mount方法中会判断是否存在template或el选项,存在的话就会直接进行模板的编译。
if (template) {
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()
是createCompiler(baseOptions)
这个工厂函数的返回结果
// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
编译过程
src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile ( // 创建一个编译器
template: string,
options: CompilerOptions
): CompiledResult {
// parse作用是将字符串模板编译成AST(抽象语法树)
// AST 就是js对象,类似VNODE
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options) // 添加static,staticRoot,用于判断是否是静态(不含data或指令)的,即要不要更新
// 优化 减少更新耗时
// 虚拟DOM中的patch,可以跳过静态子树
}
// 代码生成,AST转换成代码字符串'function(){}'
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
从上面的代码可以了解到模板编译的过程共有三个阶段:解析、优化和生成。
parse()
- 解析
解析器将模板解析为抽象语法树AST,只有将模板解析成AST后,才能基于它做优化或者生成代码字符串。
解析器内部分了HTML解析器、文本解析器和过滤器解析器,最主要是HTML解析器,核心算法说明:
src/compiler/parser/index.js
// 解析html是parse的关键,在这个过程中会用到parseText和parseFilter
parseHTML(tempalte, {
start(tag, attrs, unary){
...
// 结构性指令的处理 v-if,v-for等
processFor(element)
processIf(element)
processOnce(element)
...
}, // 遇到开始标签的处理
end(){},// 遇到结束标签的处理
chars(text){},// 遇到文本标签的处理
comment(text){}// 遇到注释标签的处理
})
optimize()
- 优化
优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节点。
标记静态子树的好处:
- 每次重新渲染,不需要为静态子树创建新节点
- 虚拟DOM中patch时,可以跳过静态子树
src/compiler/optimizer.js
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.
// 标记静态属性static
markStatic(root)
// second pass: mark static roots.
// 标记根节点静态属性staticRoot
markStaticRoots(root, false)
}
generate()
- 代码生成
将AST转换成渲染函数中的内容(代码字符串)
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
}
}
此时生成的render函数是字符串形式,需改成function,执行createCompileToFunctionFn()
src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
...
res.render = createFunction(compiled.render, fnGenErrors)
...
}
createFunction()
就是直接new了一个Function
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
v-if指令的实现
<div id="app">
<h1>Vue模板编译</h1>
<p v-if="name">{{name}}</p>
<Com1></Com1>
</div>
生成的AST中就会存在上图两个属性。
在parse()
阶段,processIf(element)
中为ast新增这两个属性。
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp // 添加了if属性
addIfCondition(el, { // 添加了ifCondition属性
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
把断点点在src/compiler/index.js中的
const code = generate(ast, options)
可以看到生成的代码字符串为:
"with(this){return _c('div',{attrs:{"id":"app"}},[_c('h1',[_v("Vue模板编译")]),_v(" "),
(name)?_c('p',[_v(_s(name))]):_e(),_v(" "), // 可以看到v-if对render函数的影响只是多了个三元表达式的判断(if-else就是三元表达式的嵌套),如果表达式name存在,就创建这个节点,否则创建空节点
_c('com1')],1)}"
这个v-if代码是在genIf()
这个函数中生成
genIf()
src/compiler/codegen/index.js
export function genIf (){
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) { // 如果存在表达式,返回一个字符串
return `(${condition.exp})?${
genTernaryExp(condition.block) // 嵌套
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
研究指令,看ast最终怎么受影响,其次就是对代码生成的影响(在src/compiler/codegen/index.js中的generate()
方法中查看)。