講道理,React中,我們為什麼需要寫 super(props)?

螢火之未發表於2019-03-04

這篇文章源自 Dan 的部落格

現在的熱點是 hooks,所以 Dan 決定寫一篇關於 class 元件的文章 ?。

文章中描述的問題,應該不會影響你寫程式碼;不過如果你想深入研究 React 是怎麼工作的,這篇文章可能會對你有幫助。

第一個問題:


我自己都不知道我寫了多少遍 super(props)

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}
複製程式碼

當然,class 屬性提案 可以簡化程式碼:

class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}
複製程式碼

2015 年初的時候,React 0.13 版本就已經計劃支援該語法。在此之前,我們需要不斷地寫 constructor,然後再呼叫 super(props)

現在我們先回顧一下之前的寫法:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}
複製程式碼

我們為什麼要呼叫 super?能不能不呼叫?如果呼叫的時候不傳入props呢?還可以傳入其他引數麼?帶著這些問題往下看。


在 JavaScript 中,super 引用的是父類建構函式。(在 React 中,引用的自然就是 React.Component

需要注意的是,在呼叫父類建構函式之前,無法用 this。其實這不是 React 的限制,而是 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!`);
  }
}
複製程式碼

如果 JavaScript 允許在呼叫 super 之前使用 this,一個月之後,我們需要修改 greetColleagues 方法,方法中使用了 name 屬性:

greetColleagues() {
  alert(`Good morning folks!`);
  alert(`My name is ` + this.name + `, nice to meet you!`);
}
複製程式碼

不過我們可能已經忘了 this.greetColleagues(); 是在呼叫 super 之前呼叫的;因此,this.name 還沒有賦值。這樣的程式碼,很難定位 bug。

為了避免這樣的錯誤,JavaScript 強制開發者在建構函式中先呼叫 super,才能使用this這一限制,也被應用到了 React 元件:

constructor(props) {
  super(props);
  // ✅ 現在可以用 `this` 啦
  this.state = { isOn: true };
}
複製程式碼

問題又來了:為什麼要傳入 props 呢?


你可能以為必須給 super 傳入 props,否則 React.Component 就沒法初始化 this.props

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}
複製程式碼

en…離真相不遠 —— 事實上,React 也的確這麼幹了

不過,如果你不小心漏傳了 props,直接呼叫了 super(),你仍然可以在 render 和其他方法中訪問 this.props(不信的話可以試試嘛)。

為啥這樣也行?因為React 會在建構函式被呼叫之後,會把 props 賦值給剛剛建立的例項物件:

// Inside React
const instance = new YourComponent(props);
instance.props = props;
複製程式碼

props 不傳也能用,是有原因的。

React 新增對 class 支援的時候,不僅僅要支援 ES6 的 class,還需要考慮其他的 class 實現, CoffeeScript, ES6, Fable, Scala.js, TypeScript 中 class 的使用方式並不一致。所以,即使有了 ES6 class,在呼叫 super()這個問題上,React 沒做太多限制。

但這意味著你在使用 React 時,可以用 super() 代替 super(props) 了麼?

別這麼幹,因為會帶來其他問題。 雖然 React 會在建構函式執行之後,為 this.props 賦值,但在 super() 呼叫之後與建構函式結束之前, this.props 仍然是沒法用的。

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Inside your code
class Button extends React.Component {
  constructor(props) {
    super(); // ? 忘了傳入 props
    console.log(props); // ✅ {}
    console.log(this.props); // ? undefined
  }
  // ...
}
複製程式碼

要是建構函式中呼叫了某個訪問 props 的方法,那這個 bug 就更難定位了。因此我強烈建議始終使用super(props),即使這不是必須的:

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ We passed props
    console.log(props); // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}
複製程式碼

上面的程式碼確保 this.props 始終是有值的。


還有一個問題可能困擾 React 開發者很久了。你應該已經注意到,當你在 class 中使用 Context API 時(無論是之前的 contextTypes 還是現在的 contextType API),context 都是作為建構函式的第二個引數。

我們為什麼不用寫 super(props, context)?我們當然可以這麼寫,不過 context API 用的相對較少,所以引發的 bug 也比較少。

感謝class 屬性提案 ,這樣的 bug 幾近絕跡。只要沒有顯式宣告建構函式,所有引數都會被自動傳遞。所以,在state = {} 表示式中,你可以訪問this.props 以及 this.context


相關文章