楔子
筆者公司的前端小組掀起了Vue源碼學(xué)習(xí)小組,前后幾個(gè)月的共同學(xué)習(xí)肝劲,讓小組成員都已經(jīng)對(duì)Vue對(duì)大致框架有了個(gè)模糊對(duì)輪廓。
現(xiàn)在已經(jīng)進(jìn)入第二階段:整理。
我們將小組分為四個(gè)部分碧库,vue對(duì)整理也分為三個(gè)大模塊:數(shù)據(jù)綁定、從template到vnode巧勤、vnode轉(zhuǎn)化為dom對(duì)patch嵌灰。
筆者對(duì)小組被分到了template到vnode對(duì)部分,拿到手后颅悉,感覺內(nèi)容比較多沽瞭,就先將內(nèi)容根據(jù)源碼對(duì)布局分為兩小塊:parse 和 render過程。
再者剩瓶,古人云:兵馬未動(dòng)驹溃,糧草先行。筆者打算先介紹思想和思路儒搭,然后在具體到一些細(xì)枝末節(jié)吠架。
本文準(zhǔn)備到就是parse部分思想。
正文
一搂鲫、整個(gè)編譯過程的目的
我們都知道使用vue到時(shí)候傍药,我們使用定制樣式到幾種方式大致為:
- 1.options template。
- 2.el魂仍。(掛載的節(jié)點(diǎn))
- 3.render 方法拐辽。
我們需要明白到是無論是哪種方式,我們最終的目的都是
生成以vnode為單位vdom樹
而生成的vdom樹擦酌,最終是用于patch過程中俱诸,生成真實(shí)dom節(jié)點(diǎn)。
所以我們的編譯的最終目的是獲得:
Virtue dom赊舶。
形式大致如下圖:
暫時(shí)不用管每個(gè)屬性的作用睁搭,我們已經(jīng)知道我們的目標(biāo)是怎樣的了。那么還需要知道的是 起始點(diǎn)和過程笼平。
正常情況下园骆,如果我們這樣初始化的話:
new Vue({
el: '#el',
template: `
<div>
<div v-for="(item,index) in options" :key="item.id">
{{item.id}}
<div>{{item.text}}</div>
</div>
</div>
`,
data: {
name:"dinglei",
options: [
{ id: 1, text: 'Hello' },
{ id: 2, text: 'World' }
]
}
})
那我們的初始化的狀態(tài)則是 template的 一串 String。
當(dāng)然初始化為String的寫法還是滿多的寓调。 比如:
- 不傳template锌唾,直接使用使用$el。
- template,使用一個(gè)模版晌涕。
所以我們從的轉(zhuǎn)換過程滋捶,就是一串String 到 virtue dom的過程。
但是值得一提的是余黎,vue 從模版到vdom的過程并非是直接一次性到位的過程重窟。可能是因?yàn)橛却蟮脑O(shè)計(jì)的vnode和原生的dom屬性差距過大惧财,直接編譯成vnode不好完成亲族。其次是考慮到性能優(yōu)化等方面。
所以vue的編譯過程其實(shí)是分為三個(gè)過程:
- parse
- optimize
- codegen for render And render
分別對(duì)應(yīng)三個(gè)過程:
- 從模版到 astElement
- 優(yōu)化添加標(biāo)記staticRoot可缚,當(dāng)然這個(gè)層面的static是用于codegen里面的。
- codegen 生成 render函數(shù)斋枢,render 綁定實(shí)例后執(zhí)行生成vnode帘靡。
接下來我們將進(jìn)入本文的主題
二、parse瓤帚、optimize描姚、codegen的核心思想解讀。
1)parse解讀
首先需要明白astElement包括哪些屬性戈次。在vue源碼 flow文件目錄下的compiler.js可以找到astElement的模型
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<ASTAttr>;
attrsMap: { [key: string]: any };
rawAttrsMap: { [key: string]: ASTAttr };
parent: ASTElement | void;
children: Array<ASTNode>;
start?: number;
end?: number;
processed?: true;
static?: boolean;
staticRoot?: boolean;
staticInFor?: boolean;
staticProcessed?: boolean;
hasBindings?: boolean;
text?: string;
attrs?: Array<ASTAttr>;
dynamicAttrs?: Array<ASTAttr>;
props?: Array<ASTAttr>;
plain?: boolean;
pre?: true;
ns?: string;
component?: string;
inlineTemplate?: true;
transitionMode?: string | null;
slotName?: ?string;
slotTarget?: ?string;
slotTargetDynamic?: boolean;
slotScope?: ?string;
scopedSlots?: { [name: string]: ASTElement };
ref?: string;
refInFor?: boolean;
if?: string;
ifProcessed?: boolean;
elseif?: string;
else?: true;
ifConditions?: ASTIfConditions;
for?: string;
forProcessed?: boolean;
key?: string;
alias?: string;
iterator1?: string;
iterator2?: string;
staticClass?: string;
classBinding?: string;
staticStyle?: string;
styleBinding?: string;
events?: ASTElementHandlers;
nativeEvents?: ASTElementHandlers;
transition?: string | true;
transitionOnAppear?: boolean;
model?: {
value: string;
callback: string;
expression: string;
};
directives?: Array<ASTDirective>;
forbidden?: true;
once?: true;
onceProcessed?: boolean;
wrapData?: (code: string) => string;
wrapListeners?: (code: string) => string;
// 2.4 ssr optimization
ssrOptimizability?: number;
// weex specific
appendAsTree?: boolean;
};
整個(gè)流程我們需要的歸納為:
- 識(shí)別器(parseHTML) (1)
- 存儲(chǔ)棧(stack) (2)
- 創(chuàng)建函數(shù)(createASTElement) (3)
- 上下文(currentParent)(4)
整個(gè)工作方式:
- 1.就是識(shí)別器(1)利用正則從前到后識(shí)別所有敏感字段轩勘。如(標(biāo)簽、事件怯邪、迭代绊寻、數(shù)據(jù)綁定、插槽等等)
- 2.open標(biāo)簽 如
<div>{{test}}</div>的<div>
悬秉,識(shí)別器識(shí)別到此類標(biāo)簽澄步,就會(huì)將div 存放到stack當(dāng)中,利用創(chuàng)建函數(shù)(3)創(chuàng)建createASTElement并處理大部分非組件屬性(具體如v-model和泌、v-if村缸、v-for)。需要注意到是:- 1.一元標(biāo)簽如:img等武氓,會(huì)直接走3處理梯皿。
- p標(biāo)簽會(huì)做特殊處理,目的是為了和瀏覽器等識(shí)別方式達(dá)到目標(biāo)一致县恕。具體點(diǎn)擊此處
- 3.識(shí)別到閉合標(biāo)簽东羹,則處理下原生屬性如:id 、 class等等弱睦、還有ref百姓、slot、component况木、key等等垒拢,并跟上下文(4)建立起父子關(guān)系旬迹。最終形成了樹的結(jié)構(gòu)。
其中的細(xì)節(jié)特別多求类,如果有興趣請(qǐng)關(guān)注我們的動(dòng)態(tài)奔垦,我們會(huì)在下期給出詳細(xì)過程講解。
2) optimize思想解讀尸疆。
我們?cè)谏厦嬉呀?jīng)給出了astElement的詳細(xì)屬性椿猎,其中有兩個(gè)屬性叫做staticRoot 和 staticInFor。而optimize 的過程寿弱,就是給astElement打上這兩個(gè)標(biāo)記犯眠。這里是為了讓這類靜態(tài)節(jié)點(diǎn),在render過程症革,能夠走緩存的方式筐咧,只渲染一次。
好處很明顯噪矛,能夠減少重復(fù)對(duì)比和渲染的過程量蕊,提高性能。
3)code generate解讀
這個(gè)過程其實(shí)還沒有生成vnode艇挨,而是生成一個(gè)執(zhí)行函數(shù)残炮,且包含了this的執(zhí)行code,其格式如下:
with(this) { _c('div'...xxx...xxx) }
我們?cè)趶?qiáng)調(diào)下缩滨,這次又是從astElement直接到另一個(gè)String势就。
上面astElement是個(gè)樹狀結(jié)構(gòu)。
然后在這個(gè)過程脉漏,基本一個(gè)astElement就對(duì)應(yīng)一個(gè)短函數(shù)蛋勺。
最基本短函數(shù)是createElement 也就是_c。
最終的樹狀結(jié)構(gòu)鸠删,會(huì)以函數(shù)的形式表現(xiàn)處理函數(shù)如下
_c(
'div',
{
key:'xxx',
ref:'xx',
pre:'xxx',
domPro:xxx,
....
},
[ // chidren
_v(_s('ding')),
_c('p',{model:'isshow',}, [ ...xxx ])
]
)
可以清晰的看到抱完,最終形成的string,依然是一個(gè)樹狀形式刃泡,是以function形式展示的樹狀巧娱,而且所有屬性都已經(jīng)抽離成createElement的第二個(gè)參數(shù)。
一句話概括下code generate做的事情就是:
生成vnode的前置工作烘贴,抽離astElement所有的屬性禁添,形成短函數(shù)鏈。
短函數(shù)對(duì)應(yīng)大致如下:
export function installRenderHelpers (target: any) {
target._o = markOnce // v-once
target._n = toNumber
target._s = toString
target._l = renderList // v-for
target._t = renderSlot // slot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic // static
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots // scopeSlot
target._g = bindObjectListeners // linsners
target._d = bindDynamicKeys
target._p = prependModifier
}
4) render 函數(shù)桨踪。
這個(gè)過程是直接執(zhí)行老翘,就會(huì)得到vnode,其細(xì)節(jié)包括了component的處理,會(huì)有單獨(dú)一節(jié)去介紹铺峭。
這里展示下vnode的格式墓怀。
declare interface VNodeData {
key?: string | number;
slot?: string;
ref?: string;
is?: string;
pre?: boolean;
tag?: string;
staticClass?: string;
class?: any;
staticStyle?: { [key: string]: any };
style?: string | Array<Object> | Object;
normalizedStyle?: Object;
props?: { [key: string]: any };
attrs?: { [key: string]: string };
domProps?: { [key: string]: any };
hook?: { [key: string]: Function };
on?: ?{ [key: string]: Function | Array<Function> };
nativeOn?: { [key: string]: Function | Array<Function> };
transition?: Object;
show?: boolean; // marker for v-show
inlineTemplate?: {
render: Function;
staticRenderFns: Array<Function>;
};
directives?: Array<VNodeDirective>;
keepAlive?: boolean;
scopedSlots?: { [key: string]: Function };
model?: {
value: any;
callback: Function;
};
};
總結(jié)
- 1.編譯四大過程parse、optimize卫键、codegen傀履、render。
- 2.用戶自定義的render其實(shí)是忽略了前面三個(gè)步驟莉炉,直接定制的方式钓账。
- 3.vdom 最終是以vnode為節(jié)點(diǎn)的樹狀結(jié)構(gòu),在patch過程絮宁,會(huì)以新老vdom進(jìn)行對(duì)比梆暮,來決定哪些dom是需要更新或添加、刪除的绍昂。