直接從問題開始吧。javascript
第一種狀況代碼:html
<template>
<button @click="sayHello">say hellow</button>
</template>
<script> export default { methods: { sayHello() { console.log('hello:',this); } } } </script>
複製代碼
運行結果:vue
第二種狀況:java
<template>
<button @click="sayHello">say hellow</button>
</template>
<script> export default { methods: { sayHello: () => { console.log('hello:',this); } } } </script>
複製代碼
運行結果:node
你能解釋爲何會這樣麼?react
咱們先經過源碼來分析一下整個流程(vue@2.5.17的dist/vue.common.js)。正則表達式
分析事件綁定,先去找v-on的實現代碼:數組
經過搜索,我定位了這樣一段代碼。bash
processAttrs這個函數是處理模板中的屬性的,其中有個分支是處理 v-on指令的babel
v-on:click.native.stop="sayHello"
複製代碼
這裏的name就是click,value就是sayHello,而native和stop就是modifiers,el爲傳進來的當前解析的元素。
addHandler顧名思義就是給當前的xx事件綁定一個handler,咱們接着去看addHandler的實現。
刪掉了一些無關代碼後的addHandler方法如圖,開始是處理各類modifier,而後是建立一個newHandler,加到事件的handlers數組中去,由於咱們這裏只綁定了一個handler,因此走的else的分支。
到這,元素的click已經綁定了handler了。
提及來,經過搜索定位到某段代碼並不能吧流程看全,咱們從模板編譯的入口開始看。
你能夠在vue@2.5.17的dist/vue.common.js文件的最後看到:
在Vue上掛了compile這個屬性,而這個屬性指向compileToFunctions,從名字能夠看出,這個方法是把模板編譯成函數的。
經過搜索,發如今這個方法屬於ref$1這個對象,而這個對象是經過createCompiler方法建立的。
繼續搜索,看到他是調用createCommpilerCreator來生成的,而createCommpilerCreator經過註釋能夠看到他是有針對ssr的特殊處理,這裏咱們不用管,看圖中標出的3個地方,就是模板編譯的3個階段:parse、optimize、generate。parse是從模板編譯成ast抽象語法樹,ast抽象語法樹優化(optimize)以後,經過generate來生成最終代碼,能夠看到返回的renderer就是咱們生成的。這就是模板編譯成render函數的過程。
其實咱們以前分析的processAttrs就是parse的部分,如今咱們關注的是generate的部分,由於咱們要去看handler生成的代碼,
從根元素開始生成,繼續去看genElement
能夠看處處理了static、once、for、if等指令,處理了template,slot等特殊標籤,而後判斷了是否是組件,咱們這裏明顯不是,因此走到了genData$2這個函數。
這個函數是處理vnode的各類屬性,咱們這裏只關注events的handler,因此繼續去看genHandlers
這裏只是對native和非native的events分別作了處理,加上了前綴on或者nativeOn,繼續去看genHandler
咱們沒有modifier因此,是這個分支。
咱們知道v-bind的值能夠是
sayHello
function() {alert('hello');} 或 () => {alert('hello');}
sayHello($event);
複製代碼
這3種方式吧,經過正則表達式判斷出了方法路徑(methodPath),函數表達式(functionExpression)這兩種方式。
(其實看到正則表達式我就犯暈,感嘆想要寫模板解析必須正則表達式得很熟啊)
咱們開始的sayHello屬於方法路徑的方式,因此直接返回sayHello。
至此,咱們已經完成了模板到render函數的解析,判斷出了最終生成的handler就是sayHello,沒作任何處理。
接下來就是render函數渲染的vdom的解析生成真實dom了,咱們只須要看事件綁定的部分,因此搜索addEventListener,而後你會發現這段代碼。
這貌似是咱們要找的代碼,往上查找調用add$1的地方,
看到updateDOMListeners這個函數名,就能夠肯定找對了,這裏調用了updateListeners函數,
這裏的on就是handlers,而cur就是具體的handler,也就是說咱們sayHello就是在這裏綁定到了元素上。
可是咱們尚未看到對this的處理啊,這是由於咱們之分析了模板和render部分,沒有分析組件對option中methods的處理。
這裏的initMixin就是初始化的過程,會處理options
點進去之後,你會發現
這說明vue對state的定義就是包含data、props、computed、methods和watch的,這和react的state定義差異挺大。
咱們看initMethods部分,這部分是咱們所關心的。
看到這裏已經找到咱們想要的東西了:組件在init的時候會把全部methods都給綁定到vm上。
還記得咱們該開始的問題是什麼嗎?
剛開始的問題是爲何this打印的是undefined,這裏已經綁定到this了啊。
這時候咱們打開babel官網,輸入這段代碼:
你發現箭頭函數的this是綁定到當前上下文,也就是父級函數運行時的this的,而咱們的組件定義根本沒父級函數。
<script>
export default {
methods : {
sayHello: () => {
console.log('hello:', this);
}
}
}
</script>
複製代碼
他的this指向全局對象,在嚴格模式下,全局對象就是undfined。
用babel repl驗證一下也是這樣。
分析到這裏,咱們已經定位到問題是由於箭頭函數的this綁定到了全局對象,而全局獨享在嚴格模式下爲undefined致使。
雖然對於模板編譯的流程和組件初始化過程的分析沒多大必要,可是經過分析,咱們知道了3種handler定義方式(方法路徑、函數表達式、函數體)最終生成的函數代碼的區別,以及vue組件初始化的時候會自動把methods的this綁定到組件實例。
簡化的運行流程如圖所示,咱們先是分析了模板編譯的流程,主要是parse階段(把模板解析成ast)和generate階段(根據ast生成vdom),而後分析了vdom運行時綁定dom handler的過程,以後又分析了組件初始化時對methods的處理。分析的流程不表明運行的流程,運行時仍是從組件初始化開始的。
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')
);
複製代碼
你以爲上面的寫法有問題麼
是沒有問題的,那爲何vue中有問題呢,就算vue使用render函數仍是有問題,不信你能夠試下下面的代碼。
<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裏表現不同呢?
其實,是由於寫法的不同,react的組件定義只是類的聲明,建立實例後纔會運行,而建立組件實例時,會初始化this,這時候this天然指向組件對象。而vue的組件定義是對象式的寫法,在定義的過程當中箭頭函數就已經綁定到了當前上下文,而這時候組件還沒建立,這時候this就是undefined。
因此,react組件的定義時方法可使用箭頭函數,而vue的組件定義時methods不可使用箭頭函數。
java是純面向對象的語言,經過new + 類的構造器的方式建立出對象之後,對象的方法裏this永遠指向該對象,也就是對象在建立好的那一刻,this就永遠固定了。
js既有面向對象的成分,也支持面向過程的寫法,在js裏函數做爲一種對象類型而存在。這就致使了函數時能夠被多個對象引用的,而且也能夠做爲一種變量而存在。
java從機制上保證了方法是隻屬於一個類的對象的,無法被別的類或變量共享,this天然永遠不變。而js由於把函數看成一種對象類型,天然也就能夠被多個對象或變量共享,那麼this就只能在運行時動態肯定了。
java就像封建社會,方法是一生只能嫁給一個類,this永遠不變,而js就像現代社會,函數是能夠隨時改變所屬對象的,須要運行時才能肯定。
也正由於這樣的語言特性,使得this成爲了js開發無處不在的一個問題。
經過vue源碼的模板編譯和組件初始化時methods的處理,以及babel對箭頭函數的轉譯等方面進行分析,肯定了vue組件中methods使用箭頭函數寫法,this爲undefind的緣由:對象式的定義方式下methods綁定到了全局對象,因此就算使用render函數替代模板也不能解決問題。
而react中使用箭頭函數定義方法是沒問題的,由於類式的聲明寫法,以後在建立對象時纔會去解析執行,render時this已經指向組件對象了。
以後經過java中方法和js中方法的區別,經過內存結構圖說明了爲何this是js中很常見的一個問題。
總之,由於js中函數是一種對象類型,在堆中分配空間,因此函數的指向是能夠修改的,this指向只有在運行時才能肯定。