内容简介:看看这个由function定义的React也支持由class来定义:(一直到最近Hooks出现之前,这是唯一可以使用有(如state)功能的方法。)
看看这个由function定义的 Greeting
组件:
function Greeting() { return <p>Hello</p>; } 复制代码
React也支持由class来定义:
class Greeting extends React.Component { render() { return <p>Hello</p>; } } 复制代码
(一直到最近Hooks出现之前,这是唯一可以使用有(如state)功能的方法。)
当你打算渲染一个 <Greeting />
时,你不会在意它是如何定义的:
// 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
。
所以React是如何识别组件是class还是function的呢?
就像 上一篇文章 , 你不需要知道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'); // :white_check_mark: Person {name: 'Fred'} var george = Person('George'); // :red_circle: 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'); // :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 复制代码
这有助我们尽早发现错误,而不是之后遇到一些难以琢磨的bug,例如 this.name
要为 george.name
的却变成了 window.name
。
但是,这也意味着React需要在调用任何class时前面加上 new
,它无法像一般function一样去调用,因为JavaScript会将其视为一个错误。
class Counter extends React.Component { render() { return <p>Hello</p>; } } // :red_circle: 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'); // :white_check_mark: Okay Person('George'); // :red_circle: Cannot call a class as a function 复制代码
你可能会在bundle中看到这些代码,这就是所有 _classCallCheck
函数的功能。(你可以通过选择"loose mode"而不进行检查来减小bundle大小,但你最终转换成的原生class在实际开发中会带来麻烦。)
现在,你应该大致明白了调用时有 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来说很重要了。
如果你的组件用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(); // :red_circle: 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) => { // :red_circle: 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(); // :white_check_mark: 'Hello' new Greeting(); // :flushed: 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); // :astonished: b === c 复制代码
但是,如果function的返回值 不是
一个对象, new
又会 完全无视
此返回值。如果你返回的是一个string或者number,那完全和不返回值一样。
function Answer() { return 42; } Answer(); // :white_check_mark: 42 new Answer(); // :flushed: 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__); // :flushed: 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 // :white_check_mark: 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 (: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!) 复制代码
而它也可以用来判断一个class是否扩展了另一个class:
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 复制代码
如果某个东西是一个class或者普通function的React组件,就可以用这个来判断我们的想法了。
然而这并不是React的作法。:flushed:
其中有个问题,在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); // :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 复制代码
全部内容就到这里了。
你可能会想,为什么它是一个对象而不是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是可行的, 它的使用者 就永远不需要去考虑其中的衍生过程 。反而他们能更专注于创造应用程序。
但如果你对此依然充满好奇。。。 知道它如何工作也是不错的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- mysql查询条件-不区分大小写
- 重构之路:webpack区分生产环境和开发环境
- 当 AI 医疗成为热点,噱头与实干如何区分?
- (译)React是如何区分Class和Function?
- 面试篇---1 如何区分深拷贝与浅拷贝
- Facebook发布全新LOGO,以区分旗下社交媒体
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。