本文转载,转载地址
考虑下这个用function定义的Greeting
Component
function Greeting() {
return <p>Hello</p>;
}
React同样支持将它定义为class
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
(至今,这是唯一使用state等特性的方式)
当你想要render Greeting
的时候,你并不关心他是如何被定义的
// 无论是class还是function,都是这么使用
<Greeting />
但是react本身是需要区分这两种方式的
如果Greeting
是function,那么react需要按照如下方式去调用
// 你的代码
function Greeting() {
return <p>Hello</p>;
}
// React内部调用
const result = Greeting(props); // <p>Hello</p>
但是如果Greeting是一个class,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的目标是获取rendered节点(在这个例子中,是 <p>Hello</p>
)但是如何操作是依赖Greeting
是如何定义的
所以react如何知道一个component是通过function还是class来定义的呢?
就像我之前的文章,你不需要在react生产环境下知道这些,我也好多年不知道这个,请不要在面试问题中提这个问题。事实上,这个文章更多讲解的是js层面的内容而不是react。
这个文章是给那些想知道react是如何以正确方式运行的好奇读者们的。你是么?让我们来深究吧~
这是一个漫长的过程。这篇文章并没有多少react本身的信息,但是我们将会浏览new
, this
, class
, 箭头函数
,prototype
,__proto__
, instanceof
这些内容,并且他们在js中是如何协同工作的。幸运的是当你使用react的时候,你并不需要考虑上面那些内容,当然如果你要实现一个react的话…
(如果你真的想知道的话,请滑到最底部)
首先我们要理解为什么区分对待functions和classes是件重要的事情。注意我们当访问一个class的时候是如何使用new
操作符的:
// 如果Greeting是一个function
const result = Greeting(props); // <p>Hello</p>
//如果Greeting是一个class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
让我们大致了解下new
在js中所做的事情
在过去的日子里,js并没有class。然而,你可以通过使用纯函数(plain functions?不知道如何翻译)去实现一个与class相似的模式。具体的,你可以使用任何function表现的类似class constructor函数,在调用之前加一个new操作符。
// 一个function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // ? Won’t work
时至今日你仍然可以这样去写,在开发者工具中试一下吧!
如果你不通过new
调用Person('Fred')
,内部的this
会指向global或者没有用(例如:window
或者是undefined
)。所以我们的代码将会崩掉或者像是给window.name去赋值一样的蠢事。(严格模式下this会指向undefined,非严格模式下会指向window)
通过在调用之前加new,就相当于我们告诉了js编译器:hey,我知道Person是一个function,但是让我们假装他是一个class的constructor函数吧。新建一个对象{}把this
指向Person
函数的内部吧,那样我就能给他们赋值了比如this.name
.然后把这个对象返回给我吧
这就是new操作符做的事情
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();
这就是在js没有添加class之前做的仿class
所以new
操作符出现在js中已经有一段时间了。然而,class是之后出的内容,那么就让我们重写我们上面的代码,以便更好的实现我们的意图
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
获取开发者意图在语言和api开发中是重中之重
如果你实现了一个function,js是不知道他应该是被当做像是alert()
那样的函数调用还是被当做一个class的构造器new Person()
这样去调用。当被作为像是Person
的constructor而忘记使用new
操作符会有意料之外的行为。
class的语法告诉我们:这不只是一个function-这是一个有constructor的class。如果你在调用他的时候忘记用了new
操作符,js直接会报错
let fred = new Person('Fred');
// ✅ 如果Person是个function,正常工作
// ✅ 如果Person是个class,也会正常工作
let george = Person('George'); // We forgot `new`
// ? 如果Person是一个构造函数:意料之外的行为
// ? 如果Person是个class,立马报错
这将帮我们更早的捕获到一些错误,而不是发生在一些模糊的bug比如this.name
会被window.name
而不是george.name
。
然而,这意味着react需要在调用class之前使用new
操作符,而不能像是调用一个正常的js中的function,那将会抛异常!
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// ? React 当然不能这么做
const instance = Counter(props);
这就麻烦了
在我们看react如何解决这个问题之前,一件重要的事情就是大部分人会使用类似babel这样的编译器来编译react,以便能在更古老的浏览器上使用一些现代特性比如classes。所以我们还需要在设计中考虑编译器的问题
在早些的babel版本中,classes可以不通过new
去调用。当然,这个bug已经被fix了,通过一些额外的代码:
function Person(name) {
// 在babel输出中的部分简化
if (!(this instanceof Person)) {
throw new TypeError("Cannot call a class as a function");
}
// 我们的代码
this.name = name;
}
new Person('Fred'); // ✅ Okay
Person('George'); // ? Can’t call class as a function
你可能在bundle中看见过这样的代码,那就是哪些_classCallCheck
函数做的事(你可以通过选择“loose mode”不去做检查,以便减少bundle文件的大小,但这将导致你向真正的原生classes的过度变复杂)
到现在,你大概理解了在调用一些东西之前是否使用new的区别
new Person() | Person() | |
---|---|---|
class | ✅ this 是Person的实例 | ? TypeError |
function | ✅ this 是Person的实例 | this 是 window 或者 undefined |
这就是react需要正确的调用你的component的重要性。如果你的conponent被定义为class,React需要在调用之前使用new
操作符
所以react是否能检测出来它是不是一个class
并不是那么容易!即使我们可以告诉这个在js中是class,但是这并不会在一些比如babel中的工具中生效。对于浏览器来说,他们还是纯函数,react的运气真糟糕!
那么,是否React可以在每次调用前都是用new
去调用?不幸的是,也不行
正常的function来说,通过new调用他们将会给他们一个this对象实例。将fucuntion写作构造函数是可取的行为(像我们上面写的Person),但是对于一些function component依旧会有问题
function Greeting() {
// 我们这里并不希望有任何的this实例!
return <p>Hello</p>;
}
当然,这是在容忍范围之内的。还有其他两个问题将这个想法扼杀了。
第一个问题是总是用new对原声的箭头函数并不生效(没有被babel编译过的),他会抛出异常
const Greeting = () => <p>Hello</p>;
new Greeting(); // ? Greeting 并不是构造函数
这个行为是意料之中的,从箭头函数的定义来源上就可以知道。箭头函数一个重要的特点就是他没有自己的this
,他的this
是最近的有this
的外层函数的this
。
class Friends extends React.Component {
render() {
const friends = this.props.friends;
return friends.map(friend =>
<Friend
// `this` 被解析为render的this
size={this.props.size}
name={friend.name}
key={friend.id}
/>
);
}
}
好的,由此可见箭头函数没有自己的this,这就以为着他们没有constructor的作用!
const Person = (name) => {
// ? 这并不会生效
this.name = name;
}
因此,js不允许通过new来调用箭头函数。如果你这样做了,尽早的告诉你无论如何你都犯了一个错误。这就类似js不允许你不通过new来调用一个class一样的道理。
这很棒但是这却阻碍了我们的计划,react不能什么都用new来调用因为将会对箭头函数报错!我们可以通过箭头函数没有prototype来检测箭头函数,然后不通过new来调用他们
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
这并不会对babel编译过的functions生效。这不是个大问题但是仍然有另一个原因使这个想法挂掉.
另一个不能这么使用的原因是这将会排除react支持的只返回字符串或者原生类型的components
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // ? Greeting {}
又一次的,这与new操作符当初设计的怪癖有关。就像我们之前看到的,new
告诉js 编译器创建了一个对象,然后将对象指向function的this
,然后返回这个对象作为new
的结果
然而,js也允许通过返回一些其他的对象来覆盖new
所创建的对象。当然,这给哪些想复用实例的pooling模式下很有用(单例模式吧?)
var zeroVector = null;
function Vector(x, y) {
if (x === 0 && y === 0) {
if (zeroVector !== null) {
// 复用实例
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
会完全忽略你的return值。如果你反回了比如数字或者字符串,对于new
来说那跟没有返回是一样的。
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // ? Answer {}
没有方法去知道当通过new
操作符去调用的原始返回值(比如number和string)。所以如果react一直通过new
来调用,那么就不能支持哪些返回字符串之类的component!
那是不能接收的,所以我们只能妥协
目前为止我们了解了什么?react需要通过new去调用classes(包括babel输出后的)但是不能用new去调用普通函数和箭头函数(包括babel输出后的)。并没有可靠的方法去区分他们。
如果我们不能解决一个普遍的问题,那么把他具体话之后我们是否可以解决呢?
当你用class定义了一个component,你想继承React.Component
,因为你想使用类似this.setState()
这些方法。相比于检测所有的class,我们是不是只检测React.Component
的子class就可以了呢?
这就是react做的方法
也许,惯用的方式去检测Greeting
是否是React.Component
的子类是通过 Greeting.prototype instanceof React.Component
class A {}
class B extends A {}
console.log(B.prototype instanceof A); // true
我想你在想,发生了什么?理解这个我们需要理解js的prototype
你可能对”原型链“比较熟悉。每一个js中的对象都可能会有"prototype"。当我们写fred.sayHi()
的时候,fred
对象上并没有sayHi
这个属性,我们在fred
’s prototype上寻找sayHi
这个属性。如果没有找到,我们会寻找下一个原型链上的prototype— fred
’s prototype’s prototype. 以此类推
疑惑的是,class或者function的propertype属性并不指向他值的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
属性是什么呢?是通过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__
链才是js如何寻找属性的方式
fred.sayHi();
// 1. fred有sayHi属性么?没有,
// 2. fred.__proto__上有sayHi属性么,是的,调用他!
fred.toString();
// 1.fred有toString属性么,没有
// 2.fred.__proto__有toSting属性么?没有
// 3. fred.__proto__.__proto__有toString属性么?有,调用他!
在实际中,在你的代码里几乎不需要接触__proto__除非你在调试原型链上的东西。如果你想调用fred.__proto__
上面的东西,你应当去调用Person.prototype
。至少这是最原始的设计(__propto__
没有被纳入浏览器的规范,但是几乎所有的浏览器都实现了他)
__proto__
终于开始在浏览器端是不应该被实现的因为原型链才是规范概念。但是部分浏览器添加了__proto__
所以他也算是被标准化了(但是废弃了因为更赞成使用Object.getPrototypeOf()
)
并且至今我被prototype这个属性所迷惑,他并没有一个prototype
的值(比如,fred.prototype
是undefined
因为fred
并不是function)。就个人而言,我认为这是对开发者们误解js的prototypes的最大理由。
这是一个很长的文章?不,现在已经80%,坚持下去。
我们清楚当我们说obj.foo
的时候js实际上寻找的是在obj
,obj.__proto__
,obj.__proto__.__proto__
上面的foo
属性
在class上面,你并不会直接接触到这个机制,但是extends
依旧是基于原形链去实现的。这就是为什么我们的react class实例可以使用比如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
2.Chainz
既然__proto__
已经被挂在在了class的链上面,我们可以通过检测Greeting
是否继承React.Component
,通过Greeting.prototype
,然后跟踪他的__proto__
链。
// `__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
这个检测就是我们检测一个component是继承自React.Component class还是一个正常function
然而react不完全是这样实现的
这样做的一个隐患是当一个页面有多个React的副本的时候React.Component
,用instanceof
并无法判断出来。
另一个给人启发的就是去判断原型链上是否拥有render
方法。然而对于未来api如何演变是不得而知的。每个检测都有一个成本,所以我们不想额外增加。或者如果render被定义为实例方法,比如类的属性的语法,那么它也不能工作。
取而代之的是,react在base component上面增加了一个特殊的标志。react通过检测这个标志的存在,进而得知他究竟是否是一个React Component的class。
最开始这个标志是在React Component本身上的
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
然而我们想要实现的class的实现方式并不会去复制静态属性,所以这个标志就会丢失
这就是为什么把这个标志移动到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测试环境中开启自动模拟了。
时至今日,在React仍旧使用isReactComponent 去判断
如果不继承React.Component
,React无法在prototype上找到isReactComponent
,所以不会把他当做class来对待。所以你现在知道最注明的问题Cannot call a class as a function
的答案是去添加extends React.Component
的原因了。最后,当prototype.render
而prototype.isReactComponent
不存在的时候,会添加一条警告。
你可能说这个故事有点bait-and-switch。实际上的解决方案就是这么简单,但是我花费了巨大的转换思路来解释React是如何得到的解决方案, 以及替代方案是什么。
根据我的经验,库函数通常都是这么实现。为了使api简单好用,你通常要考虑语言的语义(更有可能,好几种语言的情况下,甚至囊括了未来的方向)运行时状况,是否具有编译期的步骤,生态状态,开包即用的解决方案,早起的warnings,以及许多其他的东西。最终的解决方法不一定是最优雅的,但一定是最佳实践的。
如果最后的api设计是成功的,使用者永远不用操心整个过程。因而他们可以更加注重在app的实现上。
但是你是好奇的人的话,了解运行原理当然很棒!