一、編譯
-
模板到真實
DOM
渲染的過程,中間有一個環節是把模板編譯成render
函數,這個過程我們把它稱作編譯。雖然我們可以直接爲組件編寫render
函數,但是編寫template
模板更加直觀,也更符合我們的開發習慣。 -
Vue.js
提供了兩個版本,一個是Runtime + Compiler
的,一個是Runtime only
的,前者是包含編譯代碼的,可以把編譯過程放在運行時做,後者是不包含編譯代碼的,需要藉助webpack
的vue-loader
事先把模板編譯成render
函數。 -
這裏我們就來分析編譯的過程,對編譯過程的瞭解會讓我們對
Vue
的指令、內置組件等有更好的理解。不過由於編譯的過程是一個相對複雜的過程,我們只要求理解整體的流程、輸入和輸出即可。
二、編譯入口
- 當我們使用
Runtime + Compiler
的Vue.js
,它的入口是src/platforms/web/entry-runtime-with-compiler.js
,看一下它對$mount
函數的定義:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
- 這段函數邏輯之前分析過,關於編譯的入口就是在這裏:
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
compileToFunctions
方法就是把模板template
編譯生成render
以及staticRenderFns
,它的定義在src/platforms/web/compiler/index.js
中:
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
- 可以看到
compileToFunctions
方法實際上是createCompiler
方法的返回值,該方法接收一個編譯配置參數,接下來我們來看一下createCompiler
方法的定義,在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.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompiler
方法實際上是通過調用createCompilerCreator
方法返回的,該方法傳入的參數是一個函數,真正的編譯過程都在這個baseCompile
函數裏執行,那麼createCompilerCreator
又是什麼呢,它的定義在src/compiler/create-compiler.js
中:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
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 || null),
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
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
- 可以看到該方法返回了一個
createCompiler
的函數,它接收一個baseOptions
的參數,返回的是一個對象,包括compile
方法屬性和compileToFunctions
屬性,這個compileToFunctions
對應的就是$mount
函數調用的compileToFunctions
方法,它是調用createCompileToFunctionFn
方法的返回值,我們接下來看一下createCompileToFunctionFn
方法,它的定義在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 {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[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 = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
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 (cache[key] = res)
}
}
- 至此我們總算找到了
compileToFunctions
的最終定義,它接收三個參數、編譯模板template
,編譯配置options
和Vue
實例vm
。核心的編譯過程就一行代碼:
const compiled = compile(template, options)
compile
函數在執行createCompileToFunctionFn
的時候作爲參數傳入,它是createCompiler
函數中定義的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 || null),
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
}
compile
函數執行的邏輯是先處理配置參數,真正執行編譯過程就一行代碼:
const compiled = baseCompile(template, finalOptions)
baseCompile
在執行createCompilerCreator
方法時作爲參數傳入,如下所示:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
- 編譯的入口我們終於找到了,它主要就是執行了如下幾個邏輯:
- 解析模板字符串生成
AST
const ast = parse(template.trim(), options)
- 優化語法樹
optimize(ast, options)
- 生成代碼
const code = generate(ast, options)
- 總結:編譯入口邏輯之所以這麼繞,是因爲
Vue.js
在不同的平臺下都會有編譯的過程,因此編譯過程中的依賴的配置baseOptions
會有所不同。而編譯過程會多次執行,但這同一個平臺下每一次的編譯過程配置又是相同的,爲了不讓這些配置在每次編譯過程都通過參數傳入,Vue.js
利用了函數柯里化的技巧很好的實現了baseOptions
的參數保留。同樣,Vue.js
也是利用函數柯里化技巧把基礎的編譯過程函數抽出來,通過createCompilerCreator(baseCompile)
的方式把真正編譯的過程和其它邏輯如對編譯配置處理、緩存處理等剝離開。
三、parse 的理解
-
編譯過程首先就是對模板做解析,生成
AST
,它是一種抽象語法樹,是對源代碼的抽象語法結構的樹狀表現形式。在很多編譯技術中,如babel
編譯ES6
的代碼都會先生成AST
。 -
這個過程是比較複雜的,它會用到大量正則表達式對字符串解析,如果對正則不是很瞭解,建議先去補習正則表達式的知識。爲了演示
parse
的過程,我們先來看一個例子:
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
經過
parse
過程後,生成的 AST 如下:
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
]
]
}]
}
可以看到,生成的 AST 是一個樹狀結構,每一個節點都是一個
ast element
,除了它自身的一些屬性,還維護了它的父子關係,如parent
指向它的父節點,children
指向它的所有子節點。先對 AST 有一些直觀的印象,那麼接下來我們來分析一下這個 AST 是如何得到的。
- 整體流程,首先來看一下
parse
的定義,在src/compiler/parser/index.js
中:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
getFnsAndConfigFromOptions(options)
parseHTML(template, {
// options ...
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs)
processElement(element)
treeManagement()
},
end () {
treeManagement()
closeElement()
},
chars (text: string) {
handleText()
createChildrenASTOfText()
},
comment (text: string) {
createChildrenASTOfComment()
}
})
return astRootElement
}
parse
函數的代碼很長,先把它拆成僞代碼的形式,方便對於整體流程先有一個大致的瞭解。接下來我們就來分解分析每段僞代碼的作用。
- 從
options
中獲取方法和配置,對應僞代碼:
getFnsAndConfigFromOptions(options)
parse
函數的輸入是template
和options
,輸出是AST
的根節點。template
就是我們的模板字符串,而options
實際上是和平臺相關的一些配置,它的定義在src/platforms/web/compiler/options
中:
import {
isPreTag,
mustUseProp,
isReservedTag,
getTagNamespace
} from '../util/index'
import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
- 這些屬性和方法之所以放到
platforms
目錄下是因爲它們在不同的平臺(web
和weex
)的實現是不同的。我們用僞代碼getFnsAndConfigFromOptions
表示了這一過程,它的實際代碼如下:
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
- 解析
HTML
模板,對應僞代碼:
parseHTML(template, options)
對於
template
模板的解析主要是通過parseHTML
函數,它的定義在src/compiler/parser/html-parser
中:
export function parseHTML (html, options) {
let lastTag
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)){
let textEnd = html.indexOf('<')
if (textEnd === 0) {
if(matchComment) {
advance(commentLength)
continue
}
if(matchDoctype) {
advance(doctypeLength)
continue
}
if(matchEndTag) {
advance(endTagLength)
parseEndTag()
continue
}
if(matchStartTag) {
parseStartTag()
handleStartTag()
continue
}
}
handleText()
advance(textLength)
} else {
handlePlainTextElement()
parseEndTag()
}
}
}
- 由於
parseHTML
的邏輯也非常複雜,因此我也用了僞代碼的方式表達,整體來說它的邏輯就是循環解析template
,用正則做各種匹配,對於不同情況分別進行不同的處理,直到整個template
被解析完畢。
在匹配的過程中會利用advance
函數不斷前進整個模板字符串,直到字符串末尾,如下所示:
function advance (n) {
index += n
html = html.substring(n)
}
- 調用
advance
函數:
advance(4)
匹配的過程中主要利用了正則表達式,如下:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
- 通過這些正則表達式,我們可以匹配註釋節點、文檔類型節點、開始閉合標籤等,如下:
- 註釋節點、文檔類型節點,對於註釋節點和文檔類型節點的匹配,如果匹配到我們僅僅做的是做前進即可,如下所示:
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
-
對於註釋和條件註釋節點,前進至它們的末尾位置;對於文檔類型節點,則前進它自身長度的距離。
-
開始標籤,如下所示:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
- 首先通過
parseStartTag
解析開始標籤:
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
- 對於開始標籤,除了標籤名之外,還有一些標籤相關的屬性。函數先通過正則表達式
startTagOpen
匹配到開始標籤,然後定義了match
對象,接着循環去匹配開始標籤中的屬性並添加到match.attrs
中,直到匹配的開始標籤的閉合符結束。如果匹配到閉合符,則獲取一元斜線符,前進到閉合符尾,並把當前索引賦值給match.end
。parseStartTag
對開始標籤解析拿到match
後,緊接着會執行handleStartTag
對match
做處理:
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
-
handleStartTag
的核心邏輯很簡單,先判斷開始標籤是否是一元標籤,類似<img>、<br/>
這樣,接着對match.attrs
遍歷並做了一些處理,最後判斷如果非一元標籤,則往stack
裏push
一個對象,並且把tagName
賦值給lastTag
。最後調用了options.start
回調函數,並傳入一些參數。 -
閉合標籤,如下所示:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
先通過正則
endTag
匹配到閉合標籤,然後前進到閉合標籤末尾,然後執行parseEndTag
方法對閉合標籤做解析。
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
}
if (pos >= 0) {
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
parseEndTag
的核心邏輯很簡單,在介紹之前我們回顧一下在執行handleStartTag
的時候,對於非一元標籤(有 endTag)我們都把它構造成一個對象壓入到stack
中。那麼對於閉合標籤的解析,就是倒序stack
,找到第一個和當前endTag
匹配的元素。如果是正常的標籤匹配,那麼stack
的最後一個元素應該和當前的endTag
匹配,但是考慮到如下錯誤情況:
<div><span></div>
-
這個時候當
endTag
爲</div>
的時候,從stack
尾部找到的標籤是<span>
,就不能匹配,因此這種情況會報警告。匹配後把棧到pos
位置的都彈出,並從stack
尾部拿到lastTag
。最後調用了options.end
回調函數,並傳入一些參數。 -
文本,如下所示:
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
advance(textEnd)
}
if (textEnd < 0) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
-
接下來判斷
textEnd
是否大於等於 0 的,滿足則說明到從當前位置到textEnd
位置都是文本,並且如果<
是純文本中的字符,就繼續找到真正的文本結束的位置,然後前進到結束的位置。 -
再繼續判斷
textEnd
小於 0 的情況,則說明整個template
解析完畢了,把剩餘的html
都賦值給了text
。 -
最後調用了
options.chars
回調函數,並傳text
參數,這個回調函數的作用稍後我會詳細介紹。 -
因此,在循環解析整個
template
的過程中,會根據不同的情況,去執行不同的回調函數,下面我們來看看這些回調函數的作用。
- 處理開始標籤,對應僞代碼:
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs)
processElement(element)
treeManagement()
}
- 當解析到開始標籤的時候,最後會執行
start
回調函數,函數主要就做三件事情,創建AST
元素,處理AST
元素,AST
樹管理,下面我們來分別來看這幾個過程:
- 創建
AST
元素
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
}
通過
createASTElement
方法去創建一個 AST 元素,並添加了 namespace。可以看到,每一個 AST 元素就是一個普通的 JavaScript 對象,其中,type
表示 AST 元素類型,tag
表示標籤名,attrsList
表示屬性列表,attrsMap
表示屬性映射表,parent
表示父的 AST 元素,children
表示子 AST 元素集合。
- 處理
AST
元素
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.'
)
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
// element-scope stuff
processElement(element, options)
}
首先是對模塊
preTransforms
的調用,其實所有模塊的preTransforms
、transforms
和postTransforms
的定義都在src/platforms/web/compiler/modules
目錄中。接着判斷element
是否包含各種指令通過processXXX
做相應的處理,處理的結果就是擴展 AST 元素的屬性。這裏我並不會一一介紹所有的指令處理,而是結合我們當前的例子,我們來看一下processFor
和processIf
:
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`
)
}
}
}
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, '')
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
processFor
就是從元素中拿到v-for
指令的內容,然後分別解析出for
、alias
、iterator1
、iterator2
等屬性的值添加到AST
的元素上。就我們的示例v-for="(item,index) in data"
而言,解析出的的for
是data
,alias
是item
,iterator1
是index
,沒有iterator2
,如下所示:
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
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
}
}
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
processIf
就是從元素中拿v-if
指令的內容,如果拿到則給 AST 元素添加if
屬性和ifConditions
屬性;否則嘗試拿v-else
指令及v-else-if
指令的內容,如果拿到則給 AST 元素分別添加else
和elseif
屬性。
AST
樹管理,我們在處理開始標籤的時候爲每一個標籤創建了一個AST
元素,在不斷解析模板創建AST
元素的時候,我們也要爲它們建立父子關係,就像DOM
元素的父子關係那樣。AST
樹管理相關代碼如下:
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
)
}
}
}
// tree management
if (!root) {
root = element
checkRootConstraints(root)
} else if (!stack.length) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element)
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
AST
樹管理的目標是構建一顆AST
樹,本質上它要維護root
根節點和當前父節點currentParent
。爲了保證元素可以正確閉合,這裏也利用了stack
棧的數據結構,和我們之前解析模板時用到的stack
類似,如下所示:
-
當我們在處理開始標籤的時候,判斷如果有
currentParent
,會把當前AST
元素push
到currentParent.chilldren
中,同時把AST
元素的parent
指向currentParent
。 -
接着就是更新
currentParent
和stack
,判斷當前如果不是一個一元標籤,我們要把它生成的AST
元素push
到stack
中,並且把當前的AST
元素賦值給currentParent
。 -
stack
和currentParent
除了在處理開始標籤的時候會變化,在處理閉合標籤的時候也會變化,因此整個AST
樹管理要結合閉合標籤的處理邏輯看。
- 處理閉合標籤,對應僞代碼:
end () {
treeManagement()
closeElement()
}
當解析到閉合標籤的時候,最後會執行
end
回調函數:
// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
- 首先處理了尾部空格的情況,然後把
stack
的元素彈一個出棧,並把stack
最後一個元素賦值給currentParent
,這樣就保證了當遇到閉合標籤的時候,可以正確地更新stack
的長度以及currentParent
的值,這樣就維護了整個AST
樹。最後執行了closeElement(element)
:
function closeElement (element) {
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
closeElement
邏輯很簡單,就是更新一下inVPre
和inPre
的狀態,以及執行postTransforms
函數。
- 處理文本內容,對應僞代碼:
chars (text: string) {
handleText()
createChildrenASTOfText()
}
除了處理開始標籤和閉合標籤,我們還會在解析模板的過程中去處理一些文本內容:
const children = currentParent.children
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
let res
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text
})
}
}
- 文本構造的
AST
元素有兩種類型,一種是有表達式的,type
爲2
,一種是純文本,type
爲3
。在我們的例子中,文本就是{{item}}:{{index}}
,是個表達式,通過執行parseText(text, delimiters)
對文本解析,它的定義在src/compiler/parser/text-parser.js
中:
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
parseText
首先根據分隔符(默認是{{}}
)構造了文本匹配的正則表達式,然後再循環匹配文本,遇到普通文本就push
到rawTokens
和tokens
中,如果是表達式就轉換成_s(${exp})
push
到tokens
中,以及轉換成{@binding:exp}
push
到rawTokens
中。對於我們的例子{{item}}:{{index}}
,tokens
就是[_s(item),'":"',_s(index)]
;rawTokens
就是[{'@binding':'item'},':',{'@binding':'index'}]
。那麼返回的對象如下:
return {
expression: '_s(item)+":"+_s(index)',
tokens: [{'@binding':'item'},':',{'@binding':'index'}]
}
- 總結:
parse
的過程就分析完了,看似複雜,但我們可以拋開細節理清它的整體流程。parse
的目標是把template
模板字符串轉換成AST
樹,它是一種用JavaScript
對象的形式來描述整個模板。那麼整個parse
的過程是利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的回調函數,來達到構造AST
樹的目的。AST
元素節點總共有三種類型,type
爲1
表示是普通元素,爲2
表示是表達式,爲3
表示是純文本。當AST
樹構造完畢,下一步就是optimize
優化這顆樹。
三、optimize 的理解
-
當我們的模板
template
經過parse
過程後,會輸出生成AST
樹,那麼接下來我們需要對這顆樹做優化,optimize
的邏輯是遠簡單于parse
的邏輯,所以理解起來會輕鬆很多。 -
我們知道
Vue
是數據驅動,是響應式的,但是我們的模板並不是所有數據都是響應式的,也有很多數據是首次渲染後就永遠不會變化的,那麼這部分數據生成的DOM
也不會變化,我們可以在patch
的過程跳過對他們的比對。來看一下optimize
方法的定義,在src/compiler/optimizer.js
中:
/**
* 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.
*/
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.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
(keys ? ',' + keys : '')
)
}
我們在編譯階段可以把一些
AST
節點優化成靜態節點,所以整個optimize
的過程實際上就幹 2 件事情,markStatic(root)
標記靜態節點 ,markStaticRoots(root, false)
標記靜態根。
- 標記靜態節點,如下所示:
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
-
首先執行
node.static = isStatic(node)
。isStatic
是對一個AST
元素節點是否是靜態的判斷,如果是表達式,就是非靜態;如果是純文本,就是靜態;對於一個普通元素,如果有pre
屬性,那麼它使用了v-pre
指令,是靜態,否則要同時滿足以下條件:沒有使用v-if
、v-for
,沒有使用其它指令(不包括v-once
),非內置組件,是平臺保留的標籤,非帶有v-for
的template
標籤的直接子節點,節點的所有屬性的key
都滿足靜態key
;這些都滿足則這個AST
節點是一個靜態節點。 -
如果這個節點是一個普通元素,則遍歷它的所有
children
,遞歸執行markStatic
。因爲所有的elseif
和else
節點都不在children
中, 如果節點的ifConditions
不爲空,則遍歷ifConditions
拿到所有條件中的block
,也就是它們對應的AST
節點,遞歸執行markStatic
。在這些遞歸過程中,一旦子節點有不是static
的情況,則它的父節點的static
均變成false
。 -
標記靜態根,如下所示:
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
-
markStaticRoots
第二個參數是isInFor
,對於已經是static
的節點或者是v-once
指令的節點,node.staticInFor = isInFor
。接着就是對於staticRoot
的判斷邏輯,從註釋中我們可以看到,對於有資格成爲staticRoot
的節點,除了本身是一個靜態節點外,必須滿足擁有children
,並且children
不能只是一個文本節點,不然的話把它標記成靜態根節點的收益就很小了。接下來和標記靜態節點的邏輯一樣,遍歷children
以及ifConditions
,遞歸執行markStaticRoots
。 -
經過
optimize
後,AST
樹變成了如下:
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}
我們發現每一個 AST 元素節點都多了
staic
屬性,並且type
爲 1 的普通元素 AST 節點多了staticRoot
屬性。
- 總結:分析完了
optimize
的過程,就是深度遍歷這個AST
樹,去檢測它的每一顆子樹是不是靜態節點,如果是靜態節點則它們生成DOM
永遠不需要改變,這對運行時對模板的更新起到極大的優化作用。我們通過optimize
我們把整個AST
樹中的每一個AST
元素節點標記了static
和staticRoot
,它會影響我們接下來執行代碼生成的過程。
五、codegen 的理解
- 編譯的最後一步就是把優化後的
AST
樹轉換成可執行的代碼,瞭解整體流程即可。爲了方便理解,我們還是用之前的例子:
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
它經過編譯,執行
const code = generate(ast, options)
,生成的render
代碼串如下:
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}
這裏的
_c
函數定義在src/core/instance/render.js
中。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
而
_l
、_v
定義在src/core/instance/render-helpers/index.js
中:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
_c
就是執行createElement
去創建VNode
,而_l
對應renderList
渲染列表;_v
對應createTextVNode
創建文本VNode
;_e
對於createEmptyVNode
創建空的VNode
。 在compileToFunctions
中,會把這個render
代碼串轉換成函數,它的定義在src/compler/to-function.js
中:
const compiled = compile(template, options)
res.render = createFunction(compiled.render, fnGenErrors)
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
實際上就是把
render
代碼串通過new Function
的方式轉換成可執行的函數,賦值給vm.options.render
,這樣當組件通過vm._render
的時候,就會執行這個render
函數。那麼接下來我們就重點關注一下這個render
代碼串的生成過程。
generate
,如下所示:
const code = generate(ast, options)
generate
函數的定義在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
}
}
generate
函數首先通過genElement(ast, state)
生成code
,再把code
用with(this){return ${code}}}
包裹起來。這裏的state
是CodegenState
的一個實例,先來看一下genElement
:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
基本就是判斷當前 AST 元素節點的屬性執行不同的代碼生成函數,在我們的例子中,我們先了解一下
genFor
和genIf
。
genIf
,如下所示:
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
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)
}
}
genIf
主要是通過執行genIfConditions
,它是依次從conditions
獲取第一個condition
,然後通過對condition.exp
去生成一段三元運算符的代碼,:
後是遞歸調用genIfConditions
,這樣如果有多個conditions
,就生成多層三元運算邏輯。這裏我們暫時不考慮v-once
的情況,所以
genTernaryExp
最終是調用了genElement
。在我們的例子中,只有一個condition
,exp
爲isShow
,因此生成如下僞代碼:
return (isShow) ? genElement(el, state) : _e()
genFor
,如下所示:
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (process.env.NODE_ENV !== 'production' &&
state.maybeComponent(el) &&
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genFor
的邏輯很簡單,首先AST
元素節點中獲取了和for
相關的一些屬性,然後返回了一個代碼字符串。在我們的例子中,exp
是data
,alias
是item
,iterator1
,因此生成如下僞代碼:
_l((data), function(item, index) {
return genElememt(el, state)
})
genData & genChildren
,它的最外層是ul
,首先執行genIf
,它最終調用了genElement(el, state)
去生成子節點,注意,這裏的el
仍然指向的是ul
對應的AST
節點,但是此時的el.ifProcessed
爲true
,所以命中最後一個else
邏輯:
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
這裏我們只關注 2 個邏輯,
genData
和genChildren
:
genData
,如下所示:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},`
}
// DOM props
if (el.props) {
data += `domProps:{${genProps(el.props)}},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true, state.warn)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
genData
函數就是根據AST
元素節點的屬性構造出一個data
對象字符串,這個在後面創建VNode
的時候的時候會作爲參數傳入。之前我們提到了CodegenState
的實例state
,這裏有一段關於state
的邏輯:
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
state.dataGenFns
的初始化在它的構造器中,如下所示:
export class CodegenState {
constructor (options: CompilerOptions) {
// ...
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
// ...
}
}
- 實際上就是獲取所有
modules
中的genData
函數,其中,class module
和style module
定義了genData
函數。比如定義在src/platforms/web/compiler/modules/class.js
中的genData
方法:
function genData (el: ASTElement): string {
let data = ''
if (el.staticClass) {
data += `staticClass:${el.staticClass},`
}
if (el.classBinding) {
data += `class:${el.classBinding},`
}
return data
}
- 在我們的例子中,
ul
AST
元素節點定義了el.staticClass
和el.classBinding
,因此最終生成的data
字符串如下:
{
staticClass: "list",
class: bindCls
}
genChildren
,接下來我們再來看一下genChildren
,它的定義在src/compiler/codegen/index.js
中:
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
return (altGenElement || genElement)(el, state)
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
- 在我們的例子中,
li
AST
元素節點是ul
AST
元素節點的children
之一,滿足(children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot')
條件,因此通過genElement(el, state)
生成li
AST
元素節點的代碼,也就回到了我們之前調用genFor
生成的代碼,把它們拼在一起生成的僞代碼如下:
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return genElememt(el, state)
})
) : _e()
- 在我們的例子中,在執行
genElememt(el, state)
的時候,el
還是li
AST
元素節點,el.forProcessed
已爲true
,所以會繼續執行genData
和genChildren
的邏輯。由於el.events
不爲空,在執行genData
的時候,會執行 如下邏輯:
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`
}
genHandlers
的定義在src/compiler/codegen/events.js
中:
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean,
warn: Function
): string {
let res = isNative ? 'nativeOn:{' : 'on:{'
for (const name in events) {
res += `"${name}":${genHandler(name, events[name])},`
}
return res.slice(0, -1) + '}'
}
genHandler
的邏輯就不介紹了,很大部分都是對修飾符modifier
的處理,對於我們的例子,它最終genData
生成的data
字符串如下:
{
on: {
"click": function($event) {
clickItem(index)
}
}
}
genChildren
的時候,會執行到如下邏輯:
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
// ...
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
genChildren
的就是遍歷children
,然後執行genNode
方法,根據不同的type
執行具體的方法。在我們的例子中,li
AST
元素節點的children
是type
爲2
的表達式AST
元素節點,那麼會執行到genText(node)
邏輯,如下所示:
export function genText (text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
因此在我們的例子中,
genChildren
生成的代碼串如下:
[_v(_s(item) + ":" + _s(index))]
和之前拼在一起,最終生成的
code
如下:
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
- 總結:從
ast -> code
這一步有了一些瞭解,編譯後生成的代碼就是在運行時執行的代碼。