如何理解vue中的v-bind尔邓?

v-bind.jpg

如果你寫(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乳怎,隨后出現(xiàn)的幾起案子彩郊,更是在濱河造成了極大的恐慌,老刑警劉巖蚪缀,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秫逝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡询枚,警方通過(guò)查閱死者的電腦和手機(jī)违帆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)金蜀,“玉大人前方,你說(shuō)我怎么就攤上這事狈醉×停” “怎么了惠险?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)抒线。 經(jīng)常有香客問(wèn)我班巩,道長(zhǎng),這世上最難降的妖魔是什么嘶炭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任抱慌,我火速辦了婚禮,結(jié)果婚禮上眨猎,老公的妹妹穿的比我還像新娘抑进。我一直安慰自己,他們只是感情好睡陪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布寺渗。 她就那樣靜靜地躺著,像睡著了一般兰迫。 火紅的嫁衣襯著肌膚如雪信殊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天汁果,我揣著相機(jī)與錄音涡拘,去河邊找鬼。 笑死据德,一個(gè)胖子當(dāng)著我的面吹牛鳄乏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播棘利,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼橱野,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了赡译?” 一聲冷哼從身側(cè)響起仲吏,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝌焚,沒(méi)想到半個(gè)月后裹唆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡只洒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年许帐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片毕谴。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡成畦,死狀恐怖距芬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情循帐,我是刑警寧澤框仔,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站拄养,受9級(jí)特大地震影響离斩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘪匿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一跛梗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棋弥,春花似錦核偿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至家乘,卻和暖如春蝗羊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仁锯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工耀找, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人业崖。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓野芒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親双炕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子狞悲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • Vue 實(shí)例 屬性和方法 每個(gè) Vue 實(shí)例都會(huì)代理其 data 對(duì)象里所有的屬性:var data = { a:...
    云之外閱讀 2,202評(píng)論 0 6
  • 序言:亂七八糟一鍋粥! 基于Vue.js 教程妇斤、介紹— Vue.js 心得: 在vue中摇锋,推薦始終使用 kebab...
    苦苦修行閱讀 647評(píng)論 0 1
  • # 在本文中,筆者又提煉了以下幾個(gè)重點(diǎn) 補(bǔ)償雙向數(shù)據(jù)綁定 Vue.$set 數(shù)據(jù)偵聽(tīng) Vue.$watch 表單綁...
    果汁涼茶丶閱讀 1,462評(píng)論 1 15
  • vue.js是什么 是一套構(gòu)建用戶界面的漸進(jìn)式框架 vue應(yīng)用組成 一個(gè) Vue 應(yīng)用由一個(gè)通過(guò)new Vue創(chuàng)建...
    多多醬_DuoDuo_閱讀 1,019評(píng)論 0 2
  • 說(shuō)到v-model,就想到了雙向數(shù)據(jù)綁定死相,而且往往最常見(jiàn)的是在表單元素 , , 中的使用融求,在一些自定義組件中也使用...
    趁你還年輕233閱讀 724評(píng)論 0 0