vue元件定義methods使用箭頭函式
直接從問題開始吧。
第一種情況程式碼:
<template>
<button @click="sayHello">say hellow</button>
</template>
<script>
export default {
methods: {
sayHello() {
console.log('hello:',this);
}
}
}
</script>
複製程式碼
執行結果:
第二種情況:
<template>
<button @click="sayHello">say hellow</button>
</template>
<script>
export default {
methods: {
sayHello: () => {
console.log('hello:',this);
}
}
}
</script>
複製程式碼
執行結果:
你能解釋為什麼會這樣麼?
vue原始碼分析——從模板解析到執行時事件繫結
我們先通過原始碼來分析一下整個流程(vue@2.5.17的dist/vue.common.js)。
v-on的解析
分析事件繫結,先去找v-on的實現程式碼:
通過搜尋,我定位了這樣一段程式碼。
processAttrs這個函式是處理模板中的屬性的,其中有個分支是處理 v-on指令的
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函式的過程。
handler程式碼生成
其實我們之前分析的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,沒做任何處理。
vdom的執行時解析
接下來就是render函式渲染的vdom的解析生成真實dom了,我們只需要看事件繫結的部分,所以搜尋addEventListener,然後你會發現這段程式碼。
這貌似是我們要找的程式碼,往上查詢呼叫add$1的地方,
看到updateDOMListeners這個函式名,就可以確定找對了,這裡呼叫了updateListeners函式,
這裡的on就是handlers,而cur就是具體的handler,也就是說我們sayHello就是在這裡繫結到了元素上。
vue元件初始化
但是我們還沒有看到對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的處理。分析的流程不代表執行的流程,執行時還是從元件初始化開始的。
react元件的使用箭頭函式定義
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和js中this繫結的區別
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指向只有在執行時才能確定。