(译)React是如何区分Class和Function?

栏目: JavaScript · 发布时间: 5年前

内容简介:一起来看下这个 function 类型的React 同样支持将它定义为 class 类型:(直到最近 hooks-intro,这是使用state等特性的唯一方法。)

一起来看下这个 function 类型的 Greeting 组件:

function Greeting() {
  return <p>Hello</p>;
}
复制代码

React 同样支持将它定义为 class 类型:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}
复制代码

(直到最近 hooks-intro,这是使用state等特性的唯一方法。)

当你想渲染 <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如何知道某个东西是class类型还是function类型呢?

事实上,这篇文章更多的是关于JavaScript而不是关于React。 如何你好奇React为何以某种方式运作,让我们一起挖掘其中的原理。

这是一段漫长的探求之旅。这篇文章没有太多关于React本身的信息,我们将讨论 newthisclassarrow functionprototype__ proto__instanceof 这些概念,以及这些东西如何在JavaScript中运作的机制。幸运的是,当你仅仅是使用React时,你不需要考虑这么多。但你如果要深究React……

(如果你真的只想知道答案,请拉动到文章最后。)

为什么要用不同的调用方式?

首先,我们需要理解以不同方式处理class和function的重要性。注意我们在调用类时如何使用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中做了什么:

在ES6之前,Javascript没有class这个概念。但是,可以使用纯函数表现出和class相似的模式。 具体来说,你可以使用new来调用类似类构造方法的函数,来表现出和class相似的模式

// 只是一个function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // :white_check_mark: Person {name: 'Fred'}
var george = Person('George'); // :red_circle: 不会如期工作
//你今天仍然可以写这样的代码!在 `DevTools` 中尝试一下。
复制代码

如果不用 new 修饰 Person('Fred') ,Person内部的 this 在里面会指向 window 或者 undefined 。结果就是代码会崩溃或者像给 window.name 赋值一样愚蠢。

在调用之前添加 new ,等于说:“嘿 JavaScript,我知道 Person 只是一个函数,但让我们假装它是个类构造函数。 创建一个{}对象 并在 Person 函数内将 this 指向该对象, 这样我就可以赋值像 this.name 这样的东西。然后把那个对象返回给我。”

上面这些就是 new 操作符做的事情。

var fred = new Person('Fred'); // `Person`内,相同的对象作为`this`
复制代码

同时 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();
复制代码

这是之前人们在JavaScript模拟类的方式。

可以看到的是在JavaScript早已有 new 。但是, class 却是后来加入的特性。为了更明确我们的意图,重写一下代码:

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 会导致令人困惑的执行结果。

class语法让我们明确的告诉Javascript:“这不仅仅是一个函数 - 它是一个类,它有一个构造函数”。如果在调用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
复制代码

这有助于我们尽早发现错误,而不是出现一些不符合预期的结果 比如 this.name 被视为 window.name 而不是 george.name

但是,这意味着 React 需要在调用任何class之前使用 new 。它不能只是将其作为普通函数直接调用,因为JavaScript会将其视为错误!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// :red_circle: React can't just do this:
const instance = Counter(props);
复制代码

这意味着麻烦(麻烦就是在于React需要区分Class和Function……)。

探究React式如何解决的

babel之类编译 工具 给解决问题带来的麻烦

在我们探究React式如何解决这个问题时,需要考虑到大多数人都使用Babel之类的编译器来兼容浏览器(例如转义class等),所以我们需要在设计中考虑编译器这种情况。

在Babel的早期版本中,可以在没有 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: Can’t call class as a function
复制代码

你或许在打包文件中看到类似的代码,这就是 _classCallCheck 函数所做的功能。 (您可以通过设置“loose mode”不进行检查来减小捆绑包大小,但这可能会使代码最终转换为真正的原生类变得复杂。)

到现在为止,你应该大致了解使用 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为什么需要正确调用组件的重要原因。 如果您的组件被定义为类,React在调用它时需要使用 new

那么问题来了 React是否可以判断某个东西是不是一个class?

没有那么容易!即使我们可以 在JavaScript es6 中区别class 和 function ,这仍然不适用于像Babel这样的工具处理之后的class。因为对于浏览器来说,它们只是单纯的function而已(class被babel处理后)。

Okay,也许React可以在每次调用时使用 new ?不幸的是,这也并不总是奏效。

异常情况一:

作为一般function,使用 new 调用它们会为它们提供一个对象实例作为 this 。对于作为构造函数编写的函数(如上面的 Person ),它是理想的,但它会给函数组件带来混乱:

function Greeting() {
  // 我们不希望“this”在这里成为任何一种情况下的实例
  return <p>Hello</p>;
}
复制代码

虽然这种情况也是可以容忍的,但还有另外两个原因可以扼杀一直使用 new 的想法。

异常情况二:

第一个是箭头函数(未被babel编译时)会使 new 调用失效,使用 new 调用箭头函数会抛出一个异常

const Greeting = () => <p>Hello</p>;
new Greeting(); // :red_circle: Greeting is not a constructor
复制代码

这种情况时是有意的,并且遵循箭头函数的设计。箭头函数的主要优点之一是它们没有自己的 this 绑定 - 取而代之的是 this 被绑定到最近的函数体中。

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}
      />
    );
  }
}
复制代码

Okay,所以 箭头功能没有自己的 this 这意味着箭头函数无法成为构造者!

const Person = (name) => {
  // :red_circle: This wouldn’t make sense!
  this.name = name;
}
复制代码

因此,JavaScript不允许使用 new 调用箭头函数。如果你这样做,只会产生错误。这类似于JavaScript不允许在没有 new 的情况下调用类的方式。

这很不错,但它也使我们在全部函数调用前添加new的计划失败。 React不可以在所有情况下调用new,因为它会破坏箭头函数!我们可以尝试通过缺少 prototype 来判断出箭头函数:

(() => {}).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 的返回值。这可能对类似对象池这样的模式很有用:

// 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
复制代码

但是,如果函数的返回值不是对象, new 会完全忽略函数的返回值。如果你返回一个字符串或一个数字,就好像没有返回一样。

function Answer() {
  return 42;
}

Answer(); // :white_check_mark: 42
new Answer(); // :flushed: Answer {}
复制代码

当使用 new 调用函数时,无法从函数中读取原始返回值(如数字或字符串)。因此,如果React总是使用new,它将无法支持返回字符串类型的函数(组件)

这是不可接受的,所以我们得另寻他法。

解决方式

到目前为止我们了解到了什么?React需要用 new 调用类(兼容 Babel 情况),但它需要调用常规函数或箭头函数(兼容Babel)时不能使用 new 。同时并没有可靠的方法来区分它们。 如果我们无法解决一个普遍问题,那么我们能解决一个更具体的问题吗?

将Component定义为class时,你可能希望继承 React.Component 使用其内置方法(比如 this.setState() )。 那么我们可以只检测React.Component子类,而不是尝试检测所有class吗?

剧透:这正是React所做的。

prototype__proto__

也许,判断 Greeting 是否是React component class的一般方法是测试 Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
复制代码

我知道你在想什么。刚刚发生了什么?!要回答这个问题,我们需要了解JavaScript原型。

你可能熟悉原型链。JavaScript中的每个对象都可能有一个“原型”。当我们编写 fred.sayHi()fred 对象没有 sayHi 属性时,我们在 fred 的原型上查找 sayHi 属性。如果我们在那里找不到它,我们会看看链中的下一个原型-- fred 的原型的原型。并以此类推。

令人困惑的是,类或函数的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__ 都指向其构造器的prototype 函数或类的 prototype 属性就是这样一个东西

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__ ,最终它被勉强标准化。

至今我仍然觉得“ prototype 的属性没有给你一个值的原型“非常令人困惑(例如, fred.prototype 未定义,因为 fred 不是一个函数)。就个人而言,我认为这是导致经验丰富的开发人员也会误解JavaScript原型的最大原因。

extends 与 原型链

这帖子有点长 不是吗?别放弃!现在已经讲了80%的内容了,让我们继续吧

我们知道,当说调用 obj.foo 时,JavaScript实际上在 objobj .__ 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)
复制代码

换句话说,类实例的 __protp__ 链会镜像拷贝类的继承关系:

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype
复制代码

如此两个链(继承链 原型链)

instanceof 判断方式

由于 __proto__ 链镜像拷贝类的继承关系,因此我们可以通过 Greeting 的原型链来判断 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 存在。

通常,它用于确定某些东西是否是类的实例:

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 判断方式

但这并不是React所做的。 :flushed:

instanceof 解决方案的一个隐患是:当页面上有多个React副本时,我们正在检查的组件可能继承自另一个React副本的React.Component,这种instanceof方式就会失效。 在一个项目中混合使用React的多个副本是不好的方式,但我们应该尽可能避免出现由于历史遗留所产生的这种问题。 (使用Hooks,我们 可能需要 强制删除重复数据。)

另一种可能的骚操作是检查原型上是否存在 render 方法。但是,当时还 不清楚 组件API将如何变换。每个判断操作都有成本我们不想添加多于一次的操作。如果将render定义为实例方法(例如使用类属性语法),这也不起作用。

因此,React为基本组件 添加 了一个特殊标志。React通过检查是该标志来判断一个东西是否是React组件类。

最初该标志在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
复制代码

这就是React如何判断class的全部内容。

如今在React中使用就是 isReactComponent 标志检查。

如果不扩展React.Component,React将不会在原型上找到 isReactComponent ,也不会将组件视为类。现在你知道为什么 Cannot call a class as a function 问题最受欢迎的回答是添加 extends React.Component 。最后,添加了一个 prototype.render 存在时,但 prototype.isReactComponent 不存在的 警告

实际的解决方案非常简单,但我用大量的时间解释了为什么React最终采取这个解决方案,以及替代方案是什么。你可能觉得博文这个解释过程有点啰嗦,

根据我的经验,开发库API经常会遇到这种情况。为了使API易于使用,开发者需要考虑语言语义(可能,对于多种语言,包括未来的方向)、运行时性能、是否编译情况的兼容、完整体系和打包解决方案的状态、 早期预警和许多其他事情。 最终结果可能并不优雅,但必须实用。

如果最终API是成功的,则用户永远不必考虑此过程。取而代之的是他们只需要专注于创建应用程序。

但如果你也好奇......去探究其中的原因还是十分有趣的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

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

新零售:低价高效的数据赋能之路

新零售:低价高效的数据赋能之路

刘润 / 中信出版集团 / 2018-9 / 65.00元

小米新零售,如何做到20倍坪效? 天猫小店,如何利用大数据助力线下零售? 盒马鲜生,为什么坚持必须用App才能买单? 名创优品,实体小店在电商冲击下,如何拥抱春天? 新零售的未来在何方?什么样的思维模式才可应对? 新零售,不是商界大佬的专用名词,它就在我们生活触手可及的各个角落——小到便利店的酸奶,大到京东商城的冰箱,都蕴含着消费者、货物、经营场所三者共同作用的经济逻......一起来看看 《新零售:低价高效的数据赋能之路》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具