不管是什麼樣的歷史原因,或者是基於什麼樣的考慮。反正現在我們已經接受了
JavaScript
中的this
的多面性,以及樂此不疲的使用this
這種多面性,來編寫靈活的程式碼,比如借用其他物件的方法,改變回撥函式的呼叫者等,但有時候我們還是希望this
能夠老實一點,別讓我們花費很大精力去找尋他。
快速找到
- 函式包裝模式 function wrapper pattern
- 渲染繫結模式 render bind pattern
- 覆寫繫結模式 rewrite bind pattern
- 屬性賦值模式 attribute assignment pattern
- 高階函式渲染繫結模式 higher-order function render bind pattern
- 高階函式覆寫繫結模式 higher-order function rewrite bind pattern
- 屬性getter渲染繫結模式 attribute getter render bind pattern
- 屬性getter賦值繫結模式 attribute getter assignment bind pattern
說明
由於本文是主要介紹React中鎖定this
的N種方法,不會過多的介紹this
多面性的原因,相信大家應該都知道詞法作用域
和動態作用域
。並且也知道在es6
之前我們依然有很多種方式,去鎖定this
的指向(call, apply, bind)。接下來我們也會結合這些方式,在React
中來鎖定this
;不過在這之前,我們先看下之前我們採取的方式。對於本文標題facade pattern
(外觀模式)指的是,這些鎖定this
的方式,只是看起來不一樣,有些本質上是一樣的,有些是es5通過一些技巧實現的
,有些是es6原生支援的(箭頭函式)
,這些看起來很不一樣的方式,有些在babel
編譯以後本質是一樣的(使用閉包鎖住上下文, 通過高階函式返回新的函式)。
千好萬好ES6
好(箭頭函式 () => {}好)
看下在es6之前我們是如何解決this
// 回撥裡面使用this
var Demo = {
init() {
this.initEvents();
}
validateData() {
return true;
}
initEvents() {
var self = this;
$(`a.submit`).click(function () {
self.validateData();
});
}
}
Demo.init();
// 借用其他物件的方法
var name = `影帝`;
var Person = {
name: `渣渣輝`,
sayName() {
console.log(this.name);
}
};
var Other = {
name: `張家輝`
};
var sayName = Person.sayName;
sayName(); // `影帝`
sayName.call(Other); // 張家輝`
Other.sayName = sayName;
var otherSayName = Other.sayName;
otherSayName(); // `影帝`
Other.sayName.call(Person); `渣渣輝`,
複製程式碼
這樣看起來好奇怪呀,但是沒辦法,我們早已經習慣,自從ES2015
稱為標準以來,我們已經很少看到這種程式碼了。好了言歸正題,我們開始看看React
中this
的問題。
React中this的問題
既然js
中有這些問題,當然react
也不能列外,使用react
的我們都知道,為了保持元件的高複用,元件可以分為容器元件
和UI元件
,容器元件
元件負責業務處理,UI元件
負責頁面渲染,一般情況下UI元件
都儘量要是純的
,沒有自己的狀態,也不處理業務,但是有時候需要觸發一些事件,這樣就需要執行從父元件等傳過來的函式,同時這些函式裡面一般還會出現this
,我們希望this
的指向是上層元件的引用,而這個時候函式的執行者卻不是上層元件,於是this
開始變臉,變得我們不認識。但是我們要避免這種情況,就需要鎖定this
,鎖定this
的方式有很多,我們一一分析,這其中各有優劣,也有react
推薦的最佳實踐
。至於如何選擇,看業務場景,以及團隊編碼風格,建議最好還是遵守最佳實踐
;
React中‘鎖定’this的N種方法
1.函式包裝模式
/**
* 函式包裝模式 function wrapper pattern
*/
class Component extends React.Component {
doSomething() {
childDoSomething() {}
render() {
return (
<div onClick={() => this.doSomething();}>
<ChildComponent doSomething={() => this.childDoSomething();} />
</div>
);
}
}
複製程式碼
建議: 不推薦 也不禁止。
優缺點:
- 缺點:沒有明顯的缺點,只是需要多包裹一層
- 優點:簡單,易於理解,對新手比較友好
實現原理:
這裡是通過es6
箭頭函式來實現this
的鎖定;
當然對應的有es5
版本,其實就是我們之前熟悉的那種方式,且看程式碼
/**
* function wrapper pattern es5
*/
class Component extends React.Component {
doSomething() {
childDoSomething() {}
render() {
const self = this;
return (
<div onClick={function () { self.doSomething();}>
<ChildComponent doSomething={function () { self.childDoSomething();} />
</div>
);
}
}
複製程式碼
建議: 不推薦, 最好不要這樣寫。
優缺點:
- 缺點:需要對this的指向進行儲存,導致程式碼沒有箭頭函式來的簡潔, 優雅(其實也就是箭頭函式的優點)
- 優點:對熟悉es5老式寫法的比較友好
實現原理:
使用變數先保持對this
的引用,使用的時候是這個變數,也就是此函式外部的this
;
2.渲染繫結模式
/**
* 渲染繫結模式 render bind pattern
* 或者叫
* 懶繫結模式 lazy bind pattern
*/
class Component extends React.Component {
doSomething() {}
childDoSomething() {}
render() {
return (
<div onClick={this.doSomething.bind(this)}>
<ChildComponent doSomething={this.childDoSomething.bind(this);} />
</div>
);
}
}
複製程式碼
建議: 禁止採取這種模式
優缺點:
- 缺點:有效能隱患,每次
render
都會重新繫結 - 優點:好像也只有看起來稍微好看,不用像在
constructor
裡面一樣重新輔助
實現原理:
就是使用bing
來鎖定
,關於bind
的使用以及原理可以參考mdn
或者網上其他文章或者教程,當然你也可以實現自己的bind
;
3.覆寫繫結模式
/**
* 覆寫繫結模式rewrite bind pattern
* 或者叫
* 預繫結模式 prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomething = this.doSomething.bind(this);
this.childDoSomething = this.childDoSomething.bind(this);
}
doSomething() {}
childDoSomething() {}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
複製程式碼
建議: 建議採用這種方式,也是react最佳實踐
推薦的寫法。
優缺點:
- 缺點:需要在建構函式裡面重寫需要繫結
this
的方法,如果這類方法比較多了,就不是那麼的優雅了,不過尚可以接受。 - 優點:
react最佳實踐
推薦,也是最常見的方式,效能較好,只會繫結一次
實現原理:
和渲染時繫結模式實現原理一樣,只是在這種方式下是提前繫結好。
對比:
這種模式和上一種在render
時繫結實現原理是一樣的,這種方式只會繫結一次,效能是好於在render
裡面的繫結;對比下來在寫法上面也有些區別,一個是在constructor
提前繫結,一個是在準備要用的時候懶繫結。
4.屬性賦值模式
/**
* 屬性賦值模式 attribute assignment pattern
*/
class Component extends React.Component {
doSomething = () => {}
childDoSomething = () => {}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
複製程式碼
建議: 可以採用,react最佳實踐
也有推薦的這種寫法。
優缺點:
- 缺點:不被標準所支援(babel以後是沒有問題的),寫法怪怪的, 要是有很多這種寫法, 不夠優雅。
- 優點:寫法很簡單(雖然很怪),不用顯式的
bind
。
實現原理:
借用箭頭函式在定義的時候就繫結好了this
。
5.高階函式渲染繫結模式
/**
* 高階函式渲染繫結模式 higher-order function render bind pattern
* 或者叫 高階函式懶繫結模式 higher-order function lazy bind pattern
*/
class Component extends React.Component {
doSomething(data) {
return () => {
// 使用this, data
}
}
childDoSomething(data) {
return () => {
// 使用this, data
}
}
render() {
return (
<div onClick={this.doSomething()}>
<ChildComponent doSomething={this.childDoSomething()} />
</div>
);
}
}
複製程式碼
建議: 可以採用,嘗試函式式寫法。
優缺點:
- 缺點:不熟悉高階函式(或者函式式),接受起來有難度,需要呼叫一次, 每次都產生新的函式。
- 優點:優雅,高階函式, 可以提前儲存一些變數。
實現原理:
利用高階函式返回箭頭函式, 實現this
的鎖定。
當然這種方法有對應的es5
版本
/**
* higher-order function es5 pattern
*/
class Component extends React.Component {
doSomething(data) {
cosnt self = this;
return function() {
// self, data
}
}
childDoSomething(data) {
cosnt self = this;
return function() {
// self, data
}
}
render() {
return (
<div onClick={this.doSomething()}>
<ChildComponent doSomething={this.childDoSomething()} />
</div>
);
}
}
複製程式碼
注意:
這種模式被我稱為懶模式,和在render
裡面使用bind
的方式很像,在準備要使用的時候才繫結,每次都產生一個新的函式,可能會帶來效能問題。當然又這種懶模式,我們也有提前繫結模式。
6.高階函式覆寫繫結模式
/**
* 高階函式覆寫繫結模式 higher-order function rewrite bind pattern
* 或者叫
* 高階函式預繫結模式 higher-order function prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomething = this.doSomething();
this.childDoSomething = this.childDoSomething();
}
doSomething(data) {
return () => {
// 使用this, data
}
}
childDoSomething(data) {
return () => {
// 使用this, data
}
}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
複製程式碼
建議: 可以採用,嘗試函式式寫法。
優缺點:
- 缺點:不熟悉高階函式(或者函式式),接受起來有難度,需要呼叫一次。
- 優點:沒有顯式繫結,在某些場景下可以提前儲存一些變數, 對比上一種模式效能較好。
實現原理:
和上一個高階函式渲染繫結模式
一樣利用高階函式返回箭頭函式, 實現this
的鎖定。不同的是這個模式是在建構函式裡面提前呼叫。繫結後函式只會產生一次。
當然這種方法有對應的es5
版本, 和上個模式的es5
版本很像,也是通過變數快取this
, 不同就是在constructor
裡面呼叫一次函式,而不是在render
裡面。
我是分割線,到了最後一種方式了
7.屬性getter渲染繫結模式
/**
* 屬性getter渲染繫結模式 attribute getter render bind pattern
* 或者叫
* 屬性getter懶繫結模式 attribute getter lazy bind pattern
*/
class Component extends React.Component {
get doSomething() {
// 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
get childDoSomething() {
// 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
複製程式碼
建議: 可以採用,嘗試新的寫法。
優缺點:
- 缺點:接受起來需要成本, 每次產生新的函式。
- 優點:被標準所支援,沒有顯式繫結,沒有顯式呼叫,比較簡潔優雅,可以提前做一些屬性的聚合或者計算。
實現原理:
借用屬性的getter
, 返回一個箭頭函式繫結this
;
說明:
這種模式和高階函式很像,都是返回一個新的函式,這種模式在特定情況下很強大,簡潔的同時,可以對當前物件的一些屬性做一些計算(是不是很像Vue
的計算屬性), 這種模式下每次getter
後返回的都是一個新的函式,可能會有效能問題,但是如果對其他屬性進行了聚合計算,或者說是依賴其他屬性的最新值
,就需要在render
裡面getter
,以保證依賴的屬性都是乾淨的值
(最新的值);
當然大家知道里面返回的是箭頭函式,肯定也有es5
版本,其實和其他模式的es5
版本都很像,在這裡就不寫了。既然這種模式下有可能產生效能問題,對比其他模式,我們可定也有預繫結模式
。請往下看
8.屬性getter賦值繫結模式
/**
* 屬性getter賦值繫結模式attribute getter assignment bind pattern
* 或者叫
* 屬性getter預繫結模式 attribute getter prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomethingBind = this.doSomething;
this.childDoSomethingBind = this.childDoSomething
}
get doSomething() {
// 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
get childDoSomething() {
// 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
render() {
return (
<div onClick={this.doSomethingBind}>
<ChildComponent doSomething={this.childDoSomethingBind} />
</div>
);
}
}
複製程式碼
建議: 可以採用,嘗試新的寫法。
優缺點:
- 缺點:接受起來需要成本,賦值的函式需要另外一個名字 。
- 優點:被標準所支援,沒有顯式繫結,只產生一次函式,比較簡潔優雅,可以提前做一些屬性的聚合或者計算。
實現原理:
借用屬性的getter
, 返回一個箭頭函式繫結this
;賦值給物件的另外一個屬性,呼叫的是另外一個方法。
說明:
這種模式和上一種模式區別在於,提前繫結,只會產生一次函式。但是要注意不是重寫函式,而是賦值給另外一個不同的方法名,可能大家覺得這種換名字不夠好,但是換個角度考慮一下,系物件多了一個方法,同時又持有之前的getter
,這樣可以更加的靈活,可以選擇性的使用這兩種函式。
同其他一些返回箭頭的模式一樣,這種模式依然有es5
版本,寫法同上,不在贅述。
總結
上面列舉的這些模式,不一定是全部寫法,不過足以應對工作中的大多數場景,同時有些模式還可以讓我們去接觸另外的實現方式。列舉了這麼多種,每一種都有優劣,工作中可以選擇性的去使用,看場景和團隊風格。
展望
既然JavaScript
中this
的問題一直困擾著我們,那麼有沒有一種方式可以不使用this
,就可以實現我們想要的所有功能,答案是肯定的。React 16.7.0-alpha
版本加入了特別神奇的hooks
(好像Vue 3.0裡面也已經加入了相似的特性),可以讓我們徹底擺脫this
的困擾(當然this
依然是js
裡面一個神奇的存在),同時讓我們的程式碼更加函式式
,更大程度的複用處理邏輯
,當然這個特性還在等待成為事實標準,我們希望這一天很快到來,不過我們仍然可以現在就是使用它。
相關連結
1.可以讓我們不用關心this繫結問題(react hooks 官方文件)
2.關於hooks 的一些問題(react hooks 官方文件)