React 如何区别class和function?

React定义组件的方式有两种,class和function。如下:

函数式定义:

function Button() {
  return <div>hello</div>
} 

class方式定义:

class Button extends React.Component {
  render() {
    return <div>>hello</div>
  }
}

当我们需要渲染Button组件的时候,直接使用即可,无需关心它是通过什么方式定义的。

<Button />

但是React内部会关心它是通过什么方式定义的。

如果是函数,React会直接调用。

// React 内部
const result = Button(props); // <div>Hello</div>

如果是class,React 需要先用 new 操作符将其实例化,然后调用刚才生成实例的 render 方法:

// React 内部
const instance = new Button(props) // Button {}
const result = instance.render() // <div>Hello</div>

无论哪种情况,React的目的是获取渲染后的Node(节点),具体获取方式取决于Button是如何定义的。

所以React是怎么区分class和function的?

答案是:在Component的原型上定义属性 isReactComponent = {}

// React 内部
class Component {}
Component.prototype.isReactComponent = {}

简单吧,但React为什么采用如此简单的方案呢?

以下通过JavaScript的诸多特性去逐步解释,这其中涉及 new this class 箭头函数 prototype __propto__ instanceof等方面。

首先理解 new 在JavaScript中是干什么的。

在之前,js是没有类这个概念的,但是可以通过常规函数来模拟。通过new操作符调用函数,意味着任何函数都可以作为构造函数使用。

// 只是一个函数
function Person(name) {
  this.name = name
}

var jack = new Person('Jack') // Person {name: 'Jack'}
var george = Person('George') // 没用的

如果直接调用函数,函数内部的this在非严格模式下指向全局对象window,在严格模式下为undefined。所以直接调用会操作window属性或者报错。

通过new操作符调用函数,函数内部所做的事情就是,首先创建一个 {},赋值给函数内部的this,初始化之后隐式的返回this。同时创建的实例可以共享Person.prototype上的属性和方法。

function Person(name) {
  this.name = name
}
Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name)
}
var jack = new Person('Jack')
jack.sayHi()

ES6 推出类以后,我们就可以通过class的方式实现上述效果:

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

let jack = new Person('Jack')
jack.sayHi()

类是不允许直接调用的,必须通过 new 操作符去调用,否则报错。

let jack = new Person('Jack')
// 如果 Person 是个函数:有效
// 如果 Person 是个类:依然有效

let george = Person('George') // 忘记使用 `new`
// 如果 Person 是个函数: 只是简单调用,可能会出错
// 如果 Person 是个类:立即失败

这样可以帮助我们在早期就捕获到错误,而不会出现类似上面this.name时this是window还是undefined这种潜在的错误。

所以React在调用类组件时,需要通过new的方式,而不是当作常规函数调用。

以下为调用时加不加 new 的差别:

new Person() Person()
class this 是一个 Person 实例 TypeError
function this 是一个 Person 实例 this 是 window 或 undefined

这就是 React 正确调用你的组件很重要的原因。如果组件被定义为一个类,React 需要使用 new 来调用它。

单纯的判断是函数还是类,还是比较容易的。事实上,在开发中,React都会经过babel将类等新语法编译成在可在低版本浏览器上运行的代码。所以class会被编译成经过特殊处理的函数。又该如何判断?

以下为class编译后的伪代码:

function Person(name) {
  if (!(this instanceof Person)) {
    throw new TypeError('Cannot call a class as a function')
  }
  this.name = name
}

new Person('Jack') // OK
Person('George') // 无法把类当做函数来调用

为什么不干脆都都通过new的方式调用呢?并不可以。

对于常规函数来说,勉强可取,但是函数内部生成的实例对我们并没有什么用。我们只是关心其返回的节点。

此外还有两个致命的问题:

第一:箭头函数。

箭头函数是不可以被当作构造函数通过new的方式调用的。因为箭头函数没有自己的this,其内部的this指向离它最近的常规函数所处的上下文。

而且箭头函数没有 prototype 属性。那我们是不是可以通过函数是否有prototype属性来判断直接调用还是通过new方式调用?

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

不可以!

万一箭头函数被babel编译过呢。

那干脆把class和箭头函数都通过babel编译成常规函数,然后都通过new的方式不行吗? 是的,不行!看第二个原因。

第二:不能都使用new的原因是,React支持组件返回字符串、数字等基本类型的值。

function Title() {
  return 'hello title'
}

Title() // 'hello title'
new Title() // {}

返回另一个对象的函数,在使用new调用时会覆盖其创建的实例。但是,如果一个函数的返回值不是一个对象,它会被 new 完全忽略。如果你返回了一个字符串或数字,就好像完全没有 return 一样。

function Answer() {
  return 42
}

Answer() //  42
new Answer() // Answer {}

当使用 new 调用函数时,是没办法读取基本类型的返回值的。因此如果 React 总是使用 new,就没办法增加对返回字符串的组件的支持!

至此清晰的一点是,React在使用类(包括Babel编译成函数)组件时使用new调用,使用常规函数或箭头函数(包括Babel编译)时直接调用。但还是没有完美的方式区分。

不要忘了,我们定义class组件时,需要继承自React.Component,以便可以共享其原型上的方法,比如 setState()、forceUpdate()、render()等。与其检查所有自定义的类,为什么不借助React.Component检查呢?

我们知道,x instanceof Y 所做的就是判断 Y.prototype 是否在x的原型链上。

比如:

class Person extends React.Component {
  render() {
    return <div>hello</div>
  }
}

const p = new Person()

p instanceof Person // true
  // p.__proto__ ---> Person.prototype

p instanceof React.Component // true
  // p.__proto__.__proto__ ---> React.Component.prototype

p instanceof Object // true
  // p.__proto__.__proto__.__proto__ ---> Object.prototype

p instanceof Array // false
  // p.__proto__.__proto__.__proto__.__proto__ ---> null (原型链顶端)

所以基类和父类之间存在如下关系:

class A {}
class B extends A {}

// 因为 
B.prototype.__proto__ === A.prototype // true
// 所以 
B.prototype instanceof A // true

所以通过这种方式可以判断是class还是function。但React还不是这么判断的。

因为 instanceof 解决方案还存在不足之处,当页面上有多个 React 副本,并且我们要检查的组件继承自 另一个 React 副本的 React.Component 时,这种方法是无效的。尽管在一个项目里混合多个 React 副本是不好的,但我们不能排除这种问题的存在性。

另一点启发是去检查原型链上的 render 方法。然而,当时还不确定组件的 API 会如何演化。每一次检查都有成本,所以我们不想再多加了。如果 render 被定义为一个实例方法,例如使用类属性语法,这个方法也会失效。

因此, React 通过为基类增加一个特别的标记。React 检查是否有这个标记,以此知道某样东西是否是一个 React 组件类。

最初这个标记是在 React.Component 这个基类自己身上:

// React 内部
class Component {}
Component.isReactClass = {}

// 可以这样检查
class Greeting extends Component {}
Greeting.isReactClass // ok

然而,某些 compile-to-js 的语言,在类的实现上有所不同,子类在继承父类的时候,会丢失静态属性,导致在子类上访问不到父类的 isReactComponent,所以保险起见,React 把标记移到了 React.Component.prototype上,选择把它作为实例属性,以确保子类能够正确继承。

// React 内部
class Component {}
Component.prototype.isReactComponent = {}

class Greeting extends Component {}
Greeting.prototype.isReactComponent // ok

到这里可能还会有疑惑,isReactComponent 为什么是一个对象,而不是boolean值。这是早期版本的Jest(facebook推出的一款测试框架)是默认开始自动模拟功能的,生成的模拟数据省略掉了原始类型属性,破坏了检查。

所以我们编写的class组件一定要继承自React.Component,否则 React 不会在原型上找到 isReactComponent,因此就不会把组件当做类处理。以至于抛出这种错误 Cannot call a class as a function

最终方案很简单,就是 Component.prototype.isReactComponent = {},但是用了大量的篇幅来解释为什么 React 最终选择了这套方案。

以下是原文作者的感想,我觉得说的特别好。

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

如果最终的 API 成功的话,它的用户 永远不必思考这一过程。他们只需要专心创建应用就好了。

本文基本是翻译原文,一些基础有自己的理解亦有删节。

原文地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355