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)代碼:

通過(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)。

刪掉了一些無(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í)才能確定。