模板編譯
模板編譯的主要目的是將模板(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()
方法中查看)。