本文总结自Dan Abramov的博客。
React中提供了类组件和函数式组件的两种写法,那么React中是如何去判断某个组件是类组件还是函数式组件的?本文将给出解答。以Greeting组件为例,两种定义方式如下:
类组件:
class Greeting extends React.Component {
render() {
return (
<h1>Hello</h1>
);
}
}
函数式组件:
function Greeting(props) {
return (
<h1>Hello</h1>
);
}
const Greeting = (props) => {
return (
<h1>Hello</h1>
);
}
针对组件不同的定义方式,React内部获取组件渲染模板的方式也有所不同:
类组件:
const instanceOfGreeting = new Greeting(props);
const renderResult = instanceOfGreeting.render();
函数式组件:
const renderResult = Greeting(props);
因此,如何准确的判断React组件的定义方式是十分重要的。在继续探究如何区分Class和Function组件之前,让我们先回顾一下new操作符和Javascript中的“类”以及原型。
熟悉JavaScript的同学应该都知道,js中是不存在真正意义上的类的,在ES6推出class和extends语法糖之前,我们都是通过原型链来模拟类的各种特性,比如继承。例如:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi');
}
const me = new Person('zjh');
me.sayHi();
既然类的概念是通过原型链模拟的,那么你可能会好奇new操作符背后究竟做了些什么:
function initFunction(fn, ...params) {
var obj = Object.create(fn.prototype);
var resultOfFn = fn.apply(obj, params);
return isPermitive(resultOfFn) ? obj : resultOfFn; // isPermitive函数用于判断是否为基本类型
}
const me = initFunction(Person, 'zjh');
上面的函数基本上模拟了new操作符创建对象的过程。具体过程如下:
(1)创建一个新的对象,并指定fn.prototype作为该对象的原型对象(新对象能够 “继承” fn.prototype对象所处原型链上的所有属性和方法);
(2)指定函数fn的执行作用域为新创建的对象,并传入函数对应的初始化参数,然后获取其返回值(这一步实际上包含两层含义:1. 指定函数fn的this指向新对象,执行fn,将初始化属性绑定到this上 2. 获取函数fn执行的返回值)。
(3)若函数fn的返回值是对象,那么使用该对象作为new的结果返回;否则返回(1)中新创建的对象。
这时候,你可能又会产生疑惑,为何步骤(3)中,如果函数fn的返回值是对象,那么将该对象作为结果返回?这样我们新建的obj不是可能没有用到?实际上,new操作符这样定义有助于我们创建可复用的对象实例。例如:
let instance = null; // “记忆已创建过的实例”
function Example(a, b) {
this.a = a;
this.b = b;
if (a === 0, b === 0) {
if (instance !== null) {
return instance;
}
instance = this; // 函数Example的this属性指向的就是新创建的对象
}
}
const x = new Example(1, 1);
const y = new Example(0, 0);
const z = new Example(0, 0);
console.log(y === z); // true
回到我们的Person类,假如我们不用new而是直接调用会产生什么后果?
function Person(name) {
this.name = name;
}
Person('zjh');
Person函数的this指向的是调用Person函数的作用域,而上面的代码中是全局作用域。因此,name属性会被绑定到全局作用域的变量对象上,比如浏览器中的window。这样的后果可能并不是我们所预期的,所以往往会在待实例化的函数中,添加如下判断:
function Person(name) {
if (!(this instanceof Person)) {
throw new TypeError("Cannot call a class as a function");
}
this.name = name;
}
在了解了JavaScript中的类机制后,回到我们的主题。Dan Abramov在他的博客中提到,思考过如下方案去处理Class和Function组件而不做区分:
(1)每次调用组件时,都采用new的方式;
对于类组件而言,这没有问题。但是对于函数式组件就会让人感到迷惑,缺点如下:
1、导致通过函数方式定义的组件内部的this会指向一个实例。这并不是我们所期望的结果。
2、对于通过箭头函数定义的组件,无法实例化。(箭头函数内部的this指向由其外层的函数所决定,而且箭头函数没有原型,所以不支持使用new实例化)
3、使得React组件无法支持返回字符串或其他基本类型值(通过上面的initFunction函数,我们可以知道,返回的基本类型值会在new的过程中被忽略)。
上面的方案显然行不通,因此最终我们还是需要去判断某个组件是Class组件还是Function组件。
回到我们上面定义的Greeting组件,可以看到类组件的定义方式中是需要继承React.Component类的,那么我们是否可以借助原型链来完成判断呢?在此之前,我们先分析一下Greeting类(本质上还是函数)和实例化的greeting实例的原型链:
class Greeting extends React.Component {
render() {
return <h1>Hello</h1>;
}
}
const greeting = new Greeting();
console.log(Greeting.__proto__); // 等价于Object.getPrototypeOf(Greeting)
console.log(Greeting.__proto__ === React.Component); // true
console.log(React.Component.__proto__ === Function.__proto__); // true
console.log(Function.__proto === Object.__proto__); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true
// 原型链1
// Greeting.__proto__ 指向继承的类(函数) React.Component
// React.Component.__proto__ 指向 Function.__proto__
// Function.__proto__ 指向 Object.__proto__
// Object.__proto__.__proto__ 指向 Object.prototype
console.log(greeting.__proto__);
console.log(Greeting.prototype);
console.log(greeting.__proto__ === Greeting.prototype); // true
console.log(Greeting.prototype.__proto__ === React.Component.prototype); // true
console.log(React.Component.prototype.__proto__ === Object.prototype); // true
// 原型链2
// greeting.__proto__ 指向 Greeting.prototype
// Greeting.prototype.__proto__ 指向 React.Component.prototype
// React.Component.prototype.__proto__ 指向 Object.prototype
// 两条原型链的终点都是Object.prototype,而且Object.prototype.__proto__是null
对于Greeting类和greeting对象,实际上有各自的两条原型链。
(1)Greeting类的原型对象保存在__proto__(实际指向的是React.Component)中,所以其对应的原型链应该是Greeting.__proto__.__proto__. ... ;
(2)greeting对象的原型对象同样保存在__proto__中,只不过在new的过程中,greeting对象的__proto__指向的是Greeting.prototype; (上面initFunction函数中的:var obj = Object.create(fn.prototype);)
参考Dan Abramov在博客中的描述,当你使用类的时候,实例的__proto__链“镜像”了类的层级结构:
// `extends` 链
Greeting
→ React.Component
→ Object (间接的)
// `__proto__` 链
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
所以,你在Greeting类中定义的属性和方法,实际上都是绑定到了Greeting.prototype上。
而我们在使用instanceof判断某个对象是否是某个类的实例,也是通过沿着原型链不断查找判断xxxobj.__proto__是否指向原型链上的某个类的XXXObject.prototype来完成的:
let greeting = new Greeting();
console.log(greeting instanceof Greeting); // true
// greeting (🕵️ 我们从这儿开始)
// .__proto__ → Greeting.prototype (✅ 找到了!)
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype
console.log(greeting instanceof React.Component); // true
// greeting (🕵️ 我们从这儿开始)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype (✅ 找到了!)
// .__proto__ → Object.prototype
console.log(greeting instanceof Object); // true
// greeting (🕵️ 我们从这儿开始)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (✅ 找到了!)
console.log(greeting instanceof Banana); // false
// greeting (🕵️ 我们从这儿开始)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (🙅 没找到!)
到这里,答案似乎已经揭晓了,我们可以通过Greeting.prototype instanceof React.Component 来判断Greeting是否是一个继承自React.Component的类组件。但是React并不是这么做的,因为instanceof在项目中存在多个React副本的时候可能会失效(重复的副本,这当然是需要避免的问题)。
不过借助这个思路,可以在React.Component这个基类中定义一个静态(static)标记isReactClass:
// React 内部
class Component {}
Component.isReactClass = {};
// 我们可以像这样检查它
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ 是的
React只需要检查用户定义的组件中是否包含这个标记,就能够判断这个组件是否为类组件还是普通函数组件。
不过有些非标准实现的类语法,在extends时并没有复制父类的静态属性,从而导致标记的丢失。因此,最终是将这个标记定义到了React.Component.prototype中:
// React 内部
class Component {}
Component.prototype.isReactComponent = {};
// 我们可以像这样检查它
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ 是的
这也是React一直沿用至今的方式。
最后建议大家去看一下Dan Abramov博客原文,给我以很大的启发,特别是文末的这一段:
实际的解决方案其实真的很简单,但我花了大量的篇幅在转折上来解释为什么 React 最终选择了这套方案,以及还有哪些候选方案。
以我的经验来看,设计一个库的 API 也经常会遇到这种情况。为了一个 API 能够简单易用,你经常需要考虑语义化(可能的话,为多种语言考虑,包括未来的发展方向)、运行时性能、有或没有编译时步骤的工程效能、生态的状态以及打包方案、早期的警告,以及很多其它问题。最终的结果未必总是最优雅的,但必须要是可用的。