(译)React是如何区分Class和Function?
原文地址: how-does-react-tell-a-class-from-a-function
本文地址: React是如何区分Class和Function?
边看边翻译 花了2h+… 如果你觉得读起来还算通顺不费事 那也算我为大家做了一点小贡献吧
React调用两者的不同之处
一起来看下这个 function 类型的 Greeting组件:
1  | function Greeting() {  | 
React 同样支持将它定义为 class 类型:
1  | class Greeting extends React.Component {  | 
(直到最近 hooks-intro,这是使用state等特性的唯一方法。)
当你想渲染<Greeting />组件时,你不必关心它是如何定义的:
1  | //类或者函数,都可以  | 
但是,作为 React本身 是会认为这两个是有不同之处的。
如果Greeting是一个函数,React 只需要直接调用它:
1  | // 你的代码  | 
但是如果Greeting是一个类,那么 React 就需要使用new来实例化它,然后在实例上调用render方法:
1  | // 你的代码  | 
在这两种情况下,React的目标是获取渲染节点(本例中,是<p> Hello </ p>),但确切的步骤取决于Greeting的类型。
那么React如何知道某个东西是class类型还是function类型呢?
事实上,这篇文章更多的是关于JavaScript而不是关于React。
如何你好奇React为何以某种方式运作,让我们一起挖掘其中的原理。
这是一段漫长的探求之旅。这篇文章没有太多关于React本身的信息,我们将讨论new,this,class,arrow function,prototype,__ proto__,instanceof这些概念,以及这些东西如何在JavaScript中运作的机制。幸运的是,当你仅仅是使用React时,你不需要考虑这么多。但你如果要深究React……
(如果你真的只想知道答案,请拉动到文章最后。)
为什么要用不同的调用方式?
首先,我们需要理解以不同方式处理class和function的重要性。注意我们在调用类时如何使用new运算符:
1  | // If Greeting is a function  | 
让我们大致的了解下new在Javascript中做了什么:
在ES6之前,Javascript没有class这个概念。但是,可以使用纯函数表现出和class相似的模式。 具体来说,你可以使用new来调用类似类构造方法的函数,来表现出和class相似的模式
1  | // 只是一个function  | 
如果不用new修饰Person('Fred') ,Person内部的this在里面会指向 window 或者 undefined 。结果就是代码会崩溃或者像给window.name赋值一样愚蠢。
在调用之前添加new,等于说:“嘿 JavaScript,我知道Person只是一个函数,但让我们假装它是个类构造函数。 创建一个{}对象 并在Person函数内将this指向该对象, 这样我就可以赋值像this.name这样的东西。然后把那个对象返回给我。”
上面这些就是new操作符做的事情。
1  | var fred = new Person('Fred'); // `Person`内,相同的对象作为`this`  | 
同时new操作符使上面的fred对象可以使用Person.prototype上的任何内容。
1  | function Person(name) {  | 
这是之前人们在JavaScript模拟类的方式。
可以看到的是在JavaScript早已有new。但是,class却是后来加入的特性。为了更明确我们的意图,重写一下代码:
1  | class Person {  | 
在语言和API设计中把握开发人员的意图非常重要。
如果你编写一个函数,JavaScript就无法猜测你的意思是像alert()一样被直接调用,还是像new Person()那样充当构造函数。忘记添加new会导致令人困惑的执行结果。
class语法让我们明确的告诉Javascript:“这不仅仅是一个函数 - 它是一个类,它有一个构造函数”。 如果在调用class时忘记使用new,JavaScript抛出异常:
1  | let fred = new Person('Fred');  | 
这有助于我们尽早发现错误,而不是出现一些不符合预期的结果 比如this.name被视为window.name而不是george.name。
但是,这意味着React需要在调用任何class之前使用new。它不能只是将其作为普通函数直接调用,因为JavaScript会将其视为错误!
1  | class Counter extends React.Component {  | 
这意味着麻烦(麻烦就是在于React需要区分Class和Function……)。
探究React式如何解决的
babel之类编译工具给解决问题带来的麻烦
在我们探究React式如何解决这个问题时,需要考虑到大多数人都使用Babel之类的编译器来兼容浏览器(例如转义class等),所以我们需要在设计中考虑编译器这种情况。
在Babel的早期版本中,可以在没有new的情况下调用类。但是通过生成一些额外的代码,这个情况已经被修复了:
1  | function Person(name) {  | 
你或许在打包文件中看到类似的代码,这就是_classCallCheck函数所做的功能。 (您可以通过设置“loose mode”不进行检查来减小捆绑包大小,但这可能会使代码最终转换为真正的原生类变得复杂。)
到现在为止,你应该大致了解使用new与不使用new来调用某些内容之间的区别:
new Person() | 
Person() | 
|
|---|---|---|
class | 
✅ this is a Person instance | 
🔴 TypeError | 
function | 
✅ this is a Person instance | 
😳 this is window or undefined | 
这之中的区别就是React为什么需要正确调用组件的重要原因。 如果您的组件被定义为类,React在调用它时需要使用new。
那么问题来了 React是否可以判断某个东西是不是一个class?
没有那么容易!即使我们可以在JavaScript es6 中区别class 和 function,这仍然不适用于像Babel这样的工具处理之后的class。因为对于浏览器来说,它们只是单纯的function而已(class被babel处理后)。
Okay,也许React可以在每次调用时使用new?不幸的是,这也并不总是奏效。
异常情况一:
作为一般function,使用new调用它们会为它们提供一个对象实例作为this。对于作为构造函数编写的函数(如上面的Person),它是理想的,但它会给函数组件带来混乱:
1  | function Greeting() {  | 
虽然这种情况也是可以容忍的,但还有另外两个原因可以扼杀一直使用new的想法。
异常情况二:
第一个是箭头函数(未被babel编译时)会使new调用失效,使用new调用箭头函数会抛出一个异常
1  | const Greeting = () => <p>Hello</p>;  | 
这种情况时是有意的,并且遵循箭头函数的设计。箭头函数的主要优点之一是它们没有自己的this绑定 - 取而代之的是 this被绑定到最近的函数体中。
1  | class Friends extends React.Component {  | 
Okay,所以箭头功能没有自己的this。 这意味着箭头函数无法成为构造者!
1  | const Person = (name) => {  | 
因此,JavaScript不允许使用new调用箭头函数。如果你这样做,只会产生错误。这类似于JavaScript不允许在没有new的情况下调用类的方式。
这很不错,但它也使我们在全部函数调用前添加new的计划失败。 React不可以在所有情况下调用new,因为它会破坏箭头函数!我们可以尝试通过缺少prototype来判断出箭头函数:
1  | (() => {}).prototype // undefined  | 
但是这个不适用于使用babel编译的函数。这可能不是什么大问题,但还有另一个原因让这种方法失败。
异常情况三:
我们不能总是使用new的另一个原因是,这样做不支持返回字符串或其他原始类型。
1  | function Greeting() {  | 
这再次与new的设计奇怪表现有关。正如我们之前看到的那样,new告诉JavaScript引擎创建一个对象,在函数内部创建该对象,然后将该对象作为new的结果。
但是,JavaScript还允许使用new调用的函数通过返回一些其他对象来覆盖new的返回值。这可能对类似对象池这样的模式很有用:
1  | // Created lazily  | 
但是,如果函数的返回值不是对象,new会完全忽略函数的返回值。如果你返回一个字符串或一个数字,就好像没有返回一样。
1  | function Answer() {  | 
当使用new调用函数时,无法从函数中读取原始返回值(如数字或字符串)。因此,如果React总是使用new,它将无法支持返回字符串类型的函数(组件)
这是不可接受的,所以我们得另寻他法。
解决方式
到目前为止我们了解到了什么?  React需要用new调用类(兼容Babel情况),但它需要调用常规函数或箭头函数(兼容Babel)时不能使用new。同时并没有可靠的方法来区分它们。
如果我们无法解决一个普遍问题,那么我们能解决一个更具体的问题吗?
将Component定义为class时,你可能希望继承React.Component使用其内置方法(比如this.setState())。那么我们可以只检测React.Component子类,而不是尝试检测所有class吗?
剧透:这正是React所做的。
prototype 与 __proto__
也许,判断Greeting是否是React component class的一般方法是测试Greeting.prototype instanceof React.Component:
1  | class A {}  | 
我知道你在想什么。刚刚发生了什么?!要回答这个问题,我们需要了解JavaScript原型。
你可能熟悉原型链。JavaScript中的每个对象都可能有一个“原型”。当我们编写fred.sayHi()但fred对象没有sayHi属性时,我们在fred的原型上查找sayHi属性。如果我们在那里找不到它,我们会看看链中的下一个原型–fred的原型的原型。并以此类推。
令人困惑的是,类或函数的prototype属性并不指向该值的原型。 我不是在开玩笑。
1  | function Person() {}  | 
所以“原型链”更像是__proto __.__ proto __.__ proto__而不是prototype.prototype.prototype。我花了许多年才了解到这一点。
所有对象的 __proto__ 都指向其构造器的prototype 函数或类的prototype属性就是这样一个东西
1  | function Person(name) {  | 
而__proto__链是JavaScript查找属性的方式:
1  | fred.sayHi();  | 
事实上,除非您正在调试与原型链相关的内容,否则您几乎不需要直接在代码中修改__proto__。如果你想在fred .__ proto__上添加东西,你应该把它放在Person.prototype上。它最初就是这么设计的。
甚至浏览器都不应该暴露__proto__属性,因为原型链被设计为一个内部概念。但是有些浏览器添加了__proto__,最终它被勉强标准化。
至今我仍然觉得“prototype的属性没有给你一个值的原型“非常令人困惑(例如,fred.prototype未定义,因为fred不是一个函数)。就个人而言,我认为这是导致经验丰富的开发人员也会误解JavaScript原型的最大原因。
extends 与 原型链
这帖子有点长 不是吗?别放弃!现在已经讲了80%的内容了,让我们继续吧
我们知道,当说调用obj.foo时,JavaScript实际上在obj,obj .__ proto__,obj .__ proto __.__ proto__中寻找foo,依此类推。
对于类来说原型链机制会更加复杂,但extends会使类完美适用原型链机制。这也是React类的实例访问setState之类的方法的原理:
1  | class Greeting extends React.Component {  | 
换句话说,类实例的__protp__链会镜像拷贝类的继承关系:
1  | // `extends` chain  | 
如此两个链(继承链 原型链)
instanceof 判断方式
由于__proto__链镜像拷贝类的继承关系,因此我们可以通过Greeting的原型链来判断Greeting是否继承了React.Component:
1  | // `__proto__` chain  | 
方便的是,x instanceof Y就是相同的搜索原理。它在x .__ proto__链中寻找是否有Y.prototype存在。
通常,它用于确定某些东西是否是类的实例:
1  | let greeting = new Greeting();  | 
但它也可以用于确定一个类是否继承另一个类:
1  | console.log(Greeting.prototype instanceof React.Component);  | 
这种判断方式就是是我们如何确定某些东西是React组件类还是一般函数。
React 判断方式
但这并不是React所做的。 😳
instanceof解决方案的一个隐患是:当页面上有多个React副本时,我们正在检查的组件可能继承自另一个React副本的React.Component,这种instanceof方式就会失效。
在一个项目中混合使用React的多个副本是不好的方式,但我们应该尽可能避免出现由于历史遗留所产生的这种问题。 (使用Hooks,我们可能需要强制删除重复数据。)
另一种可能的骚操作是检查原型上是否存在render方法。但是,当时还不清楚组件API将如何变换。每个判断操作都有成本我们不想添加多于一次的操作。如果将render定义为实例方法(例如使用类属性语法),这也不起作用。
因此,React为基本组件添加了一个特殊标志。React通过检查是该标志来判断一个东西是否是React组件类。
最初该标志在React.Component基础类本身上:
1  | // Inside React  | 
但是,我们想要判断的一些类实现没有复制静态属性(或设置__proto__),因此标志丢失了。
这就是React将此标志移动到React.Component.prototype的原因:
1  | // Inside React  | 
这就是React如何判断class的全部内容。
如今在React中使用就是isReactComponent标志检查。
如果不扩展React.Component,React将不会在原型上找到isReactComponent,也不会将组件视为类。现在你知道为什么Cannot call a class as a function  问题最受欢迎的回答是添加extends React.Component。最后,添加了一个prototype.render存在时,但prototype.isReactComponent不存在的警告。
实际的解决方案非常简单,但我用大量的时间解释了为什么React最终采取这个解决方案,以及替代方案是什么。 你可能觉得博文这个解释过程有点啰嗦,
根据我的经验,开发库API经常会遇到这种情况。为了使API易于使用,开发者需要考虑语言语义(可能,对于多种语言,包括未来的方向)、运行时性能、是否编译情况的兼容、完整体系和打包解决方案的状态、 早期预警和许多其他事情。 最终结果可能并不优雅,但必须实用。
如果最终API是成功的,则用户永远不必考虑此过程。 取而代之的是他们只需要专注于创建应用程序。
但如果你也好奇……去探究其中的原因还是十分有趣的。