翻译:疯狂的技术宅原文:https://overreacted.io/why-do…
本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章
我听说 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 增加对普通类的支持时,曾经打算用这样的语法。定义 constructor 和调用 super(props) 始终是一个临时的解决方案,可能要等到类字段能够提供在工程学上不那么反人类的替代方案。
不过还是让我们回到前面这个例子,这次只用 ES2015 的特性:
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = {isOn: true};
}
// …
}
为什么我们要调用 super?可以调用它吗?如果必须要调用,不传递 prop 参数会发生什么?还有其他参数吗?接下来我们试一试:
在 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!’);
}
但是我们忘记了 super() 在设置 this.name 之前先调用了 this.greetColleagues()。所以此时 this.name 还没有定义!如你所见,像这样的代码很难想到问题出在哪里。
为了避免这类陷阱,JavaScript 强制要求:如果想在构造函数中使用 this,你必须首先调用 super。先让父类做完自己的事!这种限制同样也适用于被定义为类的 React 组件:
constructor(props) {
super(props);
// ✅ 在这里可以用 `this`
this.state = {isOn: true};
}
这里又给我们留下了另一个问题:为什么要传 props 参数?
你可能认为将 props 传给 super 是必要的,这可以使 React.Component 的构造函数可以初始化 this.props:
// Inside React
class Component {
constructor(props) {
this.props = props;
// …
}
}
这与正确答案很接近了 —— 实际上它就是这么做的。
但是不知道为什么,即便是你调用 super 时没有传递 props 参数,仍然可以在 render 和其他方法中访问 this.props。(不信你可以亲自去试试!)
这是究竟是为什么呢?事实证明,在调用构造函数后,React 也会在实例上分配 props:
// Inside React
const instance = new YourComponent(props);
instance.props = props;
因此,即使你忘记将 props 传给 super(),React 仍然会在之后设置它们。这是有原因的。
当 React 添加对类的支持时,它不仅仅增加了对 ES6 类的支持。它的目标是尽可能广泛的支持类抽象。目前还不清楚 ClojureScript、CoffeeScript、ES6、Fable、Scala.js、TypeScript 或其他解决方案是如何相对成功地定义组件的。所以 React 故意不关心是否需要调用 super() —— 即使是 ES6 类。
那么这是不是就意味着你可以写 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
}
// …
}
如果这种情况发生在从构造函数调用的某个方法中,可能会给调试工作带来很大的麻烦。这就是为什么我建议总是调用 super(props),即使在没有必要的情况之下:
class Button extends React.Component {
constructor(props) {
super(props); // ✅ 传递了 props 参数
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
}
// …
}
这样就确保了能够在构造函数退出之前设置好 this.props。
最后一点是长期以来 React 用户总是感到好奇的。
你可能已经注意到,当你在类中使用 Context API 时(无论是旧版的 contextTypes 或在 React 16.6 中新添加的 contextType API),context 会作为第二个参数传递给构造函数。
那么为什么我们不写成 super(props, context) 呢?我们可以这样做,但是使用 context 的频率较低,所以这个坑并没有那么多影响。
根据类字段提案的说明,这些坑大部分都会消失。如果没有显式构造函数,则会自动传递所有参数。这允许在像 state = {} 这样的表达式中包含对 this.props 或 this.context 的引用(如果有必要的话)。
而有了 Hooks 之后,我们甚至不再有 super 或 this。不过这是另外一个的话题了。
本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章