内容简介:考虑用函数定义的组件React也支持使用类定义它:(直到最近,那是唯一的方式使用state特性)
考虑用函数定义的组件 Greeting
:
function Greeting() { return <p>Hello</p> } 复制代码
React也支持使用类定义它:
class Greeting extends React.Component { render() { return <p>Hello</p> } } 复制代码
(直到最近,那是唯一的方式使用state特性)
当你render一个 <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是怎样分辨类和函数的呢?
正如我之前文章所说的,不知道这些你也能使用React生产。多年来我都不知道这件事。请不要把这个问题变成面试问题。事实上,这篇文章更多的关于是JavaScript而不是React。
这篇文章是为那些好奇于React为何会在一种特定方式下工作的读者写的。你是那样的读者吗?让我们一起深入探讨吧。
这是一段较长的旅程。这篇博客不会有很多关于React的信息,但是我们会审查某些方面 new
, this
, class
, arrow functions
, prototype
, __proto__
, instanceof
,和这些东西在 JavaScript
中是如何一起工作的。幸运的是,当你使用React时你不需要考虑太多这方面的问题。如果你正在实现那么...
(如果你只是想要知道结果,请导航到最底部。)
首先,我们需要了解为什么将函数和类视为不同的是重要的。注意我们怎样使用new操作符当调用一个类时:
// 如果Greeting是一个函数 const result = Greeting(props); // <p>Hello</p> // 如果Greeting是一个类 const instance = new Greeting(props); // Greeting {} const result = instance.render(); // <p>Hello</p> 复制代码
让我们粗略了解一下JavaScript中的new操作符是做什么的。
在过去,JavaScript中不存在类。可是,你可以通过简单的函数表示一个相似的类。具体说来,你可以使用任何函数类似于类的角色通过添加 new
在它被调用时:
// 仅仅是个函数 function Person(name) { this.name = name; } var fred = new Person('Fred'); // Person {name: 'Fred'} var george = Person('George'); // Won't work 复制代码
你现在依然能够这样写!尝试写在DevTools上。
如果你调用 Person('Fred')
没有 new
,它内部的 this
将指向全局或者不可用(例如, window
或者 undefined
).因此我们的代码可能崩溃或者做一些愚蠢的事情例如设置 window.name
。
调用之前通过添加 new
,我们说:“嗨 JavaScript
,我知道 Person
是一个函数但是假装它是一个类构造器。
创建一个 {}
对象并将 Person
函数内部的 this
指向该对象,因此我可以定义一些东西比如 this.name
。然后它会返回一个对象给我。
这就是 new
操作符所做的事情。
var fred = new Person('Fred'); // 相同的对象this在Person内部 复制代码
new
操作符也能使我们定义在 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
已经存在一段时间了。然后, classe
没出现多久。这让我们重写上面的代码,以便更加地匹配我们的意图:
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
特殊的处理像 Person
这样的函数会导致混乱的行为。
Class
语法让我们说:“这不只是函数——它是一个类并且拥有构造器” 。如果你忘记使用 new
当调用它的时候,JavaScript会抛出一个错误:
let fred = new Person('Fred'); // 如果Person是一个函数:正常工作 // 如果Person是一个类:也能正常工作 let george = Person('George'); // 没有加new // 如果Person是一个构造函数的样子,迷惑的行为 // 如果Person是一个类:将立即失败 复制代码
这会帮助我们很早地捕获错误而不是等到一些迷惑的 bug
出现就像 this.name
被认为是 window.name
而不是 george.name
。
然而,这就意味着React需要将 new
加上在调用任何类之前。不能仅仅将它作为普通的函数调用,否则JavaScript会将它看做是一个错误!
class Counter extends React.Component { render() { return <p>Hello</p> } } // React 不能这样做 const instance = Counter(props); 复制代码
这样会导致问题。
在我们看React是如何解决这个问题时,记住大多数人在使用React时,为了兼容老的浏览器,都会使用编译器比如 Babel
将现有的一些特性例如类进行编译是重要的。因此我们需要考虑编译因素在我们的设计中。
在最近的 Babel
版本中,类可以不使用 new
而被调用。但是,这很快被修复了——通过生成一些额外的代码:
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'); // 不能调用类像函数一样 复制代码
你可能有一些代码像这样在你的包中。那些都是 _classCallCheck
函数所做的事情。(你可以通过选择“松散模式”来减少包的大小,而无需进行检查,但是这可能会使你最终转换到真正的本地类的过程变得复杂。)
到目前为止,你应该大致了解了使用new或不使用new调用某些东西的区别:
New Person() Person()
class this是一个Person实例 TypeError
function this是一个Person实例 this是window或者undefined
这就是为什么正确调用组件的React非常重要。
如果你的组件被定义为一个类,则React在调用它时需要使用 new
。
那么React仅仅检查某个东西是否是类吗?
不会这么简单的!尽管我们 在JavaScript中能够从函数中分辨出类 ,这仍然不适用于Babel等 工具 处理的类。对于浏览器来说,它们只是普通函数。对React是不好的。
好吧,也许React在每次调用时都要使用 new
?不幸的是,这也不总是有效的。
对于常规函数,用 new
调用它们会给它们一个 this
的对象实例。对于作为构造函数编写的函数(如我们上面提到的Person)来说,它是可取的,但是对于函数组件来说,它可能会令人混淆:
function Greeting() { // 我们不期望this是任何类型的实例 return <p>Hello</p> } 复制代码
不过,这是可以容忍的。有两个其他的原因扼杀了这个想法。
总是使用`new`不起作用的第一个原因是本地箭头函数(不是`Babel`编译的那些函数)),调用使用`new`会抛出一个错误:
const Greeting = () => <p>Hello</p>; new Greeting(); // Greeting不是一个构造器 复制代码
这种行为是有意的,并遵循箭头函数的设计。箭头函数的一个主要好处是它们没有自己的 this
值——相反, this
的值指向最近的常规函数:
class Friends extends React.Component { render() { const friends = this.props.friends; return friends.map(friend => <Friend // `this`是render中的值 size={this.props.size} name={friend.name} key={friend.id} /> ); } } 复制代码
箭头函数没有自己的 this
。这意味着它们作为构造函数将完全无用!
const Person = (name) => { // :red_circle: 这样是没有意义的 this.name = name; } 复制代码
因此,
JavaScript不允许使用 new
调用箭头函数
。如果你这样做了,无论如何你都可能犯了一个错误,最好早点告诉你。这类似于JavaScript在没有 new
的情况下不允许调用类。
这很好,但也破坏了我们的计划。React不能对所有东西都调用 new
,因为它会破坏箭头函数!我们可以通过箭头函数缺少原型来检测它们,而不仅仅 new
一个:
(() => {}).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
的返回值。据推测,这对于我们希望重用实例的池等模式是有用的:
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); // :astonished: b === c 复制代码
然而, new
也会完全忽视函数返回非对象的值。如果你返回一个字符串或者数字,就像没有返回一样。
function Answer() { return 42; } Answer(); // :white_check_mark: 42 new Answer(); // :flushed: Answer {} 复制代码
在使用 new
调用函数时,无法从函数读取原始返回值(如数字或字符串)。因此,如果React总是使用 new
,将不能支持返回字符串的组件!
这是不能接受的,所以我们需要妥协。
到目前为止,我们都学到了些什么?React在调用 classe
时(包括使用Babel编译后的结果)需要使用 new
,而一般的函数或者箭头函数(包括使用 Babel
编译后的结果)被调用时不需要使用 new
。并且没有一种可靠的方式能分辨出它们的区别。
如果我们不能解决一般的问题,还能解决特殊的问题吗?
当你使用类定义一个组件时,你可能希望通过继承 React.Component
来扩展一些方法,如 this.setState()
。与其检测所有类,不如只检测 React.Component
的后代。
剧透:这就是React所做的事情。
也许,检测 Greeting
是 React Component
类型惯用的方式是通过检测 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
属性,我们将在它的原型上查找 sayHi
。如果我们没有在那里找到,我们将继续沿着原型链查找—— fred
的 prototype
的 prototype
。等等。
疑惑的是,类或函数的 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__
,用于类或函数的所有新对象!
function Person(name) { this.name = name; } Person.prototype.sayHi = function() { alert('Hi, I am ' + this.name); } var fred = new Person('Fred'); // 设置‘fred.__proto__’为‘Person.prototype’ 复制代码
__proto__
链就是JavaScript如何查找属性的方法。
fred.sayHi(); // 1. fred有sayHi属性吗?没有 // 2. fred.__proto__有一个sayHi属性吗?是的,Call it! fred.toString(); // 1. fred有toString属性吗?没有 // 2. fred.__proto__有一个toString属性吗?没有! // 3. fred.__proto__.__proto__有一个toString属性吗?是的,Call it! 复制代码
在实践中,除非调试与原型链相关的内容,否则几乎不需要直接从代码中接触 __proto__
。如果你想使一些东西在 fred.__proto__
起作用,你应该将它写在 Person.prototype
上。至少它最初是这样设计的。
__proto__
属性一开始甚至不应该由浏览器公开,因为原型链被认为是一个内部概念。但是一些浏览器添加了 __proto__
,最终勉强实现了标准化(但是反对使用 Object.getPrototypeOf()
)。
但是我仍然觉得很困惑,一个叫做 prototype
的属性并没有给你一个值的原型 (例如, fred.prototype
是 undefined
,因为 fred
不是一个函数)。个人看来,我认为这是即使有经验的开发人员也容易误解JavaScript原型的最大原因。
这是一篇很长的文章,嗯哼?我已经说了 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) 复制代码
换句话说,当你使用类时,实例的 __proto__
链“映射”了类的层次结构:
// `extends` chain Greeting → React.Component → Object (implicitly) // `__proto__` chain new Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype 复制代码
2种链式
因为 __proto__
链反映了类的层次结构,我们可以根据 Greeting.prototype
检查 Greeting
是否继承自 React.Component
,然后跟着 __proto__
链找下去:
// `__proto__` chain new Greeting() → Greeting.prototype // 从这里开始 → React.Component.prototype // :white_check_mark: 找到了 → 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的功能。 :flushed:
需要注意的是当页面中存在多个React的副本时, instanceof
方法是没有用的,并且我们检查的组件继承自另一个React拷贝的 React.Component
。将多个React副本混合在一个项目中是不好的,原因有几个,但在历史上,我们总是尽可能避免出现问题。(但是,使用钩子,我们 可能需要
强制删除重复数据。)
另一种具有启发性的方法是检查原型上是否存在render方法。但是,那时候还 不清楚
组件API中将包括哪些东西。每一种检查都会增加消耗因此我们不想增加多于一个的检查方式。如果在实例上添加 render
方法,这也不会工作的,例如使用类属性语法。
因此取而代之的是,React在基础组件上 添加 了一个特殊的标志,React检查标志是否存在,这就是它能辨别某些东西是否是React组件类。
原本标志是设于基础 React.Component
类自身上面的:
// React 内部 class Component {} Component.isReactClass = {}; // 我们可以这样检查它 class Greeting extends Component {} console.log(Greeting.isReactClass) ; // yes 复制代码
但是,我们对于一些类的实现目标是 不想
要复制静态属性(或者设置不标准的 __proto__
), 因此标志消失了。
这也是为什么React将标志 移入
到 React.Component.prototype
的原因:
// React 内部 class Component {} Component.prototype.isReactComponent = {}; // 我们能够这样检查它 class Greeting extends Component {} console.log(Greeting.prototype.isReactComponent); // yes 复制代码
这就是它的全部。
你也许会好奇它为何是一个对象而不是一个 boolean
值。这在实践中并没有多大影响但是在 jest
早期版本(在 Jest
是 Good™
️之前)中会默认的自动模拟。生成的mocks省略了基本属性, 破坏了检查
。感谢你,Jest.
近来 isReactComponent
检查被 用在React中
。
如果你没有继承 React.Component
,React不会在原型中寻找 isReactComponent
,也不会将组件视为一个类。现在你知道为什么获得最高票的问题“不能调用类作为一个函数”错误的回答是“ extends React.Component
”.最后,当 prototype.render
存在而 prototype.isReactComponent
不存在时会出现一个 警告
。
你也许会说这边文章有点诱导转向法的感觉。真正的解决方案很简单,但是我偏题的解释了大一堆而以这种方法结束,有什么可供选择呢
在我的经验中,库 api
通常就是这种情况。要使 API
易于使用,通常需要考虑语言语义(可能是几种语言,包括未来的发展方向),运行时性能,有无编译时间步态的人类工效学,生态系统的状态和打包解决方案,早期警告,以及许多其他东西。最终的结果不一定是最优雅的,但必须是可实践的。
**如果最后 API
成功了,用户绝不会考虑它的过程。**相反他们会刚专注于创建 APPs
。
但是如果你也好奇,知道它是如何工作是很美妙的一件事情。
原文链接: overreacted.io/how-does-re… byDan Abreamov
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 试试 kaggle 竞赛:辨别猫狗
- MSP和CMP,辨别一二
- AI神经网络如何辨别事物
- 漫画:如何辨别二逼互联网公司!?
- 压缩算法 bzip2 的官网 bzip.org 域名过期,请注意辨别
- Python 拓展之特殊函数(lambda 函数,map 函数,filter 函数,reduce 函数)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。