如果你寫(xiě)過(guò)vue名秀,對(duì)v-bind這個(gè)指令一定不陌生象踊。
下面我將從源碼層面去帶大家剖析一下v-bind背后的原理温亲。
會(huì)從以下幾個(gè)方面去探索:
- v-bind關(guān)鍵源碼分析
- v-bind化的屬性統(tǒng)一存儲(chǔ)在哪里:attrsMap與attrsList
- 綁定屬性獲取函數(shù) getBindingAttr 和 屬性操作函數(shù) getAndRemoveAttr
- v-bind如何處理不同的綁定屬性
- v-bind:key源碼分析
- v-bind:title源碼分析
- v-bind:class源碼分析
- v-bind:style源碼分析
- v-bind:text-content.prop源碼分析
- v-bind的修飾符.camel .sync源碼分析
v-bind關(guān)鍵源碼分析
v-bind化的屬性統(tǒng)一存儲(chǔ)在哪里:attrsMap與attrsList
<p v-bind:title="vBindTitle"></p>
假設(shè)為p標(biāo)簽v-bind化了title屬性,我們來(lái)分析title屬性在vue中是如何被處理的杯矩。
vue在拿到這個(gè)html標(biāo)簽之后栈虚,處理title屬性,會(huì)做以下幾步:
- 解析HTML史隆,解析出屬性集合attrs魂务,在start回調(diào)中返回
- 在start回調(diào)中創(chuàng)建ASTElement,
createASTElement(... ,attrs, ...)
- 創(chuàng)建后ASTElement會(huì)生成attrsList和attrsMap
至于創(chuàng)建之后是如何處理v-bind:title這種普通的屬性值的,可以在下文的v-bind:src源碼分析中一探究竟粘姜。
解析HTML鬓照,解析出屬性集合attrs,在start回調(diào)中返回
function handleStartTag (match) {
...
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
...
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
...
if (options.start) {
// 在這里上傳到start函數(shù)
options.start(tagName, attrs, unary, match.start, match.end)
}
}
在start回調(diào)中創(chuàng)建ASTElement相艇,createASTElement(... ,attrs, ...)
// 解析HMTL
parseHTML(template, {
...
start(tag, attrs, unary, start, end) {
let element: ASTElement = createASTElement(tag, attrs, currentParent) // 注意此處的attrs
}
})
創(chuàng)建后ASTElement會(huì)生成attrsList和attrsMap
// 創(chuàng)建AST元素
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>, // 屬性對(duì)象數(shù)組
parent: ASTElement | void // 父元素也是ASTElement
): ASTElement { // 返回的也是ASTElement
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
attrs的數(shù)據(jù)類(lèi)型定義
// 聲明一個(gè)ASTAttr 屬性抽象語(yǔ)法樹(shù)對(duì)象 數(shù)據(jù)類(lèi)型
declare type ASTAttr = {
name: string; // 屬性名
value: any; // 屬性值
dynamic?: boolean; // 是否是動(dòng)態(tài)屬性
start?: number;
end?: number
};
綁定屬性獲取函數(shù) getBindingAttr 和 屬性操作函數(shù) getAndRemoveAttr
getBindingAttr及其子函數(shù)getAndRemoveAttr在處理特定場(chǎng)景下的v-bind十分有用颖杏,也就是”v-bind如何處理不同的綁定屬性“章節(jié)很有用。
這里將其列舉出來(lái)供下文v-bind:key源碼分析;v-bind:src源碼分析;v-bind:class源碼分析;v-bind:style源碼分析;v-bind:dataset.prop源碼分析
源碼分析參照坛芽。
export function getBindingAttr (
el: ASTElement,
name: string,
getStatic?: boolean
): ?string {
const dynamicValue =
getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
const staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
}
// note: this only removes the attr from the Array (attrsList) so that it
// doesn't get processed by processAttrs.
// By default it does NOT remove it from the map (attrsMap) because the map is
// needed during codegen.
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1) // 從attrsList刪除一個(gè)屬性留储,不會(huì)從attrsMap刪除
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}
如何獲取v-bind的值
以下面代碼為例從源碼分析vue是如何獲取v-bind的值。
會(huì)從記下幾個(gè)場(chǎng)景去分析:
- 常見(jiàn)的key屬性
- 綁定一個(gè)普通html attribute:title
- 綁定class和style
- 綁定一個(gè)html DOM property:textContent
vBind:{
key: +new Date(),
title: "This is a HTML attribute v-bind",
class: "{ borderRadius: isBorderRadius }"
style: "{ minHeight: 100 + 'px' , maxHeight}"
text-content: "hello vue v-bind"
}
<div
v-bind:key="vBind.key"
v-bind:title="vBind.title"
v-bind:class="vBind.class"
v-bind:style="vBind.style"
v-bind:text-content.prop="vBind.textContent"
/>
</div>
v-bind:key源碼分析
function processKey (el) {
const exp = getBindingAttr(el, 'key')
if(exp){
...
el.key = exp;
}
}
processKey函數(shù)中用到了getBindingAttr函數(shù)咙轩,由于我們用的是v-bind获讳,沒(méi)有用:
,所以const dynamicValue = getAndRemoveAttr(el, 'v-bind:'+'key');
活喊,getAndRemoveAttr(el, 'v-bind:key')函數(shù)到attrsMap中判斷是否存在'v-bind:key'丐膝,取這個(gè)屬性的值賦為val并從從attrsList刪除,但是不會(huì)從attrsMap刪除钾菊,最后將'v-bind:key'的值帅矗,也就是val作為dynamicValue,之后再返回解析過(guò)濾后的結(jié)果煞烫,最后將結(jié)果set為processKey中將元素的key property浑此。然后存儲(chǔ)在segments中,至于segments是什么滞详,在上面的源碼中可以看到凛俱。
v-bind:title源碼分析
title是一種“非vue特殊的”也就是普通的HTML attribute。
function processAttrs(el){
const list = el.attrsList;
...
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
...
addAttr(el, name, value, list[i], ...)
}
}
export const bindRE = /^:|^\.|^v-bind:/
export function addAttr (el: ASTElement, name: string, value: any, range?: Range, dynamic?: boolean) {
const attrs = dynamic
? (el.dynamicAttrs || (el.dynamicAttrs = []))
: (el.attrs || (el.attrs = []))
attrs.push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false
}
通過(guò)閱讀源碼我們看出:對(duì)于原生的屬性料饥,比如title這樣的屬性蒲犬,vue會(huì)首先解析出name和value,然后再進(jìn)行一系列的是否有modifiers的判斷(modifier的部分在下文中會(huì)詳細(xì)講解)岸啡,最終向更新ASTElement的attrs原叮,從而attrsList和attrsMap也同步更新。
v-bind:class源碼分析
css的class在前端開(kāi)發(fā)的展現(xiàn)層面凰狞,是非常重要的一層篇裁。
因此vue在對(duì)于class屬性也做了很多特殊的處理。
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticClass = getAndRemoveAttr(el, 'class')
if (staticClass) {
el.staticClass = JSON.stringify(staticClass)
}
const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
if (classBinding) {
el.classBinding = classBinding
}
}
在transfromNode函數(shù)中赡若,會(huì)通過(guò)getAndRemoveAttr得到靜態(tài)class,也就是class="foo"
团甲;在getBindingAttr得到綁定的class逾冬,也就是v-bind:class="vBind.class"
即v-bind:class="{ borderRadius: isBorderRadius }"
,將ASTElement的classBinding賦值為我們綁定的屬性供后續(xù)使用。
v-bind:style源碼分析
style是直接操作樣式的優(yōu)先級(jí)僅次于important身腻,比class更加直觀的操作樣式的一個(gè)HTML attribute产还。
vue對(duì)這個(gè)屬性也做了特殊的處理。
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticStyle = getAndRemoveAttr(el, 'style')
if (staticStyle) {
el.staticStyle = JSON.stringify(parseStyleText(staticStyle))
}
const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
if (styleBinding) {
el.styleBinding = styleBinding
}
}
在transfromNode函數(shù)中嘀趟,會(huì)通過(guò)getAndRemoveAttr得到靜態(tài)style脐区,也就是style="{fontSize: '12px'}"
;在getBindingAttr得到綁定的style她按,也就是v-bind:style="vBind.style"
即v-bind:class={ minHeight: 100 + 'px' , maxHeight}"
牛隅,其中maxHeight是一個(gè)變量,將ASTElement的styleBinding賦值為我們綁定的屬性供后續(xù)使用酌泰。
v-bind:text-content.prop源碼分析
textContent是DOM對(duì)象的原生屬性媒佣,所以可以通過(guò)prop進(jìn)行標(biāo)識(shí)。
如果我們想對(duì)某個(gè)DOM prop直接通過(guò)vue進(jìn)行set陵刹,可以在DOM節(jié)點(diǎn)上做修改默伍。
下面我們來(lái)看源碼。
function processAttrs (el) {
const list = el.attrsList
...
if (bindRE.test(name)) { // v-bind
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
}
if (modifiers && modifiers.prop) {
addProp(el, name, value, list[i], isDynamic)
}
}
}
export function addProp (el: ASTElement, name: string, value: string, range?: Range, dynamic?: boolean) {
(el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false
}
props?: Array<ASTAttr>;
通過(guò)上面的源碼我們可以看出衰琐,v-bind:text-content.prop
中的text-content首先被駝峰化為textContent(這是因?yàn)镈OM property都是駝峰的格式)也糊,vue還對(duì)innerHtml錯(cuò)誤寫(xiě)法做了兼容也是有心,之后再通過(guò)prop標(biāo)識(shí)符羡宙,將textContent屬性增加到ASTElement的props中狸剃,而這里的props本質(zhì)上也是一個(gè)ASTAttr。
有一個(gè)很值得思考的問(wèn)題:為什么要這么做辛辨?與HTML attribute有何異同捕捂?
- 沒(méi)有HTML attribute可以直接修改DOM的文本內(nèi)容,所以需要單獨(dú)去標(biāo)識(shí)
- 比通過(guò)js去手動(dòng)更新DOM的文本節(jié)點(diǎn)更加快捷斗搞,省去了查詢dom然后替換文本內(nèi)容的步驟
- 在標(biāo)簽上即可看到我們對(duì)哪個(gè)屬性進(jìn)行了v-bind指攒,非常直觀
- 其實(shí)v-bind:title可以理解為
v-bind:title.attr,v-bind:text-content.prop
只不過(guò)vue默許不加修飾符的就是HTML attribute罷了
v-bind的修飾符.camel .sync源碼分析
.camel僅僅是駝峰化僻焚,很簡(jiǎn)單允悦。
但是.sync就不是這么簡(jiǎn)單了,它會(huì)擴(kuò)展成一個(gè)更新父組件綁定值的v-on偵聽(tīng)器虑啤。
其實(shí)剛開(kāi)始看到這個(gè).sync修飾符我是一臉懵逼的隙弛,但是仔細(xì)閱讀一下組件的.sync再結(jié)合實(shí)際工作,就會(huì)發(fā)現(xiàn)它的強(qiáng)大了狞山。
<Parent
v-bind:foo="parent.foo"
v-on:updateFoo="parent.foo = $event"
></Parent>
在vue中全闷,父組件向子組件傳遞的props是無(wú)法被子組件直接通過(guò)this.props.foo = newFoo
去修改的。
除非我們?cè)诮M件this.$emit("updateFoo", newFoo)
萍启,然后在父組件使用v-on做事件監(jiān)聽(tīng)updateFoo事件总珠。若是想要可讀性更好屏鳍,可以在$emit的name上改為update:foo,然后v-on:update:foo。
有沒(méi)有一種更加簡(jiǎn)潔的寫(xiě)法呢局服?钓瞭??
那就是我們這里的.sync操作符淫奔。
可以簡(jiǎn)寫(xiě)為:
<Parent v-bind:foo.sync="parent.foo"></Parent>
然后在子組件通過(guò)this.$emit("update:foo", newFoo);
去觸發(fā)山涡,注意這里的事件名必須是update:xxx的格式,因?yàn)樵趘ue的源碼中唆迁,使用.sync修飾符的屬性鸭丛,會(huì)自定生成一個(gè)v-on:update:xxx的監(jiān)聽(tīng)。
下面我們來(lái)看源碼:
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(el,`update:${camelize(name)}`,syncGen,null,false,warn,list[i])
// Hyphenate是連字符化函數(shù)媒惕,其中camelize是駝峰化函數(shù)
if (hyphenate(name) !== camelize(name)) {
addHandler(el,`update:${hyphenate(name)}`,syncGen,null,false,warn,list[i])
}
} else {
// handler w/ dynamic event name
addHandler(el,`"update:"+(${name})`,syncGen,null,false,warn,list[i],true)
}
}
通過(guò)閱讀源碼我們可以看到:
對(duì)于v-bind:foo.sync的屬性系吩,vue會(huì)判斷屬性是否為動(dòng)態(tài)屬性。
若不是動(dòng)態(tài)屬性妒蔚,首先為其增加駝峰化后的監(jiān)聽(tīng)穿挨,然后再為其增加一個(gè)連字符的監(jiān)聽(tīng),例如v-bind:foo-bar.sync肴盏,首先v-on:update:fooBar科盛,然后v-on:update:foo-bar。v-on監(jiān)聽(tīng)是通過(guò)addHandler加上的菜皂。
若是動(dòng)態(tài)屬性贞绵,就不駝峰化也不連字符化了,通過(guò)addHandler(el,
update:${name}, ...)
恍飘,老老實(shí)實(shí)監(jiān)聽(tīng)那個(gè)動(dòng)態(tài)屬性的事件榨崩。
一句話概括.sync:
.sync是一個(gè)語(yǔ)法糖,簡(jiǎn)化v-bind和v-on為v-bind.sync和this.$emit('update:xxx')章母。為我們提供了一種子組件快捷更新父組件數(shù)據(jù)的方式母蛛。
參考資料:
https://cn.vuejs.org/v2/api/#v-bind
https://github.com/vuejs/vue/tree/dev/src
https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6