若是你寫過vue,對v-bind這個指令必定不陌生。 下面我將從源碼層面去帶你們剖析一下v-bind背後的原理。css
會從如下幾個方面去探索:html
<p v-bind:title="vBindTitle"></p>
複製代碼
假設爲p標籤v-bind化了title屬性,咱們來分析title屬性在vue中是如何被處理的。前端
vue在拿到這個html標籤以後,處理title屬性,會作如下幾步:vue
createASTElement(... ,attrs, ...)
至於建立以後是如何處理v-bind:title這種普通的屬性值的,能夠在下文的v-bind:src源碼分析中一探究竟。git
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函數
options.start(tagName, attrs, unary, match.start, match.end)
}
}
複製代碼
createASTElement(... ,attrs, ...)
// 解析HMTL
parseHTML(template, {
...
start(tag, attrs, unary, start, end) {
let element: ASTElement = createASTElement(tag, attrs, currentParent) // 注意此處的attrs
}
})
複製代碼
// 建立AST元素
export function createASTElement ( tag: string, attrs: Array<ASTAttr>, // 屬性對象數組 parent: ASTElement | void // 父元素也是ASTElement ): ASTElement { // 返回的也是ASTElement
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
複製代碼
// 聲明一個ASTAttr 屬性抽象語法樹對象 數據類型
declare type ASTAttr = {
name: string; // 屬性名
value: any; // 屬性值
dynamic?: boolean; // 是不是動態屬性
start?: number;
end?: number
};
複製代碼
getBindingAttr及其子函數getAndRemoveAttr在處理特定場景下的v-bind十分有用,也就是」v-bind如何處理不一樣的綁定屬性「章節頗有用。 這裏將其列舉出來供下文v-bind:key源碼分析;v-bind:src源碼分析;v-bind:class源碼分析;v-bind:style源碼分析;v-bind:dataset.prop源碼分析
源碼分析參照。github
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刪除一個屬性,不會從attrsMap刪除
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}
複製代碼
如下面代碼爲例從源碼分析vue是如何獲取v-bind的值。api
會從記下幾個場景去分析:數組
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>
複製代碼
function processKey (el) {
const exp = getBindingAttr(el, 'key')
if(exp){
...
el.key = exp;
}
}
複製代碼
processKey函數中用到了getBindingAttr函數,因爲咱們用的是v-bind,沒有用:
,因此const dynamicValue = getAndRemoveAttr(el, 'v-bind:'+'key');
,getAndRemoveAttr(el, 'v-bind:key')函數到attrsMap中判斷是否存在'v-bind:key',取這個屬性的值賦爲val並從從attrsList刪除,可是不會從attrsMap刪除,最後將'v-bind:key'的值,也就是val做爲dynamicValue,以後再返回解析過濾後的結果,最後將結果set爲processKey中將元素的key property。而後存儲在segments中,至於segments是什麼,在上面的源碼中能夠看到。dom
title是一種「非vue特殊的」也就是普通的HTML attribute。ide
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
}
複製代碼
經過閱讀源碼咱們看出:對於原生的屬性,好比title這樣的屬性,vue會首先解析出name和value,而後再進行一系列的是否有modifiers的判斷(modifier的部分在下文中會詳細講解),最終向更新ASTElement的attrs,從而attrsList和attrsMap也同步更新。
css的class在前端開發的展示層面,是很是重要的一層。 所以vue在對於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函數中,會經過getAndRemoveAttr獲得靜態class,也就是class="foo"
;在getBindingAttr獲得綁定的class,也就是v-bind:class="vBind.class"
即v-bind:class="{ borderRadius: isBorderRadius }"
,將ASTElement的classBinding賦值爲咱們綁定的屬性供後續使用。
style是直接操做樣式的優先級僅次於important,比class更加直觀的操做樣式的一個HTML attribute。 vue對這個屬性也作了特殊的處理。
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函數中,會經過getAndRemoveAttr獲得靜態style,也就是style="{fontSize: '12px'}"
;在getBindingAttr獲得綁定的style,也就是v-bind:style="vBind.style"
即v-bind:class={ minHeight: 100 + 'px' , maxHeight}"
,其中maxHeight是一個變量,將ASTElement的styleBinding賦值爲咱們綁定的屬性供後續使用。
textContent是DOM對象的原生屬性,因此能夠經過prop進行標識。 若是咱們想對某個DOM prop直接經過vue進行set,能夠在DOM節點上作修改。
下面咱們來看源碼。
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>;
複製代碼
經過上面的源碼咱們能夠看出,v-bind:text-content.prop
中的text-content首先被駝峯化爲textContent(這是由於DOM property都是駝峯的格式),vue還對innerHtml錯誤寫法作了兼容也是有心,以後再經過prop標識符,將textContent屬性增長到ASTElement的props中,而這裏的props本質上也是一個ASTAttr。
有一個很值得思考的問題:爲何要這麼作?與HTML attribute有何異同?
v-bind:title.attr,v-bind:text-content.prop
只不過vue默許不加修飾符的就是HTML attribute罷了.camel僅僅是駝峯化,很簡單。 可是.sync就不是這麼簡單了,它會擴展成一個更新父組件綁定值的v-on偵聽器。
其實剛開始看到這個.sync修飾符我是一臉懵逼的,可是仔細閱讀一下組件的.sync再結合實際工做,就會發現它的強大了。
<Parent v-bind:foo="parent.foo" v-on:updateFoo="parent.foo = $event" ></Parent>
複製代碼
在vue中,父組件向子組件傳遞的props是沒法被子組件直接經過this.props.foo = newFoo
去修改的。 除非咱們在組件this.$emit("updateFoo", newFoo)
,而後在父組件使用v-on作事件監聽updateFoo事件。如果想要可讀性更好,能夠在$emit的name上改成update:foo,而後v-on:update:foo。
有沒有一種更加簡潔的寫法呢??? 那就是咱們這裏的.sync操做符。 能夠簡寫爲:
<Parent v-bind:foo.sync="parent.foo"></Parent>
複製代碼
而後在子組件經過this.$emit("update:foo", newFoo);
去觸發,注意這裏的事件名必須是update:xxx的格式,由於在vue的源碼中,使用.sync修飾符的屬性,會自定生成一個v-on:update:xxx的監聽。
下面咱們來看源碼:
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是連字符化函數,其中camelize是駝峯化函數
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)
}
}
複製代碼
經過閱讀源碼咱們能夠看到: 對於v-bind:foo.sync的屬性,vue會判斷屬性是否爲動態屬性。 若不是動態屬性,首先爲其增長駝峯化後的監聽,而後再爲其增長一個連字符的監聽,例如v-bind:foo-bar.sync,首先v-on:update:fooBar,而後v-on:update:foo-bar。v-on監聽是經過addHandler加上的。 如果動態屬性,就不駝峯化也不連字符化了,經過addHandler(el,
update:${name}, ...)
,老老實實監聽那個動態屬性的事件。
一句話歸納.sync: .sync是一個語法糖,簡化v-bind和v-on爲v-bind.sync和this.$emit('update:xxx')。爲咱們提供了一種子組件快捷更新父組件數據的方式。
參考資料: