React是如何区分class和function的?

栏目: IOS · Android · 发布时间: 6年前

内容简介:看看这个由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本身的内容,但我们会经历另一些方面的东西: newthisclassarrow functionprototype__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或者空(例如, windowundefined )。所以我们的代码会发生错误或者在不知情的情况下给 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是可行的, 它的使用者 就永远不需要去考虑其中的衍生过程 。反而他们能更专注于创造应用程序。

但如果你对此依然充满好奇。。。 知道它如何工作也是不错的。

翻译原文 How Does React Tell a Class from a Function?


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Visual Thinking

Visual Thinking

Colin Ware / Morgan Kaufmann / 2008-4-18 / USD 49.95

Increasingly, designers need to present information in ways that aid their audiences thinking process. Fortunately, results from the relatively new science of human visual perception provide valuable ......一起来看看 《Visual Thinking》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试