React是如何區分class和function的?
看看這個由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本身的內容,但我們會經歷另一些方面的東西: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'); // :white_check_mark: Person {name: 'Fred'} var george = Person('George'); // :red_circle: 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'); // :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 aPerson
instance |
:red_circle:TypeError
|
function
|
:white_check_mark:this
is aPerson
instance |
:flushed:this
iswindow
orundefined
|
這就是為什麼正確呼叫你的元件對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或functionnew
過的物件__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是可行的,它的使用者 就永遠不需要去考慮其中的衍生過程。反而他們能更專注於創造應用程式。
但如果你對此依然充滿好奇。。。 知道它如何工作也是不錯的。