本文我們一起通過學習Vue模板編譯原理(二)-AST生成Render字串來分析Vue原始碼。預計接下來會圍繞Vue原始碼來整理一些文章,如下。
- 一起來學Vue雙向繫結原理-資料劫持和釋出訂閱
- 一起來學Vue模板編譯原理(一)-Template生成AST
- 一起來學Vue模板編譯原理(二)-AST生成Render字串
- 一起來學Vue虛擬DOM解析-Virtual Dom實現和Dom-diff演算法
這些文章統一放在我的git倉庫:github.com/yzsunlei/ja…。覺得有用記得star收藏。
編譯過程
模板編譯是Vue中比較核心的一部分。關於 Vue 編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,前後關係如下:
第一步:將模板字串轉換成element ASTs(解析器)
第二步:對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)
第三步:使用element ASTs生成render函式程式碼字串(程式碼生成器)
對應的Vue原始碼如下,原始碼位置在src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.parse,模板字串 轉換成 抽象語法樹(AST)
const ast = parse(template.trim(), options)
// 2.optimize,對 AST 進行靜態節點標記
if (options.optimize !== false) {
optimize(ast, options)
}
// 3.generate,抽象語法樹(AST) 生成 render函式程式碼字串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
複製程式碼
這篇文件主要講第三步使用element ASTs生成render函式程式碼字串,對應的原始碼實現我們通常稱之為程式碼生成器。
程式碼生成器執行過程
在分析程式碼生成器的原理前,我們先舉例看下程式碼生成器的具體作用。
<div>
<p>{{name}}</p>
</div>
複製程式碼
在上節"Template生成AST"中,我們已經說了通過解析器會把上面模板解析成抽象語法樹(AST),解析結果如下:
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
複製程式碼
- 而在這一節我們會將AST轉換成可以直接執行的JavaScript字串,最終結果如下:
with(this) {
return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}
複製程式碼
現在看不懂不要緊。其實生成器的過程就是 generate 函式拿到解析好的 AST 物件,遞迴 AST 樹,為不同的 AST 節點建立不同的內部呼叫方法,然後組合成可執行的JavaScript字串,等待後面的呼叫。
內部呼叫方法
我們看上面示例生成的JavaScript字串,會發現裡面會有_v
、_c
、_s
這樣的東西,這些其實就是Vue內部定義的一些呼叫方法。
其中 _c
函式定義在 src/core/instance/render.js
中。
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 定義的_c函式是用來建立元素的
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
複製程式碼
而其他 _s
、_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 //生成列表VNode
target._t = renderSlot //生成解析slot節點
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic //生成靜態元素
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps //繫結物件屬性
target._v = createTextVNode //建立文字VNode
target._e = createEmptyVNode //建立空節點VNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
複製程式碼
以上都是會在生成的JavaScript字串中用到的,像比較常用的 _c
執行 createElement
去建立 VNode
, _l
對應 renderList
渲染列表;_v
對應 createTextVNode
建立文字 VNode
;_e
對於 createEmptyVNode
建立空的 VNode
。
整體邏輯
程式碼生成器的入口函式是generate
,具體定義如下,原始碼位置在src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 初始化一些options
const state = new CodegenState(options)
// 傳入ast和options進行生成
const code = ast ? genElement(ast, state) : '_c("div")' //重點
return {
// 最外層包一個 with(this) 之後返回
render: `with(this){return ${code}}`,
// 這個陣列中的函式與 VDOM 中的 diff 演算法優化相關
// 那些被標記為 staticRoot 節點的 VNode 就會單獨生成 staticRenderFns
staticRenderFns: state.staticRenderFns
}
}
複製程式碼
生成器入口函式就比較簡單,先初始化一些配置options,然後傳入ast進行生成,沒有ast時直接生成一個空div,最後返回生成的JavaScript字串和靜態節點的渲染函式。這裡會把靜態節點區分出來,便於後面的Vnode diff,即Vue比較演算法更新DOM,現在先略過。
現在我們重點看看genElement函式,程式碼位置在src/compiler/codegen/index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state) // 靜態節點處理生成函式
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state) // v-once處理生成函式
} else if (el.for && !el.forProcessed) {
return genFor(el, state) // v-for處理生成函式
} else if (el.if && !el.ifProcessed) {
return genIf(el, state) // v-if處理生成函式
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0' // 子節點處理生成函式
} else if (el.tag === 'slot') {
return genSlot(el, state) // slot處理生成函式
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state) // component元件處理生成函式
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state) // attributes節點屬性處理生成函式
}
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
}
}
複製程式碼
你會發現genElement函式裡面有很多條件判斷。這是因為Vue裡面的指令寫法實在太多,像 v-if
、 v-for
、 v-slot
等,每種指令寫法都分離出一個函式來單獨處理,這樣程式碼閱讀起來也清晰明瞭。下面,我們重點來看看 genFor
和 genData
的具體處理邏輯。
genFor
genFor 函式是用來處理 v-for 指令寫法的,原始碼位置在src/compiler/codegen/index.js
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.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
複製程式碼
genFor 的邏輯其實就是,首先 AST 元素節點中獲取了和 for 相關的一些屬性,然後返回了一個程式碼字串。
genData
genData 函式是用來處理節點屬性的,原始碼位置在src/compiler/codegen/index.js
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)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target,only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, 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 dynamic argument wrap
if (el.dynamicAttrs) {
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
}
複製程式碼
總結一下
程式碼生成是模板編譯的第三步,它完成了AST到Render的轉換,即將抽象語法樹AST轉換成可以直接執行的JavaScript字串。
其中genElement的程式碼比較多,因為需要分別處理的情況非常多,這裡只是對genFor和genData的程式碼邏輯進行了說明。