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

vue組件定義methods使用箭頭函數

直接從問題開始吧。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源碼分析——從模板解析到運行時事件綁定

咱們先經過源碼來分析一下整個流程(vue@2.5.17的dist/vue.common.js)。正則表達式

v-on的解析

分析事件綁定,先去找v-on的實現代碼:數組

dist/vue.common.js

經過搜索,我定位了這樣一段代碼。bash

processAttrs這個函數是處理模板中的屬性的,其中有個分支是處理 v-on指令的babel

v-on:click.native.stop="sayHello"
複製代碼

這裏的name就是click,value就是sayHello,而native和stop就是modifiers,el爲傳進來的當前解析的元素。

addHandler顧名思義就是給當前的xx事件綁定一個handler,咱們接着去看addHandler的實現。

dist/vue.common.js

刪掉了一些無關代碼後的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指向只有在運行時才能肯定。

相關文章
相關標籤/搜索