看看這個由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>
)。但確切的步驟取決於如何定義Greeting
。ecmascript
因此React是如何識別組件是class仍是function的呢?ide
就像上一篇文章,你不須要知道React中的具體實現。 多年來我也不知道。請不要把它作爲一個面試問題。事實上,這篇文章相對於React,更多的是關於JavaScript的。函數
這篇文章是給好奇知道爲何React是以某種方式運行的同窗的。你是嗎?讓咱們一塊兒挖掘吧。
這是一段漫長的旅行,繫好安全帶。這篇文章沒有太多關於React自己的內容,但咱們會經歷另外一些方面的東西:new
、this
、class
、arrow function
、prototype
、__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或者空(例如,window
或 undefined
)。因此咱們的代碼會發生錯誤或者在不知情的狀況下給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是可行的,它的使用者 就永遠不須要去考慮其中的衍生過程。反而他們能更專一於創造應用程序。
但若是你對此依然充滿好奇。。。 知道它如何工做也是不錯的。