我聽說 Hooks 最近很火。諷刺的是,我想以一些關於 class 元件的有趣故事來開始這個部落格。怎樣!(皮一下很開心)
這些小坑並不會影響你高效的使用 React,但如果你願意深入瞭解下背後的工作原理,你會發現它們非常有意思。
這是第一個。
我這輩子寫過的 super(props)
比我想象的要多得多
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
}
// ...
}複製程式碼
當然,class fields proposal 允許我們跳過這個儀式。
class Checkbox extends React.Component {
state = { isOn: true };
// ...
}複製程式碼
這樣的語法是在 2015 年 React 0.13 增加對純 Class 的支援的時候加入 計劃 的. 定義 constructor
和呼叫 super(props)
一直都只是 class fiels 出現之前的臨時解決方案。
然而,讓我們只用 ES2015 的特性來回顧一下這個例子。
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
}
// ...
}複製程式碼
我們為什麼要呼叫super
?能不能不呼叫它?如果非要呼叫,如果不傳 props
會怎樣?還有其它引數嗎?讓我們來看一下。
在 JavaScript 中,super
指代父類的建構函式。(在我們的案例中,它指向 React.Component
這個實現)
重點在於,在你呼叫父類建構函式之前,你無法在建構函式中使用 this
。JavaScript 不會允許你這麼做。
class Checkbox extends React.Component {
constructor(props) {
// ? 這時候還不能使用 `this`
super(props);
// ✅ 現在開始可以了
this.state = { isOn: true };
}
// ...
}複製程式碼
JavaScript 強制你在使用 this
前執行父類建構函式有一個很好的理由。考慮這樣一個類結構:
class Person {
constructor(name) {
this.name = name;
}
}
class PolitePerson extends Person {
constructor(name) {
this.greetColleagues(); // ? 這是不允許的,下面會解釋原因
super(name);
}
greetColleagues() {
alert('Good morning folks!');
}
}複製程式碼
想象一下如果在呼叫 super
前使用 this
是被允許的。一個月之後。我們或許會改變 greetColleagues
把 person 的 name 加到訊息中。
greetColleagues() {
alert('Good morning folks!');
alert('My name is ' + this.name + ', nice to meet you!');
}複製程式碼
但我們忘了 this.greetColleagues()
是在 super()
有機會設定 this.name
之前被呼叫的。this.name
甚至還沒被定義!如你所見,像這樣的程式碼理解起來會很困難。
為了避免這樣的陷阱,JavaScript 強制規定,如果你想在建構函式中只用this
,就必須先呼叫 super
。讓父類做它該做的事!這一限制也適用於定義成類的 React 元件。
constructor(props) {
super(props);
// ✅ 現在可以使用 `this` 了
this.state = { isOn: true };
}複製程式碼
這給我們留下了另一個問題:為什麼要傳 props
?
你或許覺得把 props
傳進 super
是必要的,這使得基類 React.Component
可以初始化 this.props
:
// React 內部
class Component {
constructor(props) {
this.props = props;
// ...
}
}複製程式碼
很接近了——事實上,它就是這麼做的。
然而,即便在呼叫 super()
時沒有傳入 props
引數,你依然能夠在 render
和其它方法中訪問 this.props
。(你要是不相信我,可以自己試一試)
這是什麼原理?其實 React 在呼叫你的建構函式之後,馬上又給例項設定了一遍 props
:
// React 內部
const instance = new YourComponent(props);
instance.props = props;複製程式碼
因此,即便你忘了把 props
傳入 super()
,React 依然會在事後設定它們。這是有理由的。
當 React 新增對 Class 的支援時,它並不是只新增了對 ES6 的支援,而是希望能夠支援儘可能廣泛的 class 抽象。由於不是很確定 ClojureScript、CoffeeScript、ES6、Fable、Scala.js、TypeScript 或其他解決方案誰更適合用來定義元件,React 對於是否有必要呼叫 super()
刻意不表態。
那麼這是否意味著你可以只寫 super()
而不用 super(props)
?
或許並非如此,因為這依然讓人困擾。誠然,React 會在你的建構函式執行之後設定 this.props
。但在 super
呼叫一直到建構函式結束之前,this.props
依然是未定義的。
// React 內部
class Component {
constructor(props) {
this.props = props;
// ...
}
}
// 你的程式碼
class Button extends React.Component {
constructor(props) {
super(); // ? 我們忘了傳入 props
console.log(props); // ✅ {}
console.log(this.props); // ? undefined
}
// ...
}複製程式碼
如果這發生在某些從建構函式中呼叫的函式,除錯起來會更加麻煩。這也是為什麼我推薦總是使用 super(props)
的寫法,即便這是非必要的:
class Button extends React.Component {
constructor(props) {
super(props); // ✅ 我們傳了 props
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
}
// ...
}複製程式碼
這樣的寫法確保了 this.props
即便在建構函式返回之前就被設定好了。
最後還有一點是 React 的長期使用者或許會好奇的。
你或許已經注意到,當你在 Class 中使用 Context API 時(無論是舊版的語法還是 React 16.6 中新增的現代化語法),context 是被作為建構函式的第二個引數傳入的。
那麼我們為什麼不寫 super(props, context)
呢?當然我們可以這麼做,但 context 的使用頻率沒那麼高,所以這個陷阱影響還沒那麼大。
伴隨著 class fields proposal 的釋出,這個問題也就不復存在了。即便不顯式呼叫建構函式,所有引數也會自動傳入。這就允許像 state = {}
這樣的表示式在必要時可以直接引用 this.props.
或 this.context
。
在 Hooks 中,我們甚至都沒有 super
或 this
。這個話題我們擇日再說。