原文: how-does-react-tell-a-class-from-a-function
譯文原文: react是如何知道元件是不是類元件
考慮這個定義為函式的Greeting
元件:
function Greeting() {
return <p>Hello</p>;
}
複製程式碼
react
同樣支援作為一個類去定義它:
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
複製程式碼
(直到最近,這是使用狀態等功能的唯一方法。)
當你想渲染<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>
)。
那麼React如何知道某個東西是類還是函式呢?
就像在我之前的帖子中一樣,你不需要知道this
在React中的所作所為。多年來我一直都不知道。請不要把它變成面試問題。事實上,這篇文章更多的是關於JavaScript
而不是關於React
。
這個部落格是為了好奇於想知道React
為何以某種方式運作的讀者。你是那個人嗎?然後讓我們一起挖掘。
這是一段漫長的旅程。繫好安全帶。這篇文章沒有太多關於React
本身的資訊,但我們將討論new
,this
,class
,arrow function
,prototype
,__ proto__
,instanceof
以及這些東西如何在JavaScript
中協同工作的一些方面。幸運的是,當你使用React時,你不需要考慮那些。如果你正在實現React ......
(如果你真的只想知道答案,請拉動到最後。)
首先,我們需要理解為什麼以不同方式處理函式和類很重要。注意我們在呼叫類時如何使用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沒有類。但是,可以使用普通函式表現出於類相似的模式。 具體來說,您可以在類似於類建構函式的角色中使用任何函式,方法是在呼叫之前新增new:
// 只是一個function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // ? 不會如期工作
複製程式碼
你今天仍然可以寫這樣的程式碼!在DevTools
中嘗試一下。
如果不攜帶new
呼叫Person('Fred')
,this
在裡面會指向全域性和無用的東西(例如,視窗或未定義)。所以我們的程式碼會崩潰或者像設定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直接新增類之前模擬類的方式。
所以new
在JavaScript已經存在了一段時間。但是,class
是新加的的。現在讓我們使用class
重寫上面的程式碼以更緊密地匹配我們的意圖:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
複製程式碼
在語言和API設計中捕獲開發人員的意圖非常重要。
如果你編寫一個函式,JavaScript就無法猜測它是否像alert()
一樣被呼叫,或者它是否像new Person()
那樣充當建構函式。忘記為像Person
這樣的函式指定new
會導致混亂的行為。
類語法讓相當於說:“這不僅僅是一個函式 - 它是一個類,它有一個建構函式”。 如果在呼叫它時忘記使用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
複製程式碼
這有助於我們儘早發現錯誤,而不是等待像this.name
這樣的一些模糊的bug
被視為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如何解決這個問題之前,重要的是要記住大多數人在React中使用像Babel這樣的編譯器來編譯現代功能,比如舊瀏覽器的類。所以我們需要在設計中考慮編譯器。
在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'); // ✅ Okay
Person('George'); // ? Can’t call class as a function
複製程式碼
你或許在你打包的程式碼中看到類似上面這的,這就是所有_classCallCheck
函式的功能。 (您可以通過選擇進入“鬆散模式”而不進行檢查來減小捆綁包大小,但這可能會使您最終轉換為真正的原生類變得複雜。)
到現在為止,你應該大致瞭解使用new
或不使用new
來呼叫某些內容之間的區別:
new Person() |
Person() |
|
---|---|---|
class |
✅ this is a Person instance |
? TypeError |
function |
✅ this is a Person instance |
? this is window or undefined |
這就是React正確呼叫元件的重要原因。 如果您的元件被定義為類,React在呼叫它時需要使用new
。
所以React可以檢查某個東西是不是一個類?
沒有那麼容易!即使我們可以用JavaScript中的函式告訴一個類,這仍然不適用於像Babel這樣的工具處理的類。對於瀏覽器來說,它們只是簡單的功能。
好吧,也許React可以在每次呼叫時使用new
?不幸的是,這並不總是奏效。
作為常規函式,使用new
呼叫它們會為它們提供一個物件例項作為this
。對於作為建構函式編寫的函式(如上面的Person
),它是理想的,但它會使函式元件混淆:
function Greeting() {
// 我們不希望“this”在這裡成為任何一種情況下的例項
return <p>Hello</p>;
}
複製程式碼
但這可能是可以容忍的。還有另外兩個原因可以扼殺一直使用new
的想法。
第一個可以扼殺的原因是因為箭頭函式,來試試:
const Greeting = () => <p>Hello</p>;
new Greeting(); // ? Greeting is not a constructor
複製程式碼
這種行為是有意的,並且遵循箭頭函式的設計。箭頭函式的主要優點之一是它們沒有自己的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}
/>
);
}
}
複製程式碼
好的,所以箭頭功能沒有自己的this
。 但這意味著他們作為構造者將完全無用!
const Person = (name) => {
// ? This wouldn’t make sense!
this.name = name;
}
複製程式碼
因此,JavaScript不允許使用new
呼叫箭頭函式。如果你這樣做,無論如何你可能犯了一個錯誤,最好早點告訴你。這類似於JavaScript不允許在沒有new
的情況下呼叫類的方式。
這很不錯,但它也影響了我們的計劃。 React不可以在所有內容上呼叫new,因為它會破壞箭頭函式!我們可以嘗試通過缺少prototype
來檢測箭頭功能,而不僅僅是new
:
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
複製程式碼
但是這個不適用於使用babel
編譯的函式。這可能不是什麼大問題,但還有另一個原因讓這種方法成為死衚衕。
我們不能總是使用new
的另一個原因是它會阻止React支援返回字串或其他原始型別的元件。
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // ? Greeting {}
複製程式碼
這再次與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); // ? b === c
複製程式碼
但是,如果函式不是物件,new
也會完全忽略函式的返回值。如果你返回一個字串或一個數字,就好像沒有顯示返回。
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // ? Answer {}
複製程式碼
使用new
呼叫函式時,無法從函式中讀取原始返回值(如數字或字串)。因此,如果React總是使用new,它將無法新增返回字串的支援元件!
這是不可接受的,所以我們需要妥協。
到目前為止我們學到了什麼? React需要用new
呼叫類(包括Babel
輸出),但它需要呼叫常規函式或箭頭函式(包括Babel輸出)而不需要new
。並沒有可靠的方法來區分它們(類和函式)。
如果我們無法解決一般問題,我們能解決一個更具體的問題嗎?
將元件定義為類時,您可能希望為內建方法(如this.setState()
)擴充套件React.Component
。我們可以只檢測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中的每個物件都可能有一個“原型”。當我們編寫fred.sayHi()
但fred
物件沒有sayHi
屬性時,我們在fred
的原型上查詢sayHi
屬性。如果我們在那裡找不到它,我們會看看鏈中的下一個原型--fred
的原型的原型。等等。
令人困惑的是,類或函式的prototype屬性並不指向該值的原型。 我不是在開玩笑。
function Person() {}
console.log(Person.prototype); // ? Not Person's prototype
console.log(Person.__proto__); // ? Person's prototype
複製程式碼
所以“原型鏈”更像是__proto __.__ proto __.__ proto__
而不是prototype.prototype.prototype
。這花了我多年才得到。
那麼函式或類的原型屬性是什麼呢? 它是__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.prototype
未定義,因為fred
不是一個函式),這讓我感到非常困惑。就個人而言,我認為這是即使是經驗豐富的開發人員也會誤解JavaScript原型的最大原因。
這是一個很長的帖子,嗯?我說我們80%在那裡。保持著。
我們知道,當說obj.foo
時,JavaScript實際上在obj
,obj .__ proto__
,obj .__ proto __.__ proto__
中尋找foo
,依此類推。
對於類,您不會直接暴露於此機制,但擴充套件也適用於良好的舊原型鏈。這就是我們的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)
複製程式碼
換句話說,當使用類時,例項的__proto__
鏈“映象”到類層次結構:
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
複製程式碼
由於__proto__
鏈反映了類層次結構,因此我們可以通過從Greeting.prototype
開始檢查Greeting
是否擴充套件了React.Component
,然後跟隨其__proto__
鏈:
// `__proto__` chain
new Greeting()
→ Greeting.prototype // ?️ We start here
→ React.Component.prototype // ✅ 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 (✅ 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所做的。 ?
對於instanceof
解決方案的一個警告是,當頁面上有多個React副本時它不起作用,而我們正在檢查的元件繼承自另一個React副本的React.Component。在一個專案中混合使用React的多個副本是不好的,原因有幾個,但從歷史上看,我們儘可能避免出現問題。 (使用Hooks,我們可能需要強制重複資料刪除。)
另一種可能的啟發式方法可能是檢查原型上是否存在渲染方法。但是,當時還不清楚元件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); // ✅ 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
複製程式碼
這實際上就是它的全部內容。
您可能想知道為什麼它是一個物件而不僅僅是一個布林值。它在實踐中並不重要,但早期版本的Jest(在Jest為Good™️之前)預設啟用了自動鎖定功能。生成的mocks省略了原始屬性,打破了檢查。感謝Jest。
isReactComponent
檢查在今天的React中使用。
如果不擴充套件React.Component,React將不會在原型上找到isReactComponent
,也不會將元件視為類。現在你知道為什麼最受歡迎的回答是: Cannot call a class as a function
錯誤的答案是新增extends React.Component
。最後,新增了一個警告,當prototype.render
存在時會發出警告,但prototype.isReactComponent
不存在。
實際的解決方案非常簡單,但我接著解釋了為什麼React最終得到了這個解決方案,以及替代方案是什麼。
根據我的經驗,庫API通常就是這種情況。 為了使API易於使用,經常需要考慮語言語義(可能,對於多種語言,包括未來的方向),執行時效能,有和沒有編譯時步驟的人體工程學,生態系統和包裝解決方案的狀態, 早期預警和許多其他事情。 最終結果可能並不總是最優雅,但它必須是實用的。
如果最終API成功,則其使用者永遠不必考慮此過程。 相反,他們可以專注於建立應用程式。
但如果你也好奇......很高興知道它是如何運作的。