React是如何區分class和function的?

看看這個由function定義的Greeting組件:html

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

React也支持由class來定義:react

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

(一直到 最近Hooks出現以前,這是惟一可使用有(如state)功能的方法。)git

當你打算渲染一個<Greeting />時,你不會在乎它是如何定義的:github

// 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>)。但確切的步驟取決於如何定義Greetingecmascript

因此React是如何識別組件是class仍是function的呢?ide

就像上一篇文章你不須要知道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'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 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');
// ✅ 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要爲george.name的卻變成了window.name

可是,這也意味着React須要在調用任何class時前面加上new,它沒法像通常function同樣去調用,由於JavaScript會將其視爲一個錯誤。

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

// 🔴 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'); // ✅ Okay
Person('George');   // 🔴 Cannot call a class as a function
複製代碼

你可能會在bundle中看到這些代碼,這就是全部_classCallCheck函數的功能。(你能夠經過選擇"loose mode"而不進行檢查來減少bundle大小,但你最終轉換成的原生class在實際開發中會帶來麻煩。)


如今,你應該大體明白了調用時有new和沒有new的區別了:

new Person() Person()
class this is a Person instance 🔴 TypeError
function this is a Person instance 😳 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(); // 🔴 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) => {
  // 🔴 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(); // ✅ 'Hello'
new Greeting(); // 😳 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); // 😲 b === c
複製代碼

可是,若是function的返回值不是一個對象,new又會徹底無視此返回值。若是你返回的是一個string或者number,那徹底和不返回值同樣。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 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__); // 😳 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 // ✅ 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 (✅ 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!)
複製代碼

而它也能夠用來判斷一個class是否擴展了另外一個class:

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

若是某個東西是一個class或者普通function的React組件,就能夠用這個來判斷咱們的想法了。


然而這並非React的做法。😳

其中有個問題,在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); // ✅ 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); // ✅ 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?

相關文章
相關標籤/搜索