看看这个由function定义的Greeting
组件:html
function Greeting() {
return <p>Hello</p>;
}
复制代码
React也支持由class来定义:react
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
复制代码
(一直到 最近Hooks出现以前,这是惟一可使用有(如state)功能的方法。)git
当你打算渲染一个<Greeting />
时,你不会在乎它是如何定义的:github
// Class or function — whatever.
<Greeting />
复制代码
可是React自己是要考虑二者之间的区别的。面试
若是Greeting
是一个function,React须要这样调用它:浏览器
// Your code
function Greeting() {
return <p>Hello</p>;
}
// Inside React
const result = Greeting(props); // <p>Hello</p>
复制代码
但若是Greeting
是一个class,React须要先用new
操做实例一个对象,而后调用实例对象的render
方法。安全
// Your code
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
复制代码
两种类别React的目的都是得到渲染后的节点(这里为,<p>Hello</p>
)。但确切的步骤取决于如何定义Greeting
。ecmascript
因此React是如何识别组件是class仍是function的呢?ide
就像上一篇文章,你不须要知道React中的具体实现。 多年来我也不知道。请不要把它作为一个面试问题。事实上,这篇文章相对于React,更多的是关于JavaScript的。函数
这篇文章是给好奇知道为何React是以某种方式运行的同窗的。你是吗?让咱们一块儿挖掘吧。
这是一段漫长的旅行,系好安全带。这篇文章没有太多关于React自己的内容,但咱们会经历另外一些方面的东西:new
、this
、class
、arrow function
、prototype
、__proto__
、instanceof
,及在JavaScript中它们的相关性。幸运的是,在使用React的时候你不用考虑太多。
(若是你真的只是想知道答案,滚动到最底部吧。)
首先,咱们须要明白不一样处理functions和classes为何重要。注意当调用class时,咱们是如何使用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中做用吧。
过去,JavaScript没有class。可是,你能够用plain function近似的表示它。具体来讲,你能够像构建class同样在function前面加new
来建立函数:
// Just a function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work
复制代码
现在你仍然能够这么编写!用开发工具试试吧。
若是你调用Person('Fred')
没有 new
,方法里的this
会指向global或者空(例如,window
或 undefined
)。因此咱们的代码会发生错误或者在不知情的状况下给window.name
赋值。
在调用方法前加new
,咱们说:“Hey JavaScript,我知道Person
只是一个function,但让咱们伪装它是一个class构造函数吧。添加一个{}
对象,将Person
里的this
指向这个对象,且我能够像this.name
这样给它赋值,而后将这个对象返回给我。”
这就是new
所作的。
var fred = new Person('Fred'); // Same object as `this` inside `Person`
复制代码
new
操做符也会将咱们存放在Person.prototype
东西做用于fred
对象:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred');
fred.sayHi();
复制代码
这就是JavaScript能够直接添加class以前模拟class的方法。
因此new
已经存在于JavaScript里有些时日了。然而,class是后面出现的。它让咱们能更接近咱们意图地重写上述的代码:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
复制代码
对于一门语言和API设计,抓住开发者的意图是重要的。
若是你写一个function,JavaScript没法猜到它是要像alert()
同样被调用,仍是说像new Person()
同样作为构造函数。忘记在function前面加new
会致使不可预测的事发生。
Class语法使咱们能够说:“这不止是个function - 它仍是个class且有一个构造函数”。若是你忘记在调用时加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
复制代码
这有助咱们尽早发现错误,而不是以后遇到一些难以琢磨的bug,例如this.name
要为george.name
的却变成了window.name
。
可是,这也意味着React须要在调用任何class时前面加上new
,它没法像通常function同样去调用,由于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中,调用class能够没有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'); // 🔴 Cannot call a class as a function
复制代码
你可能会在bundle中看到这些代码,这就是全部_classCallCheck
函数的功能。(你能够经过选择"loose mode"而不进行检查来减少bundle大小,但你最终转换成的原生class在实际开发中会带来麻烦。)
如今,你应该大体明白了调用时有new
和没有new
的区别了:
new Person() |
Person() |
|
---|---|---|
class |
✅ this is a Person instance |
🔴 TypeError |
function |
✅ this is a Person instance |
😳 this is window or undefined |
这就是为何正确调用你的组件对React来讲很重要了。若是你的组件用class声明,React须要用new
来调用它。
因此React能够只判断是不是class吗?
没这么简单!即便咱们能够区分class和function,这仍然不适用像Babel这样的工具处理后的class。对于浏览器,它们只是单纯的函数。对React来讲真是倒霉。
好的,因此也许React能够每一个调用都用上new
?不幸的是,这也不见得老是奏效。
通常的function,带上new
调用它们能够获得等同于this
的实例对象。对于作为构造函数编写的function(像前面的Person
),是可行的。但对于function组件会出现问题:
function Greeting() {
// We wouldn’t expect `this` to be any kind of instance here
return <p>Hello</p>;
}
复制代码
这中状况还算是能够忍受的。但有两个缘由能够扼杀这个想法。
第一个缘由是由于,new
没法适用于原生箭头函数(非Babel编译的),调用时带new
会抛出一个错误:
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
复制代码
这种状况是有意的,听从了箭头函数的设计。箭头函数的主要特色是它们没有本身的this
值,而this
是最临近自身的通常function决定的。
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
时调用class。
这很不错,但它也影响了咱们的计划。因为箭头函数,React不能够用new
来调用全部组件。咱们能够用缺失prototype
来检验箭头函数的可行性,而不仅仅用new
:
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
复制代码
但这不适用于使用Babel编译的function。这可能不是什么大问题,但还有另一个缘由使这种方法走向灭亡。
咱们不能老是使用new
的另外一个缘由是它会阻止React支持返回字符串或其余原始数据类型的组件。
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
复制代码
这再次与new
操做符的怪异设计有关。正如以前咱们看到的,new
告诉JavaScript引擎建立一个对象,将对象等同function内的this
,以后对象作为new
的结果返回。
可是,JavaScript也容许使用new
的function经过返回一些对象来覆盖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
复制代码
可是,若是function的返回值不是一个对象,new
又会彻底无视此返回值。若是你返回的是一个string或者number,那彻底和不返回值同样。
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
复制代码
使用new
调用function时,没法读取到原始数据返回值(像number或者string),它没法支持返回字符串的组件。
这是没法接受的,因此咱们势必要妥协。
到目前为止咱们学到了什么?React必须用new
调用class(包含 Babel 的输出),但必须不用new
调用通常的function(包含 Babel 的输出)或是箭头函数,并且并无可靠的方法区别它们。
若是咱们解决不了通常性问题,那咱们可否解决比较特定的问题呢?
当你用class声明一个组件时,你可能会想扩展React.Component
的内置方法,如this.setState()
。相比于检测全部class,咱们能够只检测React.Component
的后代组件吗?
剧透:这正是React所作的。
也许,若是Greeting
是一个class组件,能够用一个经常使用手段去检测,经过测试Greeting.prototype instanceof React.Component
:
class A {}
class B extends A {}
console.log(B.prototype instanceof A); // true
复制代码
我知道你在想什么,刚刚发生了什么?要回答这个问题,咱们须要了解JavaScript的原型(prototype)。
你可能常听到“原型链”,在JavaScript中,全部对象都应该有一个“prototype”。当咱们写fred.sayHi()
而fred
没有sayHi
属性时,咱们会从fred
的原型中寻找sayHi
。若是咱们找不到它,咱们会看看链中下一个prototype
—— fred
原型的原型,以此类推。
使人费解的是,一个class或者function的prototype
属性 并不会 指向该值的原型。我没有在开玩笑。
function Person() {}
console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype
复制代码
因此__proto__.__proto__.__proto__
比prototype.prototype.prototype
更像"原型链"。这我花了好多年才理解。
那么function或是class的prototype
属性是什么?它是提供给全部被class或function new
过的对象 __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
不是一个function导致fred.prototype
变成undefined)。对我而言,我以为这个是经验丰富的开发者也会误解JavaScript原型最大的缘由。
这是一篇很长的文章,对吧?我想说咱们到了80%了,继续吧。
当咱们编写obj.foo
时,JavaScript实际上会在obj
, obj.__proto__
, obj.__proto__.__proto__
上寻找foo
,以此类推。
在class中,你不会直接看到这种机制,不过extends
也是在这个经典的原型链基础上实现的。这就是咱们class定义的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)
复制代码
换句话说,当你使用class时,一个实例的__proto__
链“复刻”了这个class的结构:
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
复制代码
两个链。
由于__proto__
链反映了class的结构,咱们能够从Greeting.prototype
开始,随着__proto__
链往下检查,是否一个Greeting
扩展了React.Component
:
// `__proto__` chain
new Greeting()
→ Greeting.prototype // 🕵️ We start here
→ React.Component.prototype // ✅ Found it!
→ Object.prototype
复制代码
简单来讲,X instanceof Y
正好作了这种搜索。它随着x.__proto__
链寻找其中的Y.prototype
。
一般,这被拿来判断一个东西是否是一个class的实例:
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!)
复制代码
而它也能够用来判断一个class是否扩展了另外一个class:
console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
复制代码
若是某个东西是一个class或者普通function的React组件,就能够用这个来判断咱们的想法了。
然而这并非React的做法。😳
其中有个问题,在React中,咱们检查的组件多是继承至别的React组件的React.Component
副本,instanceof
解决方案对页面上这种屡次复制的React组件是无效的。从经验上看,有好几个缘由可证明,在一个项目中,屡次重复混合使用React组件是很差的选择,咱们要尽可能避免这种操做。(在Hooks中,咱们可能须要)强制执行删除重复的想法。
还有一种启发方法是检测原型上是否存在render
方法。可是,当时还不清楚组件API将如何发展。每次检测要增长一次检测时间,咱们不想花费两次以上的时间在这。而且当render
是实例上定义的方法时(例如class属性语法糖定义的),这种方法就机关用尽了。
所以,React添加了一个特殊标志到基类组件上。React经过检查是否存在该标志,来知道React组件是不是一个class。
最初此标志位于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
复制代码
所有内容就到这里了。
你可能会想,为何它是一个对象而不是boolean。这问题在实际过程当中没什么好纠结的。在早期的Jest版本中(在Jest还优秀的时候)默认启动了自动锁定功能,Jest生成的mock数据会忽略原始数据类型,导致React检查失效。多谢您勒。。Jest。
isReactComponent
检测在今天的React中还在使用。
若是你没有扩展React.Component
,React不会去原型中寻找isReactComponent
,也就不会把它看成class组件来处理。如今你知道为何Cannot call a class as a function
错误的最佳答案是使用了extends React.Component
了吧。最后,咱们还添加了一个警告,当prototype.render
存在但prototype.isReactComponent
不存在时会发出警告。
你可能会说这个故事有点诱导推销的意思。实际的解决方案其实很是简单,但我却用大量离题的事来解释为何React最后会用到这个解法,以及替代的方案有哪些。
以个人经验来看,类库的API一般就是这样,为了使API易于使用,你经常须要去考虑语言的语义(可能对于许多语言来讲,还须要考虑到将来的走向),运行时的性能、人体工程学和编译时间流程、生态、打包方案、预先的警告、和许多其余的东西。最终结果可能并不老是最优雅,但它必须是实用的。
若是最终API是可行的,它的使用者 就永远不须要去考虑其中的衍生过程。反而他们能更专一于创造应用程序。
但若是你对此依然充满好奇。。。 知道它如何工做也是不错的。