原文: how-does-react-tell-a-class-from-a-functionhtml
译文原文: react是如何知道组件是否是类组件react
考虑这个定义为函数的Greeting
组件:git
function Greeting() {
return <p>Hello</p>;
}
复制代码
react
一样支持做为一个类去定义它:github
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
复制代码
(直到最近,这是使用状态等功能的惟一方法。)面试
当你想渲染<Greeting />
组件时,你没必要关心它是如何定义的:数组
//类或者函数,均可以
<Greeting />
复制代码
可是,做为react
自己,他是关心这些差别的!浏览器
若是Greeting
是一个函数,react
须要调用他:安全
// 你的代码
function Greeting() {
return <p>Hello</p>;
}
// React内
const result = Greeting(props); // <p>Hello</p>
复制代码
可是若是Greeting
是一个类,那么React
就须要使用new
来实例化它,而后在实例上调用render
方法:babel
// 你的代码
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>
)。ecmascript
就像在我以前的帖子中同样,你不须要知道this
在React中的所做所为。多年来我一直都不知道。请不要把它变成面试问题。事实上,这篇文章更多的是关于JavaScript
而不是关于React
。
这个博客是为了好奇于想知道React
为什么以某种方式运做的读者。你是那我的吗?而后让咱们一块儿挖掘。
这是一段漫长的旅程。系好安全带。这篇文章没有太多关于React
自己的信息,但咱们将讨论new
,this
,class
,arrow function
,prototype
,__ proto__
,instanceof
以及这些东西如何在JavaScript
中协同工做的一些方面。幸运的是,当你使用React时,你不须要考虑那些。若是你正在实现React ......
(若是你真的只想知道答案,请拉动到最后。)
首先,咱们须要理解为何以不一样方式处理函数和类很重要。注意咱们在调用类时如何使用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没有类。可是,可使用普通函数表现出于类类似的模式。 具体来讲,您能够在相似于类构造函数的角色中使用任何函数,方法是在调用以前添加new:
// 只是一个function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 不会如期工做
复制代码
你今天仍然能够写这样的代码!在DevTools
中尝试一下。
若是不携带new
调用Person('Fred')
,this
在里面会指向全局和无用的东西(例如,窗口或未定义)。因此咱们的代码会崩溃或者像设置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直接添加类以前模拟类的方式。
因此new
在JavaScript已经存在了一段时间。可是,class
是新加的的。如今让咱们使用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()
那样充当构造函数。忘记为像Person
这样的函数指定new
会致使混乱的行为。
类语法让至关于说:“这不只仅是一个函数 - 它是一个类,它有一个构造函数”。 若是在调用它时忘记使用new
,JavaScript将引起错误:
let fred = new Person('Fred');
// ✅ If Person is a function: works fine
// ✅ If Person is a class: works fine too
let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately
复制代码
这有助于咱们尽早发现错误,而不是等待像this.name
这样的一些模糊的bug
被视为window.name
而不是george.name
。
可是,这意味着React
须要在调用任何类以前使用new
。它不能只是将其做为常规函数调用,由于JavaScript会将其视为错误!
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// 🔴 React can't just do this:
const instance = Counter(props);
复制代码
这意味着麻烦。
在咱们看到React如何解决这个问题以前,重要的是要记住大多数人在React中使用像Babel这样的编译器来编译现代功能,好比旧浏览器的类。因此咱们须要在设计中考虑编译器。
在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'); // ✅ Okay
Person('George'); // 🔴 Can’t call class as a function
复制代码
你或许在你打包的代码中看到相似上面这的,这就是全部_classCallCheck
函数的功能。 (您能够经过选择进入“松散模式”而不进行检查来减少捆绑包大小,但这可能会使您最终转换为真正的原生类变得复杂。)
到如今为止,你应该大体了解使用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能够检查某个东西是否是一个类?
没有那么容易!即便咱们能够用JavaScript中的函数告诉一个类,这仍然不适用于像Babel这样的工具处理的类。对于浏览器来讲,它们只是简单的功能。
好吧,也许React能够在每次调用时使用new
?不幸的是,这并不老是奏效。
做为常规函数,使用new
调用它们会为它们提供一个对象实例做为this
。对于做为构造函数编写的函数(如上面的Person
),它是理想的,但它会使函数组件混淆:
function Greeting() {
// 咱们不但愿“this”在这里成为任何一种状况下的实例
return <p>Hello</p>;
}
复制代码
但这多是能够容忍的。还有另外两个缘由能够扼杀一直使用new
的想法。
第一个能够扼杀的缘由是由于箭头函数,来试试:
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
复制代码
这种行为是有意的,而且遵循箭头函数的设计。箭头函数的主要优势之一是它们没有本身的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}
/>
);
}
}
复制代码
好的,因此箭头功能没有本身的this
。 但这意味着他们做为构造者将彻底无用!
const Person = (name) => {
// 🔴 This wouldn’t make sense!
this.name = name;
}
复制代码
所以,JavaScript不容许使用new
调用箭头函数。若是你这样作,不管如何你可能犯了一个错误,最好早点告诉你。这相似于JavaScript不容许在没有new
的状况下调用类的方式。
这很不错,但它也影响了咱们的计划。 React不能够在全部内容上调用new,由于它会破坏箭头函数!咱们能够尝试经过缺乏prototype
来检测箭头功能,而不只仅是new
:
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
复制代码
可是这个不适用于使用babel
编译的函数。这可能不是什么大问题,但还有另外一个缘由让这种方法成为死胡同。
咱们不能老是使用new
的另外一个缘由是它会阻止React支持返回字符串或其余原始类型的组件。
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 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); // 😲 b === c
复制代码
可是,若是函数不是对象,new
也会彻底忽略函数的返回值。若是你返回一个字符串或一个数字,就好像没有显示返回。
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
复制代码
使用new
调用函数时,没法从函数中读取原始返回值(如数字或字符串)。所以,若是React老是使用new,它将没法添加返回字符串的支持组件!
这是不可接受的,因此咱们须要妥协。
到目前为止咱们学到了什么? React须要用new
调用类(包括Babel
输出),但它须要调用常规函数或箭头函数(包括Babel输出)而不须要new
。并无可靠的方法来区分它们(类和函数)。
若是咱们没法解决通常问题,咱们能解决一个更具体的问题吗?
将组件定义为类时,您可能但愿为内置方法(如this.setState()
)扩展React.Component
。咱们能够只检测React.Component后代,而不是尝试检测全部类吗?
剧透:这正是React所作的。
也许,检查Greeting
是不是React组件类的惯用方法是测试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__); // 😳 Person's prototype
复制代码
因此“原型链”更像是__proto __.__ proto __.__ proto__
而不是prototype.prototype.prototype
。这花了我多年才获得。
那么函数或类的原型属性是什么呢? 它是__proto__赋予全部使用该类或函数建立的对象!
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__
,最终它被勉强标准化(但同意使用Object.getPrototypeOf())。
然而,我仍然发现一个名为prototype
的属性没有给你一个值的原型(例如,fred.prototype
未定义,由于fred
不是一个函数),这让我感到很是困惑。就我的而言,我认为这是即便是经验丰富的开发人员也会误解JavaScript原型的最大缘由。
这是一个很长的帖子,嗯?我说咱们80%在那里。保持着。
咱们知道,当说obj.foo
时,JavaScript实际上在obj
,obj .__ proto__
,obj .__ proto __.__ proto__
中寻找foo
,依此类推。
对于类,您不会直接暴露于此机制,但扩展也适用于良好的旧原型链。这就是咱们的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)
复制代码
换句话说,当使用类时,实例的__proto__
链“镜像”到类层次结构:
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
复制代码
因为__proto__
链反映了类层次结构,所以咱们能够经过从Greeting.prototype
开始检查Greeting
是否扩展了React.Component
,而后跟随其__proto__
链:
// `__proto__` chain
new Greeting()
→ Greeting.prototype // 🕵️ We start here
→ React.Component.prototype // ✅ 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 (✅ 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 (✅ 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 (✅ Found it!)
console.log(greeting instanceof Banana); // false
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (🙅 Did not find it!)
复制代码
但它肯定一个类是否扩展另外一个类也能够正常工做:
console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
复制代码
那个检查是咱们如何肯定某些东西是React组件类仍是常规函数。
但这并非React所作的。 😳
对于instanceof
解决方案的一个警告是,当页面上有多个React副本时它不起做用,而咱们正在检查的组件继承自另外一个React副本的React.Component。在一个项目中混合使用React的多个副本是很差的,缘由有几个,但从历史上看,咱们尽量避免出现问题。 (使用Hooks,咱们可能须要强制重复数据删除。)
另外一种可能的启发式方法多是检查原型上是否存在渲染方法。可是,当时还不清楚组件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); // ✅ 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); // ✅ Yes
复制代码
这实际上就是它的所有内容。
您可能想知道为何它是一个对象而不只仅是一个布尔值。它在实践中并不重要,但早期版本的Jest(在Jest为Good™️以前)默认启用了自动锁定功能。生成的mocks省略了原始属性,打破了检查。感谢Jest。
isReactComponent
检查在今天的React中使用。
若是不扩展React.Component,React将不会在原型上找到isReactComponent
,也不会将组件视为类。如今你知道为何最受欢迎的回答是: Cannot call a class as a function
错误的答案是添加extends React.Component
。最后,添加了一个警告,当prototype.render
存在时会发出警告,但prototype.isReactComponent
不存在。
实际的解决方案很是简单,但我接着解释了为何React最终获得了这个解决方案,以及替代方案是什么。
根据个人经验,库API一般就是这种状况。 为了使API易于使用,常常须要考虑语言语义(可能,对于多种语言,包括将来的方向),运行时性能,有和没有编译时步骤的人体工程学,生态系统和包装解决方案的状态, 早期预警和许多其余事情。 最终结果可能并不老是最优雅,但它必须是实用的。
若是最终API成功,则其用户永远没必要考虑此过程。 相反,他们能够专一于建立应用程序。
但若是你也好奇......很高兴知道它是如何运做的。