(譯)React是如何區分Class和Function?
一起來看下這個 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本身的資訊,我們將討論new
,this
,class
,arrow function
,prototype
,__ 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(); 複製程式碼
如果你編寫一個函式,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 aPerson
instance |
:red_circle:TypeError
|
function
|
:white_check_mark:this
is aPerson
instance |
:flushed:this
iswindow
orundefined
|
這之中的區別就是React為什麼需要正確呼叫元件的重要原因。
如果您的元件被定義為類,React在呼叫它時需要使用new
。
那麼問題來了 React是否可以判斷某個東西是不是一個class?
沒有那麼容易!即使我們可以ofollow,noindex">在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 {} 複製程式碼
這再次與Operators%2Fnew" rel="nofollow,noindex">
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實際上在obj
,obj .__ 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是成功的,則使用者永遠不必考慮此過程。取而代之的是他們只需要專注於建立應用程式。
但如果你也好奇......去探究其中的原因還是十分有趣的。