翻译:为什么我们需要些super(props)

本文出自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。但这是下次的话题了