源碼分析:vue和react組件事件綁定中的this

vue組件定義methods使用箭頭函數(shù)

直接從問(wèn)題開始吧。

第一種情況代碼:

<template>
  <button  @click="sayHello">say hellow</button>
</template>
<script>
  export default {
    methods: {
      sayHello() {
        console.log('hello:',this);
      }
    }
  }
</script>

運(yùn)行結(jié)果:

第二種情況:

<template>
  <button  @click="sayHello">say hellow</button>
</template>
<script>
  export default {
    methods: {
      sayHello: () => {
        console.log('hello:',this);
      }
    }
  }
</script>

運(yùn)行結(jié)果:

你能解釋出為什么會(huì)這樣么?

vue源碼分析——從模板解析到運(yùn)行時(shí)事件綁定

我們先通過(guò)源碼來(lái)分析一下整個(gè)流程(vue@2.5.17的dist/vue.common.js)。

v-on的解析

分析事件綁定,先去找v-on的實(shí)現(xiàn)代碼:

dist/vue.common.js

通過(guò)搜索,我定位了這樣一段代碼。

這個(gè)函數(shù)是處理模板中的屬性的,其中有個(gè)分支是處理 v-on指令的

v-on:click.native.stop="sayHello"

這里的name就是click,value就是sayHello,而native和stop就是modifiers,el為傳進(jìn)來(lái)的當(dāng)前解析的元素。

addHandler顧名思義就是給當(dāng)前的xx事件綁定一個(gè)handler,我們接著去看addHandler的實(shí)現(xiàn)。

dist/vue.common.js

刪掉了一些無(wú)關(guān)代碼后的addHandler方法如圖,開始是處理各種modifier,然后是創(chuàng)建一個(gè)newHandler,加到事件的handlers數(shù)組中去,因?yàn)槲覀冞@里只綁定了一個(gè)handler,所以走的else的分支。

到這,元素的click已經(jīng)綁定了handler了。

模板編譯流程

說(shuō)起來(lái),通過(guò)搜索定位到某段代碼并不能吧流程看全,我們從模板編譯的入口開始看。

你可以在vue@2.5.17的dist/vue.common.js文件的最后看到:

在Vue上掛了compile這個(gè)屬性,而這個(gè)屬性指向compileToFunctions,從名字可以看出,這個(gè)方法是把模板編譯成函數(shù)的。

通過(guò)搜索,發(fā)現(xiàn)在這個(gè)方法屬于ref$1這個(gè)對(duì)象,而這個(gè)對(duì)象是通過(guò)createCompiler方法創(chuàng)建的。

繼續(xù)搜索,看到他是調(diào)用createCommpilerCreator來(lái)生成的,而createCommpilerCreator通過(guò)注釋可以看到他是有針對(duì)ssr的特殊處理,這里我們不用管,看圖中標(biāo)出的3個(gè)地方,就是模板編譯的3個(gè)階段:parse、optimize、generate。parse是從模板編譯成ast抽象語(yǔ)法樹,ast抽象語(yǔ)法樹優(yōu)化(optimize)之后,通過(guò)generate來(lái)生成最終代碼,可以看到返回的renderer就是我們生成的。這就是模板編譯成render函數(shù)的過(guò)程。

handler代碼生成

其實(shí)我們之前分析的processAttrs就是parse的部分,現(xiàn)在我們關(guān)注的是generate的部分,因?yàn)槲覀円タ磆andler生成的代碼,

從根元素開始生成,繼續(xù)去看genElement

可以看到處理了static、once、for、if等指令,處理了template,slot等特殊標(biāo)簽,然后判斷了是不是組件,我們這里明顯不是,所以走到了genData$2這個(gè)函數(shù)。

這個(gè)函數(shù)是處理vnode的各種屬性,我們這里只關(guān)注events的handler,所以繼續(xù)去看genHandlers

這里只是對(duì)native和非native的events分別做了處理,加上了前綴on或者nativeOn,繼續(xù)去看genHandler

我們沒有modifier所以,是這個(gè)分支。

我們知道v-bind的值可以是

sayHello
function() {alert('hello');}   或   () => {alert('hello');}
sayHello($event);

這3種方式吧,通過(guò)正則表達(dá)式判斷出了方法路徑(methodPath),函數(shù)表達(dá)式(functionExpression)這兩種方式。

(其實(shí)看到正則表達(dá)式我就犯暈,感嘆想要寫模板解析必須正則表達(dá)式得很熟啊)

我們開始的sayHello屬于方法路徑的方式,所以直接返回sayHello。

至此,我們已經(jīng)完成了模板到render函數(shù)的解析,判斷出了最終生成的handler就是sayHello,沒做任何處理。

vdom的運(yùn)行時(shí)解析

接下來(lái)就是render函數(shù)渲染的vdom的解析生成真實(shí)dom了,我們只需要看事件綁定的部分,所以搜索addEventListener,然后你會(huì)發(fā)現(xiàn)這段代碼。

這貌似是我們要找的代碼,往上查找調(diào)用add$1的地方,

看到updateDOMListeners這個(gè)函數(shù)名,就可以確定找對(duì)了,這里調(diào)用了updateListeners函數(shù),

這里的on就是handlers,而cur就是具體的handler,也就是說(shuō)我們sayHello就是在這里綁定到了元素上。

vue組件初始化

但是我們還沒有看到對(duì)this的處理啊,這是因?yàn)槲覀冎治隽四0搴蛂ender部分,沒有分析組件對(duì)option中methods的處理。

這里的initMixin就是初始化的過(guò)程,會(huì)處理options

點(diǎn)進(jìn)去以后,你會(huì)發(fā)現(xiàn)

這說(shuō)明vue對(duì)state的定義就是包含data、props、computed、methods和watch的,這和react的state定義差別挺大。

我們看initMethods部分,這部分是我們所關(guān)心的。

看到這里已經(jīng)找到我們想要的東西了:組件在init的時(shí)候會(huì)把所有methods都給綁定到vm上。

箭頭函數(shù)的解析

還記得我們?cè)撻_始的問(wèn)題是什么嗎?

剛開始的問(wèn)題是為什么this打印的是undefined,這里已經(jīng)綁定到this了啊。

這時(shí)候我們打開babel官網(wǎng),輸入這段代碼:

你發(fā)現(xiàn)箭頭函數(shù)的this是綁定到當(dāng)前上下文,也就是父級(jí)函數(shù)運(yùn)行時(shí)的this的,而我們的組件定義根本沒父級(jí)函數(shù)。

<script>
    export default {
       methods : {
           sayHello: () => {
                   console.log('hello:', this);
           }
      }
   }
</script>

他的this指向全局對(duì)象,在嚴(yán)格模式下,全局對(duì)象就是undfined。

用babel repl驗(yàn)證一下也是這樣。

分析過(guò)程總結(jié)

分析到這里,我們已經(jīng)定位到問(wèn)題是因?yàn)榧^函數(shù)的this綁定到了全局對(duì)象,而全局獨(dú)享在嚴(yán)格模式下為undefined導(dǎo)致。

雖然對(duì)于模板編譯的流程和組件初始化過(guò)程的分析沒多大必要,但是通過(guò)分析,我們知道了3種handler定義方式(方法路徑、函數(shù)表達(dá)式、函數(shù)體)最終生成的函數(shù)代碼的區(qū)別,以及vue組件初始化的時(shí)候會(huì)自動(dòng)把methods的this綁定到組件實(shí)例。

簡(jiǎn)化的運(yùn)行流程如圖所示,我們先是分析了模板編譯的流程,主要是parse階段(把模板解析成ast)和generate階段(根據(jù)ast生成vdom),然后分析了vdom運(yùn)行時(shí)綁定dom handler的過(guò)程,之后又分析了組件初始化時(shí)對(duì)methods的處理。分析的流程不代表運(yùn)行的流程,運(yùn)行時(shí)還是從組件初始化開始的。

react組件的使用箭頭函數(shù)定義

class Hello extends React.Component {
  sayHello = () => {
    console.log('hello', this);
  }
  render() {
    return <button onClick={this.sayHello}>say hello</button>;
  }
}

ReactDOM.render(
  <Hello/>,
  document.getElementById('container')
);

你覺得上面的寫法有問(wèn)題么

是沒有問(wèn)題的,那為什么vue中有問(wèn)題呢,就算vue使用render函數(shù)還是有問(wèn)題,不信你可以試下下面的代碼。


<script>
  export default {
    methods:{
      sayHello: () => {
        console.log('hello:', this);
      }
    },
    render:function (createElement) {
      return createElement('button', {
        on: {
          click: this.sayHello
        }
      },'say Hello')
    }
  }
</script>

打印的this依然是undefined。

為什么同樣的邏輯在vue和react里表現(xiàn)不一樣呢?

其實(shí),是因?yàn)閷懛ǖ牟灰粯?,react的組件定義只是類的聲明,創(chuàng)建實(shí)例后才會(huì)運(yùn)行,而創(chuàng)建組件實(shí)例時(shí),會(huì)初始化this,這時(shí)候this自然指向組件對(duì)象。而vue的組件定義是對(duì)象式的寫法,在定義的過(guò)程中箭頭函數(shù)就已經(jīng)綁定到了當(dāng)前上下文,而這時(shí)候組件還沒創(chuàng)建,這時(shí)候this就是undefined。

所以,react組件的定義時(shí)方法可以使用箭頭函數(shù),而vue的組件定義時(shí)methods不可以使用箭頭函數(shù)。

java和js中this綁定的區(qū)別

java是純面向?qū)ο蟮恼Z(yǔ)言,通過(guò)new + 類的構(gòu)造器的方式創(chuàng)建出對(duì)象以后,對(duì)象的方法里this永遠(yuǎn)指向該對(duì)象,也就是對(duì)象在創(chuàng)建好的那一刻,this就永遠(yuǎn)固定了。

js既有面向?qū)ο蟮某煞郑仓С置嫦蜻^(guò)程的寫法,在js里函數(shù)作為一種對(duì)象類型而存在。這就導(dǎo)致了函數(shù)時(shí)可以被多個(gè)對(duì)象引用的,并且也可以作為一種變量而存在。

java從機(jī)制上保證了方法是只屬于一個(gè)類的對(duì)象的,沒法被別的類或變量共享,this自然永遠(yuǎn)不變。而js因?yàn)榘押瘮?shù)當(dāng)作一種對(duì)象類型,自然也就可以被多個(gè)對(duì)象或變量共享,那么this就只能在運(yùn)行時(shí)動(dòng)態(tài)確定了。

java就像封建社會(huì),方法是一輩子只能嫁給一個(gè)類,this永遠(yuǎn)不變,而js就像現(xiàn)代社會(huì),函數(shù)是可以隨時(shí)改變所屬對(duì)象的,需要運(yùn)行時(shí)才能確定。

也正因?yàn)檫@樣的語(yǔ)言特性,使得this成為了js開發(fā)無(wú)處不在的一個(gè)問(wèn)題。

總結(jié)

通過(guò)vue源碼的模板編譯和組件初始化時(shí)methods的處理,以及babel對(duì)箭頭函數(shù)的轉(zhuǎn)譯等方面進(jìn)行分析,確定了vue組件中methods使用箭頭函數(shù)寫法,this為undefind的原因:對(duì)象式的定義方式下methods綁定到了全局對(duì)象,所以就算使用render函數(shù)替代模板也不能解決問(wèn)題。

而react中使用箭頭函數(shù)定義方法是沒問(wèn)題的,因?yàn)轭愂降穆暶鲗懛ǎ笤趧?chuàng)建對(duì)象時(shí)才會(huì)去解析執(zhí)行,render時(shí)this已經(jīng)指向組件對(duì)象了。

之后通過(guò)java中方法和js中方法的區(qū)別,通過(guò)內(nèi)存結(jié)構(gòu)圖說(shuō)明了為什么this是js中很常見的一個(gè)問(wèn)題。

總之,因?yàn)閖s中函數(shù)是一種對(duì)象類型,在堆中分配空間,所以函數(shù)的指向是可以修改的,this指向只有在運(yùn)行時(shí)才能確定。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容