前言
這篇文章是 Vue 編譯器的最後一部分,前兩部分分別是:Vue 原始碼解讀(8)—— 編譯器 之 解析、Vue 原始碼解讀(9)—— 編譯器 之 優化。
從 HTML 模版字串開始,解析所有標籤以及標籤上的各個屬性,得到 AST 語法樹,然後基於 AST 語法樹進行靜態標記,首先標記每個節點是否為靜態靜態,然後進一步標記出靜態根節點。這樣在後續的更新中就可以跳過這些靜態根節點的更新,從而提高效能。
這最後一部分講的是如何從 AST 生成渲染函式。
目標
深入理解渲染函式的生成過程,理解編譯器是如何將 AST 變成執行時的程式碼,也就是我們寫的類 html 模版最終變成了什麼?
原始碼解讀
入口
/src/compiler/index.js
/**
* 在這之前做的所有的事情,只有一個目的,就是為了構建平臺特有的編譯選項(options),比如 web 平臺
*
* 1、將 html 模版解析成 ast
* 2、對 ast 樹進行靜態標記
* 3、將 ast 生成渲染函式
* 靜態渲染函式放到 code.staticRenderFns 陣列中
* code.render 為動態渲染函式
* 在將來渲染時執行渲染函式得到 vnode
*/
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 將模版解析為 AST,每個節點的 ast 物件上都設定了元素的所有資訊,比如,標籤資訊、屬性資訊、插槽資訊、父節點、子節點等。
// 具體有那些屬性,檢視 options.start 和 options.end 這兩個處理開始和結束標籤的方法
const ast = parse(template.trim(), options)
// 優化,遍歷 AST,為每個節點做靜態標記
// 標記每個節點是否為靜態節點,然後進一步標記出靜態根節點
// 這樣在後續更新中就可以跳過這些靜態節點了
// 標記靜態根,用於生成渲染函式階段,生成靜態根節點的渲染函式
if (options.optimize !== false) {
optimize(ast, options)
}
// 程式碼生成,將 ast 轉換成可執行的 render 函式的字串形式
// code = {
// render: `with(this){return ${_c(tag, data, children, normalizationType)}}`,
// staticRenderFns: [_c(tag, data, children, normalizationType), ...]
// }
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
generate
/src/compiler/codegen/index.js
/**
* 從 AST 生成渲染函式
* @returns {
* render: `with(this){return _c(tag, data, children)}`,
* staticRenderFns: state.staticRenderFns
* }
*/
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 例項化 CodegenState 物件,生成程式碼的時候需要用到其中的一些東西
const state = new CodegenState(options)
// 生成字串格式的程式碼,比如:'_c(tag, data, children, normalizationType)'
// data 為節點上的屬性組成 JSON 字串,比如 '{ key: xx, ref: xx, ... }'
// children 為所有子節點的字串格式的程式碼組成的字串陣列,格式:
// `['_c(tag, data, children)', ...],normalizationType`,
// 最後的 normalization 是 _c 的第四個引數,
// 表示節點的規範化型別,不是重點,不需要關注
// 當然 code 並不一定就是 _c,也有可能是其它的,比如整個元件都是靜態的,則結果就為 _m(0)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
genElement
/src/compiler/codegen/index.js
閱讀建議:
先讀最後的 else 模組生成 code 的語句部分,即處理自定義元件和原生標籤的 else 分支,理解最終生成的資料格式是什麼樣的;然後再回頭閱讀
genChildren
和genData
,先讀genChildren
,程式碼量少,徹底理解最終生成的資料結構,最後再從上到下去閱讀其它的分支。在閱讀以下程式碼時,請把 Vue 原始碼解讀(8)—— 編譯器 之 解析(下) 最後得到的 AST 物件放旁邊輔助閱讀,因為生成渲染函式的過程就是在處理該物件上眾多的屬性的過程。
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
/**
* 處理靜態根節點,生成節點的渲染函式
* 1、將當前靜態節點的渲染函式放到 staticRenderFns 陣列中
* 2、返回一個可執行函式 _m(idx, true or '')
*/
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
/**
* 處理帶有 v-once 指令的節點,結果會有三種:
* 1、當前節點存在 v-if 指令,得到一個三元表示式,condition ? render1 : render2
* 2、當前節點是一個包含在 v-for 指令內部的靜態節點,得到 `_o(_c(tag, data, children), number, key)`
* 3、當前節點就是一個單純的 v-once 節點,得到 `_m(idx, true of '')`
*/
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
/**
* 處理節點上的 v-for 指令
* 得到 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
*/
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
/**
* 處理帶有 v-if 指令的節點,最終得到一個三元表示式:condition ? render1 : render2
*/
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
/**
* 當前節點不是 template 標籤也不是插槽和帶有 v-pre 指令的節點時走這裡
* 生成所有子節點的渲染函式,返回一個陣列,格式如:
* [_c(tag, data, children, normalizationType), ...]
*/
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
/**
* 生成插槽的渲染函式,得到
* _t(slotName, children, attrs, bind)
*/
return genSlot(el, state)
} else {
// component or element
// 處理動態元件和普通元素(自定義元件、原生標籤)
let code
if (el.component) {
/**
* 處理動態元件,生成動態元件的渲染函式
* 得到 `_c(compName, data, children)`
*/
code = genComponent(el.component, el, state)
} else {
// 自定義元件和原生標籤走這裡
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 非普通元素或者帶有 v-pre 指令的元件走這裡,處理節點的所有屬性,返回一個 JSON 字串,
// 比如 '{ key: xx, ref: xx, ... }'
data = genData(el, state)
}
// 處理子節點,得到所有子節點字串格式的程式碼組成的陣列,格式:
// `['_c(tag, data, children)', ...],normalizationType`,
// 最後的 normalization 表示節點的規範化型別,不是重點,不需要關注
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 得到最終的字串格式的程式碼,格式:
// '_c(tag, data, children, normalizationType)'
code = `_c('${el.tag}'${data ? `,${data}` : '' // data
}${children ? `,${children}` : '' // children
})`
}
// 如果提供了 transformCode 方法,
// 則最終的 code 會經過各個模組(module)的該方法處理,
// 不過框架沒提供這個方法,不過即使處理了,最終的格式也是 _c(tag, data, children)
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
genChildren
/src/compiler/codegen/index.js
/**
* 生成所有子節點的渲染函式,返回一個陣列,格式如:
* [_c(tag, data, children, normalizationType), ...]
*/
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]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
// 優化,只有一個子節點 && 子節點的上有 v-for 指令 && 子節點的標籤不為 template 或者 slot
// 優化的方式是直接呼叫 genElement 生成該節點的渲染函式,不需要走下面的迴圈然後呼叫 genCode 最後得到渲染函式
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
// 獲取節點規範化型別,返回一個 number 0、1、2,不是重點, 不重要
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
// 函式,生成程式碼的一個函式
const gen = altGenNode || genNode
// 返回一個陣列,陣列的每個元素都是一個子節點的渲染函式,
// 格式:['_c(tag, data, children, normalizationType)', ...]
return `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : ''
}`
}
}
genNode
/src/compiler/codegen/index.js
function genNode(node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
genText
/src/compiler/codegen/index.js
export function genText(text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
genComment
/src/compiler/codegen/index.js
export function genComment(comment: ASTText): string {
return `_e(${JSON.stringify(comment.text)})`
}
genData
/src/compiler/codegen/index.js
/**
* 處理節點上的眾多屬性,最後生成這些屬性組成的 JSON 字串,比如 data = { key: xx, ref: xx, ... }
*/
export function genData(el: ASTElement, state: CodegenState): string {
// 節點的屬性組成的 JSON 字串
let data = '{'
// 首先先處理指令,因為指令可能在生成其它屬性之前改變這些屬性
// 執行指令編譯方法,比如 web 平臺的 v-text、v-html、v-model,然後在 el 物件上新增相應的屬性,
// 比如 v-text: el.textContent = _s(value, dir)
// v-html:el.innerHTML = _s(value, dir)
// 當指令在執行時還有任務時,比如 v-model,則返回 directives: [{ name, rawName, value, arg, modifiers }, ...}]
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key,data = { key: xx }
if (el.key) {
data += `key:${el.key},`
}
// ref,data = { ref: xx }
if (el.ref) {
data += `ref:${el.ref},`
}
// 帶有 ref 屬性的節點在帶有 v-for 指令的節點的內部, data = { refInFor: true }
if (el.refInFor) {
data += `refInFor:true,`
}
// pre,v-pre 指令,data = { pre: true }
if (el.pre) {
data += `pre:true,`
}
// 動態元件,data = { tag: 'component' }
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// 為節點執行模組(class、style)的 genData 方法,
// 得到 data = { staticClass: xx, class: xx, staticStyle: xx, style: xx }
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// 其它屬性,得到 data = { attrs: 靜態屬性字串 } 或者
// data = { attrs: '_d(靜態屬性字串, 動態屬性字串)' }
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props,結果同 el.attrs
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// 自定義事件,data = { `on${eventName}:handleCode` } 或者 { `on_d(${eventName}:handleCode`, `${eventName},handleCode`) }
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
// 帶 .native 修飾符的事件,
// data = { `nativeOn${eventName}:handleCode` } 或者 { `nativeOn_d(${eventName}:handleCode`, `${eventName},handleCode`) }
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// 非作用域插槽,得到 data = { slot: slotName }
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots,作用域插槽,data = { scopedSlots: '_u(xxx)' }
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// 處理 v-model 屬性,得到
// data = { model: { value, callback, expression } }
// component v-model
if (el.model) {
data += `model:{value:${el.model.value
},callback:${el.model.callback
},expression:${el.model.expression
}},`
}
// inline-template,處理內聯模版,得到
// data = { inlineTemplate: { render: function() { render 函式 }, staticRenderFns: [ function() {}, ... ] } }
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
// 刪掉 JSON 字串最後的 逗號,然後加上閉合括號 }
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
// 存在動態屬性,data = `_b(data, tag, 靜態屬性字串或者_d(靜態屬性字串, 動態屬性字串))`
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
genDirectives
/src/compiler/codegen/index.js
閱讀建議:這部分內容也可以放到其它方法後面去讀,比如你想深究 v-model 的實現原理
/**
* 執行指令的編譯方法,如果指令存在執行時任務,則返回 directives: [{ name, rawName, value, arg, modifiers }, ...}]
*/
function genDirectives(el: ASTElement, state: CodegenState): string | void {
// 獲取指令陣列
const dirs = el.directives
// 沒有指令則直接結束
if (!dirs) return
// 指令的處理結果
let res = 'directives:['
// 標記,用於標記指令是否需要在執行時完成的任務,比如 v-model 的 input 事件
let hasRuntime = false
let i, l, dir, needRuntime
// 遍歷指令陣列
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
// 獲取節點當前指令的處理方法,比如 web 平臺的 v-html、v-text、v-model
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// 執行指令的編譯方法,如果指令還需要執行時完成一部分任務,則返回 true,比如 v-model
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
// 表示該指令在執行時還有任務
hasRuntime = true
// res = directives:[{ name, rawName, value, arg, modifiers }, ...]
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
// 也就是說,只有指令存在執行時任務時,才會返回 res
return res.slice(0, -1) + ']'
}
}
genProps
/src/compiler/codegen/index.js
/**
* 遍歷屬性陣列 props,得到所有屬性組成的字串
* 如果不存在動態屬性,則返回:
* 'attrName,attrVal,...'
* 如果存在動態屬性,則返回:
* '_d(靜態屬性字串, 動態屬性字串)'
*/
function genProps(props: Array<ASTAttr>): string {
// 靜態屬性
let staticProps = ``
// 動態屬性
let dynamicProps = ``
// 遍歷屬性陣列
for (let i = 0; i < props.length; i++) {
// 屬性
const prop = props[i]
// 屬性值
const value = __WEEX__
? generateValue(prop.value)
: transformSpecialNewlines(prop.value)
if (prop.dynamic) {
// 動態屬性,`dAttrName,dAttrVal,...`
dynamicProps += `${prop.name},${value},`
} else {
// 靜態屬性,'attrName,attrVal,...'
staticProps += `"${prop.name}":${value},`
}
}
// 去掉靜態屬性最後的逗號
staticProps = `{${staticProps.slice(0, -1)}}`
if (dynamicProps) {
// 如果存在動態屬性則返回:
// _d(靜態屬性字串,動態屬性字串)
return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
} else {
// 說明屬性陣列中不存在動態屬性,直接返回靜態屬性字串
return staticProps
}
}
genHandlers
/src/compiler/codegen/events.js
/**
* 生成自定義事件的程式碼
* 動態:'nativeOn|on_d(staticHandlers, [dynamicHandlers])'
* 靜態:`nativeOn|on${staticHandlers}`
*/
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
// 原生:nativeOn,否則為 on
const prefix = isNative ? 'nativeOn:' : 'on:'
// 靜態
let staticHandlers = ``
// 動態
let dynamicHandlers = ``
// 遍歷 events 陣列
// events = [{ name: { value: 回撥函式名, ... } }]
for (const name in events) {
// 獲取指定事件的回撥函式名,即 this.methodName 或者 [this.methodName1, ...]
const handlerCode = genHandler(events[name])
if (events[name] && events[name].dynamic) {
// 動態,dynamicHandles = `eventName,handleCode,...,`
dynamicHandlers += `${name},${handlerCode},`
} else {
// 靜態,staticHandles = `"eventName":handleCode,`
staticHandlers += `"${name}":${handlerCode},`
}
}
// 去掉末尾的逗號
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
// 動態,on_d(statickHandles, [dynamicHandlers])
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
// 靜態,`on${staticHandlers}`
return prefix + staticHandlers
}
}
genStatic
/src/compiler/codegen/index.js
/**
* 生成靜態節點的渲染函式
* 1、將當前靜態節點的渲染函式放到 staticRenderFns 陣列中
* 2、返回一個可執行函式 _m(idx, true or '')
*/
// hoist static sub-trees out
function genStatic(el: ASTElement, state: CodegenState): string {
// 標記當前靜態節點已經被處理過了
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
// 將靜態根節點的渲染函式 push 到 staticRenderFns 陣列中,比如:
// [`with(this){return _c(tag, data, children)}`]
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
// 返回一個可執行函式:_m(idx, true or '')
// idx = 當前靜態節點的渲染函式在 staticRenderFns 陣列中下標
return `_m(${state.staticRenderFns.length - 1
}${el.staticInFor ? ',true' : ''
})`
}
genOnce
/src/compiler/codegen/index.js
/**
* 處理帶有 v-once 指令的節點,結果會有三種:
* 1、當前節點存在 v-if 指令,得到一個三元表示式,condition ? render1 : render2
* 2、當前節點是一個包含在 v-for 指令內部的靜態節點,得到 `_o(_c(tag, data, children), number, key)`
* 3、當前節點就是一個單純的 v-once 節點,得到 `_m(idx, true of '')`
*/
function genOnce(el: ASTElement, state: CodegenState): string {
// 標記當前節點的 v-once 指令已經被處理過了
el.onceProcessed = true
if (el.if && !el.ifProcessed) {
// 如果含有 v-if 指令 && if 指令沒有被處理過,則走這裡
// 處理帶有 v-if 指令的節點,最終得到一個三元表示式,condition ? render1 : render2
return genIf(el, state)
} else if (el.staticInFor) {
// 說明當前節點是被包裹在還有 v-for 指令節點內部的靜態節點
// 獲取 v-for 指令的 key
let key = ''
let parent = el.parent
while (parent) {
if (parent.for) {
key = parent.key
break
}
parent = parent.parent
}
// key 不存在則給出提示,v-once 節點只能用於帶有 key 的 v-for 節點內部
if (!key) {
process.env.NODE_ENV !== 'production' && state.warn(
`v-once can only be used inside v-for that is keyed. `,
el.rawAttrsMap['v-once']
)
return genElement(el, state)
}
// 生成 `_o(_c(tag, data, children), number, key)`
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
// 上面幾種情況都不符合,說明就是一個簡單的靜態節點,和處理靜態根節點時的操作一樣,
// 得到 _m(idx, true or '')
return genStatic(el, state)
}
}
genFor
/src/compiler/codegen/index.js
/**
* 處理節點上的 v-for 指令
* 得到 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
*/
export function genFor(
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
// v-for 的迭代器,比如 一個陣列
const exp = el.for
// 迭代時的別名
const alias = el.alias
// iterator 為 v-for = "(item ,idx) in obj" 時會有,比如 iterator1 = idx
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
// 提示,v-for 指令在元件上時必須使用 key
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.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
// 標記當前節點上的 v-for 指令已經被處理過了
el.forProcessed = true // avoid r
// 得到 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genIf
/src/compiler/codegen/index.js
/**
* 處理帶有 v-if 指令的節點,最終得到一個三元表示式,condition ? render1 : render2
*/
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 標記當前節點的 v-if 指令已經被處理過了,避免無效的遞迴
el.ifProcessed = true // avoid recursion
// 得到三元表示式,condition ? render1 : render2
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()'
}
// 從 conditions 陣列中拿出第一個條件物件 { exp, block }
const condition = conditions.shift()
// 返回結果是一個三元表示式字串,condition ? 渲染函式1 : 渲染函式2
if (condition.exp) {
// 如果 condition.exp 條件成立,則得到一個三元表示式,
// 如果條件不成立,則通過遞迴的方式找 conditions 陣列中下一個元素,
// 直到找到條件成立的元素,然後返回一個三元表示式
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)
}
}
genSlot
/src/compiler/codegen/index.js
/**
* 生成插槽的渲染函式,得到
* _t(slotName, children, attrs, bind)
*/
function genSlot(el: ASTElement, state: CodegenState): string {
// 插槽名稱
const slotName = el.slotName || '"default"'
// 生成所有的子節點
const children = genChildren(el, state)
// 結果字串,_t(slotName, children, attrs, bind)
let res = `_t(${slotName}${children ? `,${children}` : ''}`
const attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
})))
: null
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
return res + ')'
}
genComponent
/src/compiler/codegen/index.js
// componentName is el.component, take it as argument to shun flow's pessimistic refinement
/**
* 生成動態元件的渲染函式
* 返回 `_c(compName, data, children)`
*/
function genComponent(
componentName: string,
el: ASTElement,
state: CodegenState
): string {
// 所有的子節點
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 返回 `_c(compName, data, children)`
// compName 是 is 屬性的值
return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : ''
})`
}
總結
-
面試官 問:簡單說一下 Vue 的編譯器都做了什麼?
答:
Vue 的編譯器做了三件事情:
-
將元件的 html 模版解析成 AST 物件
-
優化,遍歷 AST,為每個節點做靜態標記,標記其是否為靜態節點,然後進一步標記出靜態根節點,這樣在後續更新的過程中就可以跳過這些靜態節點了;標記靜態根用於生成渲染函式階段,生成靜態根節點的渲染函式
-
從 AST 生成執行渲染函式,即大家說的 render,其實還有一個,就是 staticRenderFns 陣列,裡面存放了所有的靜態節點的渲染函式
-
-
面試官:詳細說一下渲染函式的生成過程
答:
大家一說到渲染函式,基本上說的就是 render 函式,其實編譯器生成的渲染有兩類:
-
第一類就是一個 render 函式,負責生成動態節點的 vnode
-
第二類是放在一個叫 staticRenderFns 陣列中的靜態渲染函式,這些函式負責生成靜態節點的 vnode
渲染函式生成的過程,其實就是在遍歷 AST 節點,通過遞迴的方式,處理每個節點,最後生成形如:
_c(tag, attr, children, normalizationType)
的結果。tag 是標籤名,attr 是屬性物件,children 是子節點組成的陣列,其中每個元素的格式都是_c(tag, attr, children, normalizationTYpe)
的形式,normalization 表示節點的規範化型別,是一個數字 0、1、2,不重要。在處理 AST 節點過程中需要大家重點關注也是面試中常見的問題有:
-
靜態節點是怎麼處理的
靜態節點的處理分為兩步:
-
將生成靜態節點 vnode 函式放到 staticRenderFns 陣列中
-
返回一個 _m(idx) 的可執行函式,意思是執行 staticRenderFns 陣列中下標為 idx 的函式,生成靜態節點的 vnode
-
-
v-once、v-if、v-for、元件 等都是怎麼處理的
-
單純的 v-once 節點處理方式和靜態節點一致
-
v-if 節點的處理結果是一個三元表示式
-
v-for 節點的處理結果是可執行的 _l 函式,該函式負責生成 v-for 節點的 vnode
-
元件的處理結果和普通元素一樣,得到的是形如
_c(compName)
的可執行程式碼,生成元件的 vnode
-
-
到這裡,Vue 編譯器 的原始碼解讀就結束了。相信大家在閱讀的過程中不免會產生雲裡霧裡的感覺。這個沒什麼,編譯器這塊兒確實是比較複雜,可以說是整個框架最難理解也是程式碼量最大的一部分了。一定要靜下心來多讀幾遍,遇到無法理解的地方,一定要勤動手,通過示例程式碼加斷點除錯的方式幫助自己理解。
當你讀完幾遍以後,這時候情況可能就會好一些,但是有些地方可能還會有些暈,這沒事,正常。畢竟這是一個框架的編譯器,要處理的東西太多太多了,你只需要理解其核心思想(模版解析、靜態標記、程式碼生成)就可以了。後面會有 手寫 Vue 系列,編譯器這部分會有一個簡版的實現,幫助加深對這部分知識的理解。
編譯器讀完以後,會發現有個不明白的地方:編譯器最後生成的程式碼都是經過 with
包裹的,比如:
<div id="app">
<div v-for="item in arr" :key="item">{{ item }}</div>
</div>
經過編譯後生成:
with (this) {
return _c(
'div',
{
attrs:
{
"id": "app"
}
},
_l(
(arr),
function (item) {
return _c(
'div',
{
key: item
},
[_v(_s(item))]
)
}
),
0
)
}
都知道,with
語句可以擴充套件作用域鏈,所以生成的程式碼中的 _c、_l、_v、_s
都是 this 上一些方法,也就是說在執行時執行這些方法可以生成各個節點的 vnode。
所以聯絡前面的知識,響應式資料更新的整個執行過程就是:
-
響應式攔截到資料的更新
-
dep 通知 watcher 進行非同步更新
-
watcher 更新時執行元件更新函式 updateComponent
-
首先執行 vm._render 生成元件的 vnode,這時就會執行編譯器生成的函式
-
問題:
-
渲染函式中的
_c、_l、、_v、_s
等方法是什麼? -
它們是如何生成 vnode 的?
-
下一篇文章 Vue 原始碼解讀(11)—— render helper 將會帶來這部分知識的詳細解讀,也是面試經常被問題的:比如:v-for
的原理是什麼?
連結
- 配套視訊,微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:關注、點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注、 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。