(译)React是如何区分Class和Function?
栏目: JavaScript · 发布时间: 6年前
内容简介:一起来看下这个 function 类型的React 同样支持将它定义为 class 类型:(直到最近 hooks-intro,这是使用state等特性的唯一方法。)
一起来看下这个 function 类型的 Greeting
组件:
function Greeting() { return <p>Hello</p>; } 复制代码
React 同样支持将它定义为 class 类型:
class Greeting extends React.Component { render() { return <p>Hello</p>; } } 复制代码
(直到最近 hooks-intro,这是使用state等特性的唯一方法。)
当你想渲染 <Greeting />
组件时,你不必关心它是如何定义的:
//类或者函数,都可以 <Greeting /> 复制代码
但是,作为 React本身 是会认为这两个是有不同之处的。
如果 Greeting
是一个函数,React 只需要直接调用它:
// 你的代码 function Greeting() { return <p>Hello</p>; } // React内 const result = Greeting(props); // <p>Hello</p> 复制代码
但是如果 Greeting
是一个类,那么 React 就需要使用 new
来实例化它,然后在实例上调用 render
方法:
// 你的代码 class Greeting extends React.Component { render() { return <p>Hello</p>; } } // React内 const instance = new Greeting(props); // Greeting {} const result = instance.render(); // <p>Hello</p> 复制代码
在这两种情况下, 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运算符:
// 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中做了什么:
在ES6之前,Javascript没有class这个概念。但是,可以使用纯函数表现出和class相似的模式。 具体来说,你可以使用new来调用类似类构造方法的函数,来表现出和class相似的模式
// 只是一个function function Person(name) { this.name = name; } var fred = new Person('Fred'); // :white_check_mark: Person {name: 'Fred'} var george = Person('George'); // :red_circle: 不会如期工作 //你今天仍然可以写这样的代码!在 `DevTools` 中尝试一下。 复制代码
如果不用 new
修饰 Person('Fred')
,Person内部的 this
在里面会指向 window
或者 undefined
。结果就是代码会崩溃或者像给 window.name
赋值一样愚蠢。
在调用之前添加 new
,等于说:“嘿 JavaScript,我知道 Person
只是一个函数,但让我们假装它是个类构造函数。
创建一个{}对象 并在 Person
函数内将 this
指向该对象, 这样我就可以赋值像 this.name
这样的东西。然后把那个对象返回给我。”
上面这些就是 new
操作符做的事情。
var fred = new Person('Fred'); // `Person`内,相同的对象作为`this` 复制代码
同时 new
操作符使上面的 fred
对象可以使用 Person.prototype
上的任何内容。
function Person(name) { this.name = name; } Person.prototype.sayHi = function() { alert('Hi, I am ' + this.name); } var fred = new Person('Fred'); fred.sayHi(); 复制代码
这是之前人们在JavaScript模拟类的方式。
可以看到的是在JavaScript早已有 new
。但是, class
却是后来加入的特性。为了更明确我们的意图,重写一下代码:
class Person { constructor(name) { this.name = name; } sayHi() { alert('Hi, I am ' + this.name); } } let fred = new Person('Fred'); fred.sayHi(); 复制代码
在语言和API设计中把握开发人员的意图非常重要。
如果你编写一个函数,JavaScript就无法猜测你的意思是像 alert()
一样被直接调用,还是像 new Person()
那样充当构造函数。忘记添加 new
会导致令人困惑的执行结果。
class语法让我们明确的告诉Javascript:“这不仅仅是一个函数 - 它是一个类,它有一个构造函数”。如果在调用class时忘记使用 new
,JavaScript抛出异常:
let fred = new Person('Fred'); // :white_check_mark: If Person is a function: works fine // :white_check_mark: If Person is a class: works fine too let george = Person('George'); // We forgot `new` // :flushed: If Person is a constructor-like function: confusing behavior // :red_circle: If Person is a class: fails immediately 复制代码
这有助于我们尽早发现错误,而不是出现一些不符合预期的结果 比如 this.name
被视为 window.name
而不是 george.name
。
但是,这意味着 React
需要在调用任何class之前使用 new
。它不能只是将其作为普通函数直接调用,因为JavaScript会将其视为错误!
class Counter extends React.Component { render() { return <p>Hello</p>; } } // :red_circle: React can't just do this: const instance = Counter(props); 复制代码
这意味着麻烦(麻烦就是在于React需要区分Class和Function……)。
探究React式如何解决的
babel之类编译 工具 给解决问题带来的麻烦
在我们探究React式如何解决这个问题时,需要考虑到大多数人都使用Babel之类的编译器来兼容浏览器(例如转义class等),所以我们需要在设计中考虑编译器这种情况。
在Babel的早期版本中,可以在没有 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'); // :white_check_mark: Okay Person('George'); // :red_circle: Can’t call class as a function 复制代码
你或许在打包文件中看到类似的代码,这就是 _classCallCheck
函数所做的功能。 (您可以通过设置“loose mode”不进行检查来减小捆绑包大小,但这可能会使代码最终转换为真正的原生类变得复杂。)
到现在为止,你应该大致了解使用 new
与不使用 new
来调用某些内容之间的区别:
new Person()
|
Person()
|
|
---|---|---|
class
|
:white_check_mark: this
is a Person
instance |
:red_circle: TypeError
|
function
|
:white_check_mark: this
is a Person
instance |
:flushed: 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
),它是理想的,但它会给函数组件带来混乱:
function Greeting() { // 我们不希望“this”在这里成为任何一种情况下的实例 return <p>Hello</p>; } 复制代码
虽然这种情况也是可以容忍的,但还有另外两个原因可以扼杀一直使用 new
的想法。
异常情况二:
第一个是箭头函数(未被babel编译时)会使 new
调用失效,使用 new
调用箭头函数会抛出一个异常
const Greeting = () => <p>Hello</p>; new Greeting(); // :red_circle: Greeting is not a constructor 复制代码
这种情况时是有意的,并且遵循箭头函数的设计。箭头函数的主要优点之一是它们没有自己的 this
绑定 - 取而代之的是 this
被绑定到最近的函数体中。
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} /> ); } } 复制代码
Okay,所以
箭头功能没有自己的 this
。
这意味着箭头函数无法成为构造者!
const Person = (name) => { // :red_circle: This wouldn’t make sense! this.name = name; } 复制代码
因此,JavaScript不允许使用 new
调用箭头函数。如果你这样做,只会产生错误。这类似于JavaScript不允许在没有 new
的情况下调用类的方式。
这很不错,但它也使我们在全部函数调用前添加new的计划失败。 React不可以在所有情况下调用new,因为它会破坏箭头函数!我们可以尝试通过缺少 prototype
来判断出箭头函数:
(() => {}).prototype // undefined (function() {}).prototype // {constructor: f} 复制代码
但是这个 不适用
于使用 babel
编译的函数。这可能不是什么大问题,但还有另一个原因让这种方法失败。
异常情况三:
我们不能总是使用 new
的另一个原因是,这样做不支持返回字符串或其他原始类型。
function Greeting() { return 'Hello'; } Greeting(); // :white_check_mark: 'Hello' new Greeting(); // :flushed: Greeting {} 复制代码
这再次与
new
的设计
奇怪表现有关。正如我们之前看到的那样, new
告诉JavaScript引擎创建一个对象,在函数内部创建该对象,然后将该对象作为 new
的结果。
但是,JavaScript还允许使用 new
调用的函数通过返回一些其他对象来覆盖 new
的返回值。这可能对类似对象池这样的模式很有用:
// Created lazily var zeroVector = null; function Vector(x, y) { if (x === 0 && y === 0) { if (zeroVector !== null) { // Reuse the same instance return zeroVector; } zeroVector = this; } this.x = x; this.y = y; } var a = new Vector(1, 1); var b = new Vector(0, 0); var c = new Vector(0, 0); // :astonished: b === c 复制代码
但是,如果函数的返回值不是对象, new
会完全忽略函数的返回值。如果你返回一个字符串或一个数字,就好像没有返回一样。
function Answer() { return 42; } Answer(); // :white_check_mark: 42 new Answer(); // :flushed: 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
:
class A {} class B extends A {} console.log(B.prototype instanceof A); // true 复制代码
我知道你在想什么。刚刚发生了什么?!要回答这个问题,我们需要了解JavaScript原型。
你可能熟悉原型链。JavaScript中的每个对象都可能有一个“原型”。当我们编写 fred.sayHi()
但 fred
对象没有 sayHi
属性时,我们在 fred
的原型上查找 sayHi
属性。如果我们在那里找不到它,我们会看看链中的下一个原型-- fred
的原型的原型。并以此类推。
令人困惑的是,类或函数的prototype属性并不指向该值的原型。我不是在开玩笑。
function Person() {} console.log(Person.prototype); // Not Person's prototype console.log(Person.__proto__); // :flushed: Person's prototype 复制代码
所以“原型链”更像是 __proto __.__ proto __.__ proto__
而不是 prototype.prototype.prototype
。我花了许多年才了解到这一点。
所有对象的 __proto__
都指向其构造器的prototype 函数或类的 prototype
属性就是这样一个东西
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__
。如果你想在 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
之类的方法的原理:
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) 复制代码
换句话说,类实例的 __protp__
链会镜像拷贝类的继承关系:
// `extends` chain Greeting → React.Component → Object (implicitly) // `__proto__` chain new Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype 复制代码
如此两个链(继承链 原型链)
instanceof 判断方式
由于 __proto__
链镜像拷贝类的继承关系,因此我们可以通过 Greeting
的原型链来判断 Greeting
是否继承了 React.Component
:
// `__proto__` chain new Greeting() → Greeting.prototype // ️ We start here → React.Component.prototype // :white_check_mark: Found it! → Object.prototype 复制代码
方便的是, x instanceof Y
就是相同的搜索原理。它在 x .__ proto__
链中寻找是否有 Y.prototype
存在。
通常,它用于确定某些东西是否是类的实例:
let greeting = new Greeting(); console.log(greeting instanceof Greeting); // true // greeting ( ️ We start here) // .__proto__ → Greeting.prototype (:white_check_mark: 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 (:white_check_mark: 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 (:white_check_mark: Found it!) console.log(greeting instanceof Banana); // false // greeting ( ️ We start here) // .__proto__ → Greeting.prototype // .__proto__ → React.Component.prototype // .__proto__ → Object.prototype (:no_good: Did not find it!) 复制代码
但它也可以用于确定一个类是否继承另一个类:
console.log(Greeting.prototype instanceof React.Component); // greeting // .__proto__ → Greeting.prototype ( ️ We start here) // .__proto__ → React.Component.prototype (:white_check_mark: Found it!) // .__proto__ → Object.prototype 复制代码
这种判断方式就是是我们如何确定某些东西是React组件类还是一般函数。
React 判断方式
但这并不是React所做的。 :flushed:
instanceof
解决方案的一个隐患是:当页面上有多个React副本时,我们正在检查的组件可能继承自另一个React副本的React.Component,这种instanceof方式就会失效。
在一个项目中混合使用React的多个副本是不好的方式,但我们应该尽可能避免出现由于历史遗留所产生的这种问题。 (使用Hooks,我们 可能需要
强制删除重复数据。)
另一种可能的骚操作是检查原型上是否存在 render
方法。但是,当时还 不清楚
组件API将如何变换。每个判断操作都有成本我们不想添加多于一次的操作。如果将render定义为实例方法(例如使用类属性语法),这也不起作用。
因此,React为基本组件 添加 了一个特殊标志。React通过检查是该标志来判断一个东西是否是React组件类。
最初该标志在React.Component基础类本身上:
// Inside React class Component {} Component.isReactClass = {}; // We can check it like this class Greeting extends Component {} console.log(Greeting.isReactClass); // :white_check_mark: Yes 复制代码
但是,我们想要判断的一些类实现没有 复制
静态属性(或设置 __proto__
),因此标志丢失了。
这就是React将此标志移动到 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); // :white_check_mark: Yes 复制代码
这就是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是成功的,则用户永远不必考虑此过程。取而代之的是他们只需要专注于创建应用程序。
但如果你也好奇......去探究其中的原因还是十分有趣的。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- mysql查询条件-不区分大小写
- 重构之路:webpack区分生产环境和开发环境
- 当 AI 医疗成为热点,噱头与实干如何区分?
- 面试篇---1 如何区分深拷贝与浅拷贝
- React是如何区分class和function的?
- Facebook发布全新LOGO,以区分旗下社交媒体
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
500 Lines or Less
Amy Brown、Michael DiBernardo / 2016-6-28 / USD 35.00
This book provides you with the chance to study how 26 experienced programmers think when they are building something new. The programs you will read about in this book were all written from scratch t......一起来看看 《500 Lines or Less》 这本书的介绍吧!