原文: how-does-react-tell-a-class-from-a-functionhtml
譯文原文: react是如何知道組件是否是類組件react
考慮這個定義爲函數的Greeting
組件:git
function Greeting() {
return <p>Hello</p>;
}
複製代碼
react
一樣支持做爲一個類去定義它:github
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
方法:babel
// 你的代碼
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>
)。ecmascript
就像在我以前的帖子中同樣,你不須要知道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成功,則其用戶永遠沒必要考慮此過程。 相反,他們能夠專一於建立應用程序。
但若是你也好奇......很高興知道它是如何運做的。