React是如何区分Class和Function的?

本文总结自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 能够简单易用,你经常需要考虑语义化(可能的话,为多种语言考虑,包括未来的发展方向)、运行时性能、有或没有编译时步骤的工程效能、生态的状态以及打包方案、早期的警告,以及很多其它问题。最终的结果未必总是最优雅的,但必须要是可用的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值