React 是如何分辨函数式组件和类组件的?

前言

原文连接:How Does React Tell a Class from a Function?html

本文中经过探讨这个问题,涉及到了JavaScript中大量的重要概念像原型、原型链、this、类、继承等,经过思考这个问题对这些知识进行一个回顾,不失为一个好的学习方法,但若是你只是想知道这个问题的答案,就像做者说的那样,直接滚动到底部吧。前端

限于本人水平有限,翻译不到位的地方,敬请谅解。react

正文

在React中咱们能够用函数定义一个组件:git

function Greeting() {
  return <p>Hello</p>;
}
复制代码

一样可使用Class定义一个组件:es6

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

在React推出Hooks以前,Class定义的组件是使用像state这样的功能的惟一方式。github

当你想渲染的时候,你不须要关心它是怎样定义的:数组

// Class or function — whatever.
<Greeting />
复制代码

可是React会关心这些不一样。浏览器

若是Greeting是一个函数,React须要像下面这样调用:bash

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

// Inside React
const result = Greeting(props); // <p>Hello</p>
复制代码

可是若是Greeting是一个类,React须要用new命令建立一个实例,而后调用建立的实例的render方法:ide

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

// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
复制代码

那么React是怎么分辨class或者function的呢?


这会是一个比较长的探索之旅,这篇文章不会过多的讨论React,咱们将探索new,this,class,箭头函数,prototype,__proto__,instanceof的某些方面以及它们是怎么在JavaScript中一块儿工做的。

首先,咱们须要理解为何区分functions和class之间不一样是如此重要,注意怎样使用new命令去调用一个class:

// If Greeting is a function
const result = Greeting(props); // <p>Hello</p>

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
复制代码

下面让咱们一块儿来了解下new命令在JavaScript中到底是干什么的。

以前JavaScript并无Class,可是你能用一个正常的函数去模拟Class。具体地说,你可使用任何经过new调用的函数去模拟class的构造函数

// Just a 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会指向window或者undefined,这样咱们的代码将会炸掉或者出现怪异的行为像设置了window.name

经过使用new命令调用函数,至关于咱们说:“JavaScript,你好,我知道Person仅仅只是一个普通函数可是让咱们假设它就是类的一个构造函数。建立一个{}对象而后传入Person函数的内部做为它的this这样我就能进行一些设置像this.name,接着请把那个对象返回给我。”

这就是使用new命令调用函数后发生的事

var fred = new Person('Fred'); // Same object as `this` inside `Person`
复制代码

new命令也作了一些事情让咱们给Person.prototype添加的属性在fred上一样能访问:

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

var fred = new Person('Fred');
fred.sayHi();
复制代码

上面就是你们在JavaScript添加Class(类)以前是怎样模拟Class(类)的。

若是你定义了一个函数,JavaScript是不能肯定你会像alert()同样直接调用或者做为一个构造函数像new Person()。忘了使用new命令去调用像Person这样的函数将会致使一些使人困惑的行为。

Class(类)的语法至关于告诉咱们:“这不只仅是一个函数,它是一个有构造函数的类”。若是你在调用Class(类)的时候,忘了加new命令,JavaScript将会抛出一个错误:

et fred = new Person('Fred');
// ✅  If Person is a function: works fine
// ✅  If Person is a class: works fine too

let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately
复制代码

这将帮助咱们及早的发现错误,而不是等到出现明显的bug的时候才知道,像this.name变成了window.name而不是george.name

无论怎样,这意味着React须要使用new命令去调用全部的类,它不能像调用正常函数同样去调用类,若是这样作了,JavaScript会报错的!

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

// 🔴 React can't just do this: const instance = Counter(props); 复制代码

上面是错误的写法。


在咱们讲React是怎么解决的以前,咱们要知道大多数人会使用Babel去编译React项目,目的是为了让项目中使用的最新特性像class(类)可以兼容低端的浏览器,这样咱们就须要了解的Babel的编译机制。

在Babel早期的版本中,class(类)可使用new命令调用。可是它经过一些额外的代码去修复这个问题:

function Person(name) {
  // A bit simplified from Babel output:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Our code:
  this.name = name;
}

new Person('Fred'); // ✅ Okay
Person('George');   // 🔴 Can’t call class as a function
复制代码

你可能有在打包出来的文件中看到过上面的代码,这就是_classCallCheck所作的事情。

到目前为止,你应该已经大概掌握了使用new命令和不使用new命令之间的差异:

这就是为何React须要正确调用组件是如此重要的缘由。若是你使用class(类)定义一个组件,React须要使用new命令去调用。

那么React能判断出一个组件是不是由class(类)定义的呢?

没那么容易,即便咱们能分辨出函数和class(类):

function isClass(func) {
  return typeof func === 'function' 
    && /^class\s/.test(Function.prototype.toString.call(func));
}
复制代码

但若是咱们使用了像Babel这样的编译工具,上面的方法是不会起做用的,Babel会将class(类)编译为:

// 类
class Person {
}
// Babel编译后
"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function Person() {
  _classCallCheck(this, Person);
};
复制代码

对于浏览器来讲,它们都是普通的函数。


ok,React里面的函数能不能都使用new命令调用呢?答案是不能。

用new命令调用普通函数的时候,会传入一个对象实例做为this,像上面的Person那样将函数做为构造函数来使用是能够的,可是对于函数式的组件却会让人懵逼的:

function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return <p>Hello</p>;
}
复制代码

即便你能这样写,下面的两个缘由会杜绝你的这种想法。

第一个缘由:使用new命令调用箭头函数(未经Babel编译过)会报错

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
复制代码

这样的报错是故意的而且听从箭头函数的设计。箭头函数的一大特色是它没有本身的thisthis绑定的是定义的时候绑定的,指向父执行上下文:

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` is resolved from the `render` method
        size={this.props.size}
        name={friend.name}
        key={friend.id}
      />
    );
  }
}

复制代码
Tips:

若是不太理解的童鞋,能够参考下面的文章

阮一峰ES6教程--箭头函数

全方位解读this-这波能反杀

ok,箭头函数没有本身的this,这就意味着它不能做为构造函数:

const Person = (name) => {
  // 🔴 This wouldn’t make sense!
  this.name = name;
}
复制代码

所以,JavaScript不能使用new命令调用箭头函数,若是你这样作了,程序就会报错,和你不用new命令去调用class(类)同样。

这是很是好的,可是不利于咱们的计划,由于箭头函数的存在,React不能只用new命令去调用,固然咱们也能试着去经过箭头函数没有prototype去区分它们,而后不用new命令调用:

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
复制代码

可是若是你的项目中使用了Babel,这也不是个好主意,还有另外一个缘由使这条路完全走不通。

这个缘由是使用new命令调用React中的函数式组件,会获取不到这些函数式组件返回的字符串或者其余基本数据类型。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
复制代码

关于这点,咱们须要知道new命令到底干了什么?

经过new操做符调用构造函数,会经历如下4个阶段

  • 建立一个新的对象;
  • 将构造函数的this指向这个新对象;
  • 指向构造函数的代码,为这个对象添加属性,方法等;
  • 返回新对象。

关于这些内容在全方位解读this-这波能反杀有更为详细的解释。

若是React只使用new命令调用函数或者类,那么就没法支持返回字符串或者其余原始数据类型的组件,这确定是不能接受的。


到目前为止,咱们知道了,React须要去使用new命令调用class(包括通过Babel编译的),不使用new命令调用正常函数和箭头函数,这仍没有一个可行的方法去区分它们。

当你使用class(类)声明一个组件,你确定想继承React.Component中像this.setState()同样的内部方法。与其去费力去分辨一个函数是否是一个类,还不如咱们去验证这个类是否是React.Component的实例。

剧透:React就是这么作的。

可能咱们经常使用的检测Greeting是React组件示例的方法是Greeting.prototype instanceof React.Component:

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
复制代码

我猜你估计在想,这中间发生了什么?为了回答这个问题,咱们须要理解JavaScript的原型。

你可能已经很是熟悉原型链了,JavaScript中每个对象都有一个“prototype(原型)”。

下面的示例和图来源于前端基础进阶(九):详解面向对象、构造函数、原型与原型链,我的以为比原文示例更能说明问题

// 声明构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 经过prototye属性,将方法挂载到原型对象上
Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

复制代码

image

当咱们想要调用p1上的getName方法时,可是p1自身并无这个方法,它会在p1的原型上寻找,若是没有找到咱们会沿着原型链在上一层的原型上继续找,也就是在p1的原型的原型...,一直找下去,直到原型链的终极null

原型链更像__proto__.__proto__.__proto__而不是prototype.prototype.prototype

那么函数或者类的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__链展现了在JavaScript中是怎样向上寻找属性的:

fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!
复制代码

在实际开发编码中,除非你要调试和原型链相关的东西,不然你根本不须要接触到__proto__。若是你想往原型上添加一些东西,你应该添加到Person.prototype上,那添加到__proto___能够吗?固然能够,能生效,可是这样不符合规范的,有性能问题和兼容性问题,详情点击这里

早期的浏览器是没有暴露__proto属性的,由于原型类是一个内部的概念,后来一些浏览器逐渐支持,在ECMAScript2015规范中被标准化了,想要获取某个对象的原型,建议老老实实的使用Object.getPrototypeOf()


咱们如今已经知道了,当访问obj.foo的时候,JavaScript一般在obj中这样寻找fooobj.__proto__,obj.__proto__.__proto__...

定义一个类组件,你可能看不到原型链这套机制,可是extends(继承)只是原型链的语法糖,React的类组件就是这样访问到React.Component中像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)
复制代码

换句话说,当你使用类的时候,一个实例的原型链映射这个类的层级

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

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype
复制代码

由于原型链映射类的层级,那咱们就能从一个继承自React.Component的组件GreetingGreeting.prototype开始,顺着原型链往下找:

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ We start here
    → React.Component.prototype // ✅ Found it!
      → Object.prototype
复制代码

实际上,x instanceof y就是作的这种查找,它沿着x的原型链查找y的原型。

一般这用来肯定某个实例是不是一个类的实例:

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!)
复制代码

而且它也能用来检测一个类是否继承自另外一个类:

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

咱们就能经过这种方式检测出一个组件是函数组件仍是类组件。


然而React并无这样作。

做者此处还探讨了两种方案,在此略去,有兴趣看原文哟。

实际上React对基础的组件也就是React.Component添加了一个标记,并经过这个标记来区分一个组件是不是一个类组件。

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

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

像上面这样把标记直接添加到基础组件自身,有时候会出现静态属性丢失的状况,因此咱们应该把标记添加到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
复制代码

React就是这样解决的。

后面还有几段,参考文末另外一位大兄弟的译文吧。

后续

这文章有点长,涉及的知识点也比较多,最后的解决方案,看似挺简单的,实际上走到这一步并不简单,但愿你们都有所收获。 翻译到一半的时候,在React的一个Issues中发现另外一我的这篇文章的译文,有兴趣的童鞋,能够点击阅读