原始碼分析:vue和react元件事件繫結中的this

凌霄光發表於2018-10-23

vue元件定義methods使用箭頭函式

直接從問題開始吧。

第一種情況程式碼:

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

執行結果:

原始碼分析:vue和react元件事件繫結中的this

第二種情況:

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

執行結果:

原始碼分析:vue和react元件事件繫結中的this

你能解釋為什麼會這樣麼?

vue原始碼分析——從模板解析到執行時事件繫結

我們先通過原始碼來分析一下整個流程(vue@2.5.17的dist/vue.common.js)。

v-on的解析

分析事件繫結,先去找v-on的實現程式碼:

dist/vue.common.js

通過搜尋,我定位了這樣一段程式碼。

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

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和react元件事件繫結中的this

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

原始碼分析:vue和react元件事件繫結中的this

通過搜尋,發現在這個方法屬於ref$1這個物件,而這個物件是通過createCompiler方法建立的。

原始碼分析:vue和react元件事件繫結中的this

繼續搜尋,看到他是呼叫createCommpilerCreator來生成的,而createCommpilerCreator通過註釋可以看到他是有針對ssr的特殊處理,這裡我們不用管,看圖中標出的3個地方,就是模板編譯的3個階段:parse、optimize、generate。parse是從模板編譯成ast抽象語法樹,ast抽象語法樹優化(optimize)之後,通過generate來生成最終程式碼,可以看到返回的renderer就是我們生成的。這就是模板編譯成render函式的過程。

handler程式碼生成

其實我們之前分析的processAttrs就是parse的部分,現在我們關注的是generate的部分,因為我們要去看handler生成的程式碼,

原始碼分析:vue和react元件事件繫結中的this

從根元素開始生成,繼續去看genElement

原始碼分析:vue和react元件事件繫結中的this

可以看到處理了static、once、for、if等指令,處理了template,slot等特殊標籤,然後判斷了是不是元件,我們這裡明顯不是,所以走到了genData$2這個函式。

原始碼分析:vue和react元件事件繫結中的this

這個函式是處理vnode的各種屬性,我們這裡只關注events的handler,所以繼續去看genHandlers

原始碼分析:vue和react元件事件繫結中的this

這裡只是對native和非native的events分別做了處理,加上了字首on或者nativeOn,繼續去看genHandler

原始碼分析:vue和react元件事件繫結中的this

我們沒有modifier所以,是這個分支。

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

sayHello
function() {alert('hello');}   或   () => {alert('hello');}
sayHello($event);
複製程式碼

這3種方式吧,通過正規表示式判斷出了方法路徑(methodPath),函式表示式(functionExpression)這兩種方式。

原始碼分析:vue和react元件事件繫結中的this

(其實看到正規表示式我就犯暈,感嘆想要寫模板解析必須正規表示式得很熟啊)

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

至此,我們已經完成了模板到render函式的解析,判斷出了最終生成的handler就是sayHello,沒做任何處理。

vdom的執行時解析

接下來就是render函式渲染的vdom的解析生成真實dom了,我們只需要看事件繫結的部分,所以搜尋addEventListener,然後你會發現這段程式碼。

原始碼分析:vue和react元件事件繫結中的this

這貌似是我們要找的程式碼,往上查詢呼叫add$1的地方,

原始碼分析:vue和react元件事件繫結中的this

看到updateDOMListeners這個函式名,就可以確定找對了,這裡呼叫了updateListeners函式,

原始碼分析:vue和react元件事件繫結中的this

這裡的on就是handlers,而cur就是具體的handler,也就是說我們sayHello就是在這裡繫結到了元素上。

vue元件初始化

但是我們還沒有看到對this的處理啊,這是因為我們之分析了模板和render部分,沒有分析元件對option中methods的處理。

原始碼分析:vue和react元件事件繫結中的this

這裡的initMixin就是初始化的過程,會處理options

原始碼分析:vue和react元件事件繫結中的this

點進去以後,你會發現

原始碼分析:vue和react元件事件繫結中的this

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

我們看initMethods部分,這部分是我們所關心的。

原始碼分析:vue和react元件事件繫結中的this

看到這裡已經找到我們想要的東西了:元件在init的時候會把所有methods都給繫結到vm上。

箭頭函式的解析

還記得我們該開始的問題是什麼嗎?

剛開始的問題是為什麼this列印的是undefined,這裡已經繫結到this了啊。

這時候我們開啟babel官網,輸入這段程式碼:

原始碼分析:vue和react元件事件繫結中的this

你發現箭頭函式的this是繫結到當前上下文,也就是父級函式執行時的this的,而我們的元件定義根本沒父級函式。

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

他的this指向全域性物件,在嚴格模式下,全域性物件就是undfined。

原始碼分析:vue和react元件事件繫結中的this

用babel repl驗證一下也是這樣。

分析過程總結

分析到這裡,我們已經定位到問題是因為箭頭函式的this繫結到了全域性物件,而全域性獨享在嚴格模式下為undefined導致。

雖然對於模板編譯的流程和元件初始化過程的分析沒多大必要,但是通過分析,我們知道了3種handler定義方式(方法路徑、函式表示式、函式體)最終生成的函式程式碼的區別,以及vue元件初始化的時候會自動把methods的this繫結到元件例項。

原始碼分析:vue和react元件事件繫結中的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和react元件事件繫結中的this

是沒有問題的,那為什麼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就永遠固定了。

原始碼分析:vue和react元件事件繫結中的this

js既有物件導向的成分,也支援程式導向的寫法,在js裡函式作為一種物件型別而存在。這就導致了函式時可以被多個物件引用的,並且也可以作為一種變數而存在。

原始碼分析:vue和react元件事件繫結中的this

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指向只有在執行時才能確定。

相關文章