來源 劉濤
Vue
的核心可以分為三個大塊:數據處理和雙向綁定、模板編譯、虛擬dom。
前面我們對第一部分的主要內容雙向綁定做了一個分析講解,接下來我們說一說模板編譯。
這一部分的內容比較多,也比較復雜。由於所涉及的情況太多了,我也不可能把每一種情況都覆蓋到。
盡量做到既不啰嗦,又能分享更多的內容。前面我們也提到過,模板編譯分為三個階段:
生成ast
、優化靜態內容、生成render
。
官方文檔中提到render
比template
更加底層,許多人對render
函數都不是很明白,
相信通過這一部分的分析,你可以對render
基本做到了如指掌。
如果你在創建對象時直接傳入render
函數,模板編譯這一步就可以直接跳過,這樣效率肯定更高,
(react就是不用模板編譯的, 如果vue比react效率高,那就是因為需要編譯jsx?)
但同時我們編寫代碼的難度會增加很多。實際開發過程中,根據需要,恰當選擇。
Vue
對象上有一個全局函數compile
,在src/entries/web-runtime-with-compiler.js
中。
import { compileToFunctions } from 'web/compiler/index' ... ... ... Vue.compile = compileToFunctions
該方法來自src/platforms/web/compiler/index
文件。
import { isUnaryTag, canBeLeftOpenTag } from './util' import { genStaticKeys } from 'shared/util' import { createCompiler } from 'compiler/index' import modules from './modules/index' import directives from './directives/index' import { isPreTag, mustUseProp, isReservedTag, getTagNamespace } from '../util/index' export const baseOptions: CompilerOptions = { expectHTML: true, modules, directives, isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) } const { compile, compileToFunctions } = createCompiler(baseOptions) export { compile, compileToFunctions }
該文件中主要定義了一個baseOptions
,它主要保存了解析模板時和平台相關的一些配置。
對應的src/platforms/weex/compiler/index
中也有一份名稱一樣的配置。
這里我們簡單說說web
相關的這些配置都是什么意思。
-
expectHTML
。目前具體還不是很明白,weex
中沒有改項,從字面意思來看,應該是是否期望HTML
。 -
modules
。包括klass
和style
,對模板中類和樣式的解析。 -
directives
。這里包括model
(v-model
)、html
(v-html
)、text
(v-text
)三個指令。 -
isPreTag
。是否是pre
標簽。 -
isUnaryTag
。是否是單標簽,比如img
、input
、iframe
等。 -
mustUseProp
。需要使用props
綁定的屬性,比如value
、selected
等。 -
canBeLeftOpenTag
。可以不閉合的標簽,比如tr
、td
等。 -
isReservedTag
。是否是保留標簽,html
標簽和SVG
標簽。 -
getTagNamespace
。獲取命名空間,svg
和math
。 -
staticKeys
。靜態關鍵詞,包括staticClass,staticStyle
。
上面這些方法或者屬性,在編譯模板時會用到。
這里知識簡單的列出來它們的用途,方便看一眼。
我們來看createCompiler
函數的實現。
export function createCompiler (baseOptions: CompilerOptions) { const functionCompileCache: { [key: string]: CompiledFunctionResult; } = Object.create(null) // compile 函數的實現 // compileToFunctions 函數的實現 return { compile, compileToFunctions } }
該函數只是compile
和compileToFunctions
的簡單封裝,開始定義了functionCompileCache
,
它用來緩存編譯之后的模板,方便之后復用。
因為compileToFunctions
里面調用了compile
,所以我們先看一下compile
。
function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } if (options) { // 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), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled }
我們從上往下,依次看看它都做了哪些事兒。
它接收兩個參數template
和options
,template
不用過多解釋,
options
在內部主要是用戶自己定義的delimiters
。
finalOptions
繼承自我們上面提到的baseOptions
,
並添加了一個搜集錯誤的warn
方法,然后合並了options
傳入的各種配置選項。
modules
和directives
合並方法不同是因為modules
是數組,而directives
是一個對象。
baseCompile
中執行的就是模板編譯的三個重要步驟,后面我們會詳細講解。
最終,返回編譯之后的對象。
function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = options || {} ... // check cache const key = options.delimiters ? String(options.delimiters) + template : template if (functionCompileCache[key]) { return functionCompileCache[key] } // compile const compiled = compile(template, options) // check compilation errors/tips if (process.env.NODE_ENV !== 'production') { if (compiled.errors && compiled.errors.length) { warn( `Error compiling template:\n\n${template}\n\n` + compiled.errors.map(e => `- ${e}`).join('\n') + '\n', vm ) } if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(msg => tip(msg, vm)) } } // turn code into functions const res = {} const fnGenErrors = [] res.render = makeFunction(compiled.render, fnGenErrors) const l = compiled.staticRenderFns.length res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors) } 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 ) } } return (functionCompileCache[key] = res) }
compileToFunctions
函數中,首先從緩存中獲取編譯結果,沒有則調用compile
函數來編譯。
在開發環境,我們在這里會拋出編譯過程中產生的錯誤,
最終返回一個含有render
函數,和staticRenderFns
數組的對象,並把它放在緩存中。
這里我們使用makeFunction
來創建函數。
function makeFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
很簡單,就是利用了我們new Function
,並搜集了錯誤。
compile
和compileToFunctions
兩個方法的不同之處有以下幾點。
1、 compile
返回的結果中render
是字符串,staticRenderFns
是字符串組成的數組,而compileToFunctions
中把它們變成了函數。
2、 compile
返回的結果中,有模板生成的ast
和搜集到的錯誤。而compileToFunctions
對其結果進行了一些處理。