react如何将一个function执行为class?(How Does React Tell a Class from a Function?)

本文转载,转载地址
考虑下这个用function定义的Greeting Component

function Greeting() {
  return <p>Hello</p>;
}

React同样支持将它定义为class

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

(至今,这是唯一使用state等特性的方式)
当你想要render Greeting 的时候,你并不关心他是如何被定义的

// 无论是class还是function,都是这么使用
<Greeting />

但是react本身是需要区分这两种方式的
如果Greeting是function,那么react需要按照如下方式去调用

// 你的代码
function Greeting() {
  return <p>Hello</p>;
}

// React内部调用
const result = Greeting(props); // <p>Hello</p>

但是如果Greeting是一个class,react需要通过new来实例化并且之后需要调用在刚实例话的对象上的render方法

// 你的代码
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// React内部调用
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

无论哪种情况,react的目标是获取rendered节点(在这个例子中,是 <p>Hello</p>)但是如何操作是依赖Greeting是如何定义的
所以react如何知道一个component是通过function还是class来定义的呢?
就像我之前的文章你不需要在react生产环境下知道这些,我也好多年不知道这个,请不要在面试问题中提这个问题。事实上,这个文章更多讲解的是js层面的内容而不是react。
这个文章是给那些想知道react是如何以正确方式运行的好奇读者们的。你是么?让我们来深究吧~
这是一个漫长的过程。这篇文章并没有多少react本身的信息,但是我们将会浏览new, this, class, 箭头函数,prototype,__proto__, instanceof这些内容,并且他们在js中是如何协同工作的。幸运的是当你使用react的时候,你并不需要考虑上面那些内容,当然如果你要实现一个react的话…
(如果你真的想知道的话,请滑到最底部)


首先我们要理解为什么区分对待functions和classes是件重要的事情。注意我们当访问一个class的时候是如何使用new操作符的:

// 如果Greeting是一个function
const result = Greeting(props); // <p>Hello</p>

//如果Greeting是一个class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

让我们大致了解下new在js中所做的事情


在过去的日子里,js并没有class。然而,你可以通过使用纯函数(plain functions?不知道如何翻译)去实现一个与class相似的模式。具体的,你可以使用任何function表现的类似class constructor函数,在调用之前加一个new操作符。

// 一个function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // ? Won’t work

时至今日你仍然可以这样去写,在开发者工具中试一下吧!
如果你不通过new调用Person('Fred'),内部的this会指向global或者没有用(例如:window或者是undefined)。所以我们的代码将会崩掉或者像是给window.name去赋值一样的蠢事。(严格模式下this会指向undefined,非严格模式下会指向window
通过在调用之前加new,就相当于我们告诉了js编译器:hey,我知道Person是一个function,但是让我们假装他是一个class的constructor函数吧。新建一个对象{}把this指向Person函数的内部吧,那样我就能给他们赋值了比如this.name.然后把这个对象返回给我吧
这就是new操作符做的事情
new操作符也能让在fred对象上调用Person.prototype上的属性、方法

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred');
fred.sayHi();

这就是在js没有添加class之前做的仿class


所以new操作符出现在js中已经有一段时间了。然而,class是之后出的内容,那么就让我们重写我们上面的代码,以便更好的实现我们的意图

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert('Hi, I am ' + this.name);
  }
}

let fred = new Person('Fred');
fred.sayHi();

获取开发者意图在语言和api开发中是重中之重
如果你实现了一个function,js是不知道他应该是被当做像是alert()那样的函数调用还是被当做一个class的构造器new Person()这样去调用。当被作为像是Person的constructor而忘记使用new操作符会有意料之外的行为。
class的语法告诉我们:这不只是一个function-这是一个有constructor的class。如果你在调用他的时候忘记用了new操作符,js直接会报错

let fred = new Person('Fred');
// ✅  如果Person是个function,正常工作
// ✅  如果Person是个class,也会正常工作

let george = Person('George'); // We forgot `new`
// ? 如果Person是一个构造函数:意料之外的行为
// ? 如果Person是个class,立马报错

这将帮我们更早的捕获到一些错误,而不是发生在一些模糊的bug比如this.name会被window.name而不是george.name
然而,这意味着react需要在调用class之前使用new操作符,而不能像是调用一个正常的js中的function,那将会抛异常!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// ? React 当然不能这么做
const instance = Counter(props);

这就麻烦了


在我们看react如何解决这个问题之前,一件重要的事情就是大部分人会使用类似babel这样的编译器来编译react,以便能在更古老的浏览器上使用一些现代特性比如classes。所以我们还需要在设计中考虑编译器的问题
在早些的babel版本中,classes可以不通过new去调用。当然,这个bug已经被fix了,通过一些额外的代码:

function Person(name) {
  // 在babel输出中的部分简化
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // 我们的代码
  this.name = name;
}

new Person('Fred'); // ✅ Okay
Person('George');   // ? Can’t call class as a function

你可能在bundle中看见过这样的代码,那就是哪些_classCallCheck 函数做的事(你可以通过选择“loose mode”不去做检查,以便减少bundle文件的大小,但这将导致你向真正的原生classes的过度变复杂)


到现在,你大概理解了在调用一些东西之前是否使用new的区别

new Person()Person()
class✅ this 是Person的实例? TypeError
function✅ this 是Person的实例this 是 window 或者 undefined

这就是react需要正确的调用你的component的重要性。如果你的conponent被定义为class,React需要在调用之前使用new操作符
所以react是否能检测出来它是不是一个class
并不是那么容易!即使我们可以告诉这个在js中是class,但是这并不会在一些比如babel中的工具中生效。对于浏览器来说,他们还是纯函数,react的运气真糟糕!


那么,是否React可以在每次调用前都是用new去调用?不幸的是,也不行
正常的function来说,通过new调用他们将会给他们一个this对象实例。将fucuntion写作构造函数是可取的行为(像我们上面写的Person),但是对于一些function component依旧会有问题

function Greeting() {
  // 我们这里并不希望有任何的this实例!
  return <p>Hello</p>;
}

当然,这是在容忍范围之内的。还有其他两个问题将这个想法扼杀了。


第一个问题是总是用new对原声的箭头函数并不生效(没有被babel编译过的),他会抛出异常

const Greeting = () => <p>Hello</p>;
new Greeting(); // ? Greeting 并不是构造函数

这个行为是意料之中的,从箭头函数的定义来源上就可以知道。箭头函数一个重要的特点就是他没有自己的this,他的this是最近的有this的外层函数的this

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` 被解析为render的this
        size={this.props.size}
        name={friend.name}
        key={friend.id}
      />
    );
  }
}

好的,由此可见箭头函数没有自己的this,这就以为着他们没有constructor的作用!

const Person = (name) => {
  // ? 这并不会生效
  this.name = name;
}

因此,js不允许通过new来调用箭头函数。如果你这样做了,尽早的告诉你无论如何你都犯了一个错误。这就类似js不允许你不通过new来调用一个class一样的道理。
这很棒但是这却阻碍了我们的计划,react不能什么都用new来调用因为将会对箭头函数报错!我们可以通过箭头函数没有prototype来检测箭头函数,然后不通过new来调用他们

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}

这并不会对babel编译过的functions生效。这不是个大问题但是仍然有另一个原因使这个想法挂掉.


另一个不能这么使用的原因是这将会排除react支持的只返回字符串或者原生类型的components

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // ? Greeting {}

又一次的,这与new操作符当初设计的怪癖有关。就像我们之前看到的,new告诉js 编译器创建了一个对象,然后将对象指向function的this,然后返回这个对象作为new的结果
然而,js也允许通过返回一些其他的对象来覆盖new所创建的对象。当然,这给哪些想复用实例的pooling模式下很有用(单例模式吧?

var zeroVector = null;

function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // 复用实例
      return zeroVector;
    }
    zeroVector = this;
  }
  this.x = x;
  this.y = y;
}

var a = new Vector(1, 1);
var b = new Vector(0, 0);
var c = new Vector(0, 0); // ? b === c

然而,当你返回的不是一个对象的时候,new会完全忽略你的return值。如果你反回了比如数字或者字符串,对于new来说那跟没有返回是一样的。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // ? Answer {}

没有方法去知道当通过new操作符去调用的原始返回值(比如number和string)。所以如果react一直通过new来调用,那么就不能支持哪些返回字符串之类的component!
那是不能接收的,所以我们只能妥协


目前为止我们了解了什么?react需要通过new去调用classes(包括babel输出后的)但是不能用new去调用普通函数和箭头函数(包括babel输出后的)。并没有可靠的方法去区分他们。
如果我们不能解决一个普遍的问题,那么把他具体话之后我们是否可以解决呢?
当你用class定义了一个component,你想继承React.Component,因为你想使用类似this.setState()这些方法。相比于检测所有的class,我们是不是只检测React.Component的子class就可以了呢?
这就是react做的方法


也许,惯用的方式去检测Greeting是否是React.Component的子类是通过 Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true

我想你在想,发生了什么?理解这个我们需要理解js的prototype
你可能对”原型链“比较熟悉。每一个js中的对象都可能会有"prototype"。当我们写fred.sayHi()的时候,fred对象上并没有sayHi这个属性,我们在fred’s prototype上寻找sayHi这个属性。如果没有找到,我们会寻找下一个原型链上的prototype— fred’s prototype’s prototype. 以此类推
疑惑的是,class或者function的propertype属性并不指向他值的prototype。我并不是在开玩笑

function Person() {}

console.log(Person.prototype); // ? Not Person's prototype
console.log(Person.__proto__); // ? Person's prototype

(这块很是迷惑…)
所以原型链更像是__proto__.__proto__.__proto__而不是prototype.prototype.prototype.好多年才理解这个
那么function和class的prototype属性是什么呢?是通过new创建的所有对象的__proto__属性

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`

__proto__链才是js如何寻找属性的方式

fred.sayHi();
// 1. fred有sayHi属性么?没有,
// 2. fred.__proto__上有sayHi属性么,是的,调用他!

fred.toString();
// 1.fred有toString属性么,没有
// 2.fred.__proto__有toSting属性么?没有
// 3. fred.__proto__.__proto__有toString属性么?有,调用他!

在实际中,在你的代码里几乎不需要接触__proto__除非你在调试原型链上的东西。如果你想调用fred.__proto__上面的东西,你应当去调用Person.prototype。至少这是最原始的设计(__propto__没有被纳入浏览器的规范,但是几乎所有的浏览器都实现了他
__proto__终于开始在浏览器端是不应该被实现的因为原型链才是规范概念。但是部分浏览器添加了__proto__所以他也算是被标准化了(但是废弃了因为更赞成使用Object.getPrototypeOf()
并且至今我被prototype这个属性所迷惑,他并没有一个prototype的值(比如,fred.prototypeundefined因为fred并不是function)。就个人而言,我认为这是对开发者们误解js的prototypes的最大理由。


这是一个很长的文章?不,现在已经80%,坚持下去。
我们清楚当我们说obj.foo的时候js实际上寻找的是在objobj.__proto__,obj.__proto__.__proto__上面的foo属性

在class上面,你并不会直接接触到这个机制,但是extends依旧是基于原形链去实现的。这就是为什么我们的react class实例可以使用比如setState这样的方法

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Found on c.__proto__ (Greeting.prototype)
c.setState();    // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString();    // Found on c.__proto__.__proto__.__proto__ (Object.prototype)

换而言之,当你使用class的时候,一个实例的__proto__属性已经被挂在在class的原型链上了

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype

2.Chainz

既然__proto__已经被挂在在了class的链上面,我们可以通过检测Greeting是否继承React.Component,通过Greeting.prototype,然后跟踪他的__proto__链。

// `__proto__` chain
new Greeting()
  → Greeting.prototype // ?️ We start here
    → React.Component.prototype // ✅ Found it!
      → Object.prototype

方便的是,X instanceof Y 做的就是这种搜寻。他跟随着x.__proto__的链去寻找Y.prototype;
通常,这被用来检测一个对象是否是class的实例

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (?️‍ We start here)
//   .__proto__ → Greeting.prototype (✅ Found it!)
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype

console.log(greeting instanceof React.Component); // true
// greeting (?️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype

console.log(greeting instanceof Object); // true
// greeting (?️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (✅ Found it!)

console.log(greeting instanceof Banana); // false
// greeting (?️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype (?‍ Did not find it!)

但是这对于检测一个class是否继承另一个class是很有效的

console.log(Greeting.prototype instanceof React.Component);
// greeting
//   .__proto__ → Greeting.prototype (?️‍ We start here)
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype

这个检测就是我们检测一个component是继承自React.Component class还是一个正常function


然而react不完全是这样实现的
这样做的一个隐患是当一个页面有多个React的副本的时候React.Component,用instanceof并无法判断出来。
另一个给人启发的就是去判断原型链上是否拥有render方法。然而对于未来api如何演变是不得而知的。每个检测都有一个成本,所以我们不想额外增加。或者如果render被定义为实例方法,比如类的属性的语法,那么它也不能工作。
取而代之的是,react在base component上面增加了一个特殊的标志。react通过检测这个标志的存在,进而得知他究竟是否是一个React Component的class。
最开始这个标志是在React Component本身上的

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes

然而我们想要实现的class的实现方式并不会去复制静态属性,所以这个标志就会丢失
这就是为什么把这个标志移动到React.Component.prototype上面

// Inside React
class Component {}
Component.prototype.isReactComponent = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes

这就是他的实现方式
你可能想知道他为什么是一个对象而不是布尔值。在实践中这并不重要但是在早期的Jest测试环境中开启自动模拟了。
时至今日,在React仍旧使用isReactComponent 去判断
如果不继承React.Component,React无法在prototype上找到isReactComponent ,所以不会把他当做class来对待。所以你现在知道最注明的问题Cannot call a class as a function的答案是去添加extends React.Component的原因了。最后,当prototype.renderprototype.isReactComponent不存在的时候,会添加一条警告。

你可能说这个故事有点bait-and-switch。实际上的解决方案就是这么简单,但是我花费了巨大的转换思路来解释React是如何得到的解决方案, 以及替代方案是什么。

根据我的经验,库函数通常都是这么实现。为了使api简单好用,你通常要考虑语言的语义(更有可能,好几种语言的情况下,甚至囊括了未来的方向)运行时状况,是否具有编译期的步骤,生态状态,开包即用的解决方案,早起的warnings,以及许多其他的东西。最终的解决方法不一定是最优雅的,但一定是最佳实践的。
如果最后的api设计是成功的,使用者永远不用操心整个过程。因而他们可以更加注重在app的实现上。
但是你是好奇的人的话,了解运行原理当然很棒!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值