React 組件中處理 onClick
類似事件綁定的時候,是需要顯式給處理器綁定上下文(context)的,這一度使代碼變得冗余和難看。
請看如下的示例:
class App extends Component {
constructor() {
super();
this.state = {
isChecked: false
};
}
render() {
return (
<span class="pl-k"><</span>div className<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>App<span class="pl-pds">"</span></span><span class="pl-k">></span>
<span class="pl-k"><</span>label <span class="pl-k">></span>
check me<span class="pl-k">:</span>
<span class="pl-k"><</span>input
type<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>checkbox<span class="pl-pds">"</span></span>
checked<span class="pl-k">=</span>{<span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">isChecked</span>}
onChange<span class="pl-k">=</span>{<span class="pl-c1">this</span>.<span class="pl-smi">toggleCheck</span>}
<span class="pl-k">/</span><span class="pl-k">></span>
<span class="pl-k"><</span><span class="pl-k">/</span>label<span class="pl-k">></span>
<span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span>
);
}
toggleCheck() {
this.setState(currentState => {
return {
isChecked: !currentState.isChecked
};
});
}
}
頁面上放了一個 checkbox
元素,點擊之后切換其選中狀態。這是很直觀的一段代碼,但並不會像你想的那樣正常工作。
因為 checkbox
的 onChange
事件處理器中,找不到 React 組件的 setState
方法,這說明其執行時的上下文不是該組件,而是別的什么東西,具體我們來調試下。
出乎意料,是 undefined
,這個方法在一個完全野生的環境下執行,沒有任何上下文。
WHY
當然這並不是 React 的鍋,這是 JavaScript 中 this
的工作原理。具體可參見 Chapter 2: this All Makes Sense Now! 來追溯其底層原因,簡單來講 this
的值取決於函數調用的方式。
默認的綁定
function display(){
console.log(this)
}
display() // 嚴格模式下為全局 window
,非嚴格模式下為 undefined
隱式綁定
通過對象來調用,該函數的上下文被隱式地指定為該對象。
var obj = {
name: 'Nobody',
display: function(){
console.log(this.name);
}
};
obj.display(); // Nobody. 里面取的是 obj 身上的 `name` 屬性
但,如果把該對象上的方法賦值給其他變量,或通過參數傳遞的形式,再執行,那光景就又不一樣了。
var obj = {
name: "Nobody",
display: function() {
console.log(this.name);
}
};
var name = "global!";
var outerDisplay = obj.display;
outerDisplay(); // global! 這里取到的 name
是全局中的內個
這里賦值給 outerDisplay
后再調用,等同於調用一個普通函數,而不是對象中的那個,所以此時 this
為全局對象,剛好全局里面有定義一個 name
變量。同樣地,如果是嚴格模式下,因為此時 this
為 undefined
,所以訪問不到所謂的 undefiend.name
,於是會拋錯。
function invoker(fn) {
fn();
}
setTimeout( obj.display, 1000 ); // global!
invoker(obj.display); // global!
這里 setTimeout
調用的時候,因為它的簽名實際上是 setTimeout(fn,delay)
,所以,可以理解為將 obj.display
賦值給了它的入參 fn
,實際上執行的是 fn
而不再是對象上的方法了。對於 invoker
函數也是一樣的道理。
強制綁定
這個時候,bind
就成了那個拯救世界的英雄,任何時間我們都可以通過它來顯式地指定函數的執行上下文。
var name = “global!”;
obj.display = obj.display.bind(obj);
var outerDisplay = obj.display;
outerDisplay(); // Nobody
bind
將指定的上下文與函數綁定后返回一個新的函數,這個新函數再拿去賦值或傳參什么的都不會對其上下文產生影響了,執行時始終是我們指定的那個。
現場還原
有了上面的背景,就可以還原文章開頭的問題了,即事件處理器的上下文 丟失的問題。
JSX 中的 HTML 標簽本質上對應 React 中創建該標簽的一個函數。比如你寫的 div
編譯會其實是 React.createElement(‘div’)
。所以當你書寫 <Input>
時其實是調用了 React.createElement 來創建一個 <Input>
標簽。
React.createElement(
type,
[props],
[...children]
)
標簽上的屬性會作為 props
參數傳遞給 createElement
函數。
<Input onChange={this.toggleCheck}>
表示將組件中的 toggleCheck
方法賦值給 createElement
的入參 props
(props
是個對象,接收所有書寫在標簽上的屬性,),實際調用的時候一如上面所說的,調用的已經不是組件中的 toggleCheck
方法了。
React.createElement(type, props){
// 讓我們創建一個 <type> 並在 <type> 的值發生變化的時候調用一下 `props.onChange`
...
props.onChange() // 它已經不是原來的方法了,丟失了上下文
...
}
因為 ES6 的 Class 是在嚴格模式下執行的,所以事件處理器中如果使用了 this
那它就是 undefined
。
所以你看到 React 官方的示例中,constructor 里有 bind(this)
的語句就不奇怪了,就是為了糾正這個事件處理器歪了的執行上下文。
constructor() {
super();
this.state = {
isChecked: false
};
+ this.toggleCheck = this.toggleCheck.bind(this);
}
這樣是能正常工作了,但是,這句代碼的存在真的很別扭,因為,
- 對於業務來說,毫無意義,徒增代碼量
- 很丑陋,每加一個處理器就要加一條這樣的綁定
- 冗余,這樣重復的代碼大量冗余在項目中,在搜索中混淆了原本的方法
避免的方式有很多,就看哪種最對味。下面來看看如何避免寫這些綁定方法。
#0行內的綁定
最簡單的可以在行內進行綁定操作,這樣不用單獨寫一句出來。
<input
type="checkbox"
checked={this.state.isChecked}
- onChange={this.toggleCheck}
+ onChange={this.toggleCheck.bind(this)}
/>
#1箭頭函數
因為箭頭函數不會創建新的作用域,其上下文是語義上的(lexically)上下文。所以在綁定事件處理器時,直接使用剪頭函數是很方便的一種規避方法。
<input
type="checkbox"
checked={this.state.isChecked}
- onChange={this.toggleCheck}
+ onChange={() => this.toggleCheck()}
/>
#2將類的方法改成屬性
如果將這個處理器作為該組件的一個屬性,這個屬性作為事件的處理器以箭頭函數的形式存在,執行的時候也是能正常獲取到上下文的。
- toggleCheck() {
+ toggleCheck = () => {
this.setState(currentState => {
return {
isChecked: !currentState.isChecked
};
});
}
總結
React 組件中,其實跟 React 沒多大關系,傳遞事件處理器,或方法作為回調時,其上下文會丟失。為了修復,我們需要顯式地給這個方法綁定一下上下文。除了常用的在構造器中進行外,還可通過箭頭函數,公有屬性等方式來避免冗余的綁定語句。