本文出自overreacted,这是Dan Abramov写的博客,我觉得对很有用所以特意做了这个翻译系列,原文链接请查看这里
听说最近好像Hooks是一个热议的话题。可笑的是我的第一篇博客和这个相去甚远,我希望能够描述好组件class的内部巧妙的实现。不知道大家对这点是否有兴趣。
这些内部巧妙的实现可能对在生产环境中使用React并没有特别大的益处,但是针对那些渴望知道React的内部的人来说会觉得非常有趣。
那么第一个例子来了
我这辈子写过无数个super(props),比如:
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
}
// ...
}
当然,类属性提案让我们可以跳过这种写法
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? 我们可以不调用它么?如果我们调用它,如果我们不传props这个参数会发生什么?还可以传其他什么更多的参数么? 让我们来了解一下
在Javascipt中, 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的实现,并在弹出一个消息的时候使用了name的属性:
greetColleagues() {
alert('Good morning folks!');
alert('My name is ' + this.name + ', nice to meet you!');
}
但是我们忘记了this.greetColleagues()在调用父类的构造函数之前被调用了,我们会以为this.name已经被初始化了。但是this.name并没有被定义,如你所见,这样的代码任然非常难理解。
为了避免这样的问题,JavaScript必须让你在使用this之前调用父类的构造函数,你必须先调用父类的构造函数。让父类做它自己的事情!但是这个定义类的限制也同样作用于React的组件定义:
constructor(props) {
super(props);
// ✅ 好啦!现在能够使用`this`了
this.state = { isOn: true };
}
这让我们产生了另一个问题:为什么需要传props
你可能会觉得将props传递到父类的构造函数中,这样父类React.Component的构造函数就能够初始化this.props
// React内部
class Component {
constructor(props) {
this.props = props;
// ...
}
}
其实这里真相已经不远了 - React真正做的事情其实是这样的
接下来有个令人不解的问题就是,即使你在调用父类的构造函数的时候没有传递props参数,你也依然可以在render或者其他的成员函数中访问this.props(如果你不相信我,可以自己尝试一下)
上述的现象是如何产生的?显而易见,这个现象证明了React还会在调用你的构造函数之后,为生成的实例的props赋值
// React内部
const instance = new YourComponent(props);
instance.props = props;
所以即使你忘记掉在调用super()的时候传入props,React依然会在构造函数结束之后将props赋值,这就是产生这个现象的原因
当React要添加对class的支持的时候,这不只是代表着React单纯的只是支持ES6 class,React的目标是尽可能的支持最宽泛的class的概念。当时还不能够确定ClojureScript, CoffeeScript, ES6, Fable, Scala.js, TypeScript或者其他语言那种相对来说用于定义组件会比较好。所以React特意对是否必须调用super()不敢妄自约束 - 即使是ES6的类。
那么难道这就意味着你能够使用super()而不去调用super(props)?
可能还是不可以这么做,因为这还存在一些问题 当然,React会在调用构造函数之后为this.props赋值。但是在调用了super和构造函数结束之间this.props还是undefuned:
// Inside 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
}
// ...
}
你也可以做更多的尝试,比如在一些函数中调用this.props,然后在构造函数中调用这些函数,看看结果如何。这也是为什么我推荐你最好能够总是将props传到父类的构造函数中,即使这并不是严格上必须的
class Button extends React.Component {
constructor(props) {
super(props); // ✅ 我们传入了props
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
}
// ...
}
这确保了this.props在构造函数结束之前就被初始化
这里还有一点,很长的一段时间里,React的使用者都非常的好奇。
你可能注意到,当你在class中使用 context api(无论是老的contextTypes或者现在的React16.6的contextTypeAPI),context会作为第二个参数传递到构造函数中。
但是我们会像super(props, context)这么写么?我们可以这么做,但是context会用的比较低频,相同的context的问题并不会像props这么多。
通过类属性提案,这些问题大多数都能够解决即使没有明确构造函数的定义,所有的参数都能够自动的传入父类,这让我们可以通过类似于state = {}的表达式来赋值this.props或者this.contxt
通过 Hooks,我们甚至可以不使用super或者this。但这是下次的话题了