React 是如何分辨函數式組件和類組件的?

前言

原文連接:How Does React Tell a Class from a Function?html

本文中經過探討這個問題,涉及到了JavaScript中大量的重要概念像原型、原型鏈、this、類、繼承等,經過思考這個問題對這些知識進行一個回顧,不失爲一個好的學習方法,但若是你只是想知道這個問題的答案,就像做者說的那樣,直接滾動到底部吧。前端

限於本人水平有限,翻譯不到位的地方,敬請諒解。react

正文

在React中咱們能夠用函數定義一個組件:git

function Greeting() {
  return <p>Hello</p>;
}
複製代碼

一樣可使用Class定義一個組件:es6

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}
複製代碼

在React推出Hooks以前,Class定義的組件是使用像state這樣的功能的惟一方式。github

當你想渲染的時候,你不須要關心它是怎樣定義的:數組

// Class or function — whatever.
<Greeting />
複製代碼

可是React會關心這些不一樣。瀏覽器

若是Greeting是一個函數,React須要像下面這樣調用:bash

// Your code
function Greeting() {
  return <p>Hello</p>;
}

// Inside React
const result = Greeting(props); // <p>Hello</p>
複製代碼

可是若是Greeting是一個類,React須要用new命令建立一個實例,而後調用建立的實例的render方法:ide

// 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是怎麼分辨class或者function的呢?


這會是一個比較長的探索之旅,這篇文章不會過多的討論React,咱們將探索new,this,class,箭頭函數,prototype,__proto__,instanceof的某些方面以及它們是怎麼在JavaScript中一塊兒工做的。

首先,咱們須要理解爲何區分functions和class之間不一樣是如此重要,注意怎樣使用new命令去調用一個class:

// 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,可是你能用一個正常的函數去模擬Class。具體地說,你可使用任何經過new調用的函數去模擬class的構造函數

// Just a 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會指向window或者undefined,這樣咱們的代碼將會炸掉或者出現怪異的行爲像設置了window.name

經過使用new命令調用函數,至關於咱們說:「JavaScript,你好,我知道Person僅僅只是一個普通函數可是讓咱們假設它就是類的一個構造函數。建立一個{}對象而後傳入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(類)的。

若是你定義了一個函數,JavaScript是不能肯定你會像alert()同樣直接調用或者做爲一個構造函數像new Person()。忘了使用new命令去調用像Person這樣的函數將會致使一些使人困惑的行爲。

Class(類)的語法至關於告訴咱們:「這不只僅是一個函數,它是一個有構造函數的類」。若是你在調用Class(類)的時候,忘了加new命令,JavaScript將會拋出一個錯誤:

et fred = new Person('Fred');
// ✅  If Person is a function: works fine
// ✅  If Person is a class: works fine too

let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately
複製代碼

這將幫助咱們及早的發現錯誤,而不是等到出現明顯的bug的時候才知道,像this.name變成了window.name而不是george.name

無論怎樣,這意味着React須要使用new命令去調用全部的類,它不能像調用正常函數同樣去調用類,若是這樣作了,JavaScript會報錯的!

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

// 🔴 React can't just do this: const instance = Counter(props); 複製代碼

上面是錯誤的寫法。


在咱們講React是怎麼解決的以前,咱們要知道大多數人會使用Babel去編譯React項目,目的是爲了讓項目中使用的最新特性像class(類)可以兼容低端的瀏覽器,這樣咱們就須要瞭解的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'); // ✅ Okay
Person('George');   // 🔴 Can’t call class as a function
複製代碼

你可能有在打包出來的文件中看到過上面的代碼,這就是_classCallCheck所作的事情。

到目前爲止,你應該已經大概掌握了使用new命令和不使用new命令之間的差異:

這就是爲何React須要正確調用組件是如此重要的緣由。若是你使用class(類)定義一個組件,React須要使用new命令去調用。

那麼React能判斷出一個組件是不是由class(類)定義的呢?

沒那麼容易,即便咱們能分辨出函數和class(類):

function isClass(func) {
  return typeof func === 'function' 
    && /^class\s/.test(Function.prototype.toString.call(func));
}
複製代碼

但若是咱們使用了像Babel這樣的編譯工具,上面的方法是不會起做用的,Babel會將class(類)編譯爲:

// 類
class Person {
}
// Babel編譯後
"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function Person() {
  _classCallCheck(this, Person);
};
複製代碼

對於瀏覽器來講,它們都是普通的函數。


ok,React裏面的函數能不能都使用new命令調用呢?答案是不能。

用new命令調用普通函數的時候,會傳入一個對象實例做爲this,像上面的Person那樣將函數做爲構造函數來使用是能夠的,可是對於函數式的組件卻會讓人懵逼的:

function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return <p>Hello</p>;
}
複製代碼

即便你能這樣寫,下面的兩個緣由會杜絕你的這種想法。

第一個緣由:使用new命令調用箭頭函數(未經Babel編譯過)會報錯

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
複製代碼

這樣的報錯是故意的而且聽從箭頭函數的設計。箭頭函數的一大特色是它沒有本身的thisthis綁定的是定義的時候綁定的,指向父執行上下文:

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}
      />
    );
  }
}

複製代碼
Tips:

若是不太理解的童鞋,能夠參考下面的文章

阮一峯ES6教程--箭頭函數

全方位解讀this-這波能反殺

ok,箭頭函數沒有本身的this,這就意味着它不能做爲構造函數:

const Person = (name) => {
  // 🔴 This wouldn’t make sense!
  this.name = name;
}
複製代碼

所以,JavaScript不能使用new命令調用箭頭函數,若是你這樣作了,程序就會報錯,和你不用new命令去調用class(類)同樣。

這是很是好的,可是不利於咱們的計劃,由於箭頭函數的存在,React不能只用new命令去調用,固然咱們也能試着去經過箭頭函數沒有prototype去區分它們,而後不用new命令調用:

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
複製代碼

可是若是你的項目中使用了Babel,這也不是個好主意,還有另外一個緣由使這條路完全走不通。

這個緣由是使用new命令調用React中的函數式組件,會獲取不到這些函數式組件返回的字符串或者其餘基本數據類型。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
複製代碼

關於這點,咱們須要知道new命令到底幹了什麼?

經過new操做符調用構造函數,會經歷如下4個階段

  • 建立一個新的對象;
  • 將構造函數的this指向這個新對象;
  • 指向構造函數的代碼,爲這個對象添加屬性,方法等;
  • 返回新對象。

關於這些內容在全方位解讀this-這波能反殺有更爲詳細的解釋。

若是React只使用new命令調用函數或者類,那麼就沒法支持返回字符串或者其餘原始數據類型的組件,這確定是不能接受的。


到目前爲止,咱們知道了,React須要去使用new命令調用class(包括通過Babel編譯的),不使用new命令調用正常函數和箭頭函數,這仍沒有一個可行的方法去區分它們。

當你使用class(類)聲明一個組件,你確定想繼承React.Component中像this.setState()同樣的內部方法。與其去費力去分辨一個函數是否是一個類,還不如咱們去驗證這個類是否是React.Component的實例。

劇透:React就是這麼作的。

可能咱們經常使用的檢測Greeting是React組件示例的方法是Greeting.prototype instanceof React.Component:

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
複製代碼

我猜你估計在想,這中間發生了什麼?爲了回答這個問題,咱們須要理解JavaScript的原型。

你可能已經很是熟悉原型鏈了,JavaScript中每個對象都有一個「prototype(原型)」。

下面的示例和圖來源於前端基礎進階(九):詳解面向對象、構造函數、原型與原型鏈,我的以爲比原文示例更能說明問題

// 聲明構造函數
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 經過prototye屬性,將方法掛載到原型對象上
Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

複製代碼

image

當咱們想要調用p1上的getName方法時,可是p1自身並無這個方法,它會在p1的原型上尋找,若是沒有找到咱們會沿着原型鏈在上一層的原型上繼續找,也就是在p1的原型的原型...,一直找下去,直到原型鏈的終極null

原型鏈更像__proto__.__proto__.__proto__而不是prototype.prototype.prototype

那麼函數或者類的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__鏈展現了在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__。若是你想往原型上添加一些東西,你應該添加到Person.prototype上,那添加到__proto___能夠嗎?固然能夠,能生效,可是這樣不符合規範的,有性能問題和兼容性問題,詳情點擊這裏

早期的瀏覽器是沒有暴露__proto屬性的,由於原型類是一個內部的概念,後來一些瀏覽器逐漸支持,在ECMAScript2015規範中被標準化了,想要獲取某個對象的原型,建議老老實實的使用Object.getPrototypeOf()


咱們如今已經知道了,當訪問obj.foo的時候,JavaScript一般在obj中這樣尋找fooobj.__proto__,obj.__proto__.__proto__...

定義一個類組件,你可能看不到原型鏈這套機制,可是extends(繼承)只是原型鏈的語法糖,React的類組件就是這樣訪問到React.Component中像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)
複製代碼

換句話說,當你使用類的時候,一個實例的原型鏈映射這個類的層級

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

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype
複製代碼

由於原型鏈映射類的層級,那咱們就能從一個繼承自React.Component的組件GreetingGreeting.prototype開始,順着原型鏈往下找:

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ We start here
    → React.Component.prototype // ✅ Found it!
      → Object.prototype
複製代碼

實際上,x instanceof y就是作的這種查找,它沿着x的原型鏈查找y的原型。

一般這用來肯定某個實例是不是一個類的實例:

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!)
複製代碼

而且它也能用來檢測一個類是否繼承自另外一個類:

console.log(Greeting.prototype instanceof React.Component);
// greeting
//   .__proto__ → Greeting.prototype (🕵️‍ We start here)
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype
複製代碼

咱們就能經過這種方式檢測出一個組件是函數組件仍是類組件。


然而React並無這樣作。

做者此處還探討了兩種方案,在此略去,有興趣看原文喲。

實際上React對基礎的組件也就是React.Component添加了一個標記,並經過這個標記來區分一個組件是不是一個類組件。

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
複製代碼

像上面這樣把標記直接添加到基礎組件自身,有時候會出現靜態屬性丟失的狀況,因此咱們應該把標記添加到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
複製代碼

React就是這樣解決的。

後面還有幾段,參考文末另外一位大兄弟的譯文吧。

後續

這文章有點長,涉及的知識點也比較多,最後的解決方案,看似挺簡單的,實際上走到這一步並不簡單,但願你們都有所收穫。 翻譯到一半的時候,在React的一個Issues中發現另外一我的這篇文章的譯文,有興趣的童鞋,能夠點擊閱讀

相關文章
相關標籤/搜索