最近看了Dan Abramov的一些部落格,學到了一些React的一些有趣的知識。決定結合自己的理解總結下。這些內容可能對你實際開發並沒有什麼幫助,不過這可以讓你瞭解到更多React底層實現的內容以及為什麼要怎樣實現。可以讓你跟別人有更多的談資,當然,也可以在某些場合裝一下逼。那麼接下來直接進入正文。
React如何區分類元件和函式元件
我們可以考慮從幾種方式:
統一使用new方法來生成例項
問題:
-
對於函式元件而言,這樣會讓它們生成一個多餘的
this
作為物件例項。 -
對於箭頭函式而言,會報錯。因為箭頭函式並沒有
this
,它的this
是取自於定義這個箭頭函式所在環境的this
const fun = () => console.log(2); new fun(); // Uncaught TypeError: fun is not a constructor 複製程式碼
-
使用
new
會妨礙函式元件返回原始型別(string、number等)。我們都知道,使用new操作符後,只有當函式返回非
null
和非undefined
的物件的時候,返回值才會生效。否則new操作符的返回值都會是物件。關於new
操作符詳細的內容可以點選這裡function Greeting() { return 'Hello'; } // 並不會返回字串 new Gretting(); // Gretting {} 複製程式碼
綜上所述,這個方法不可行。
通過instanceof來判斷
不知道你有沒有察覺,我們寫React
的類元件的時候,我們都需要通過extends React.Component
的方式來寫。那麼,我們是否可以通過以下方式來判斷呢?
class A extends React.Component {
}
A.prototype instanceOf React.Component; // true
複製程式碼
通過這種方式,我們確實可以區分類元件和函式元件,可是也存在一些問題:
-
箭頭函式沒有
prototyoe
這個問題其實好解決,如下
function getType(Component) { if (Component.prototyoe && Component.prototype instance React.Component) { return 'class'; } return 'function'; } 複製程式碼
-
對於一些專案(雖然很少)可能存在著多個React副本,並且我們目前要檢查的元件它繼承的React.Component是來自於另一個React副本的,這就會出現問題。這個問題的話就沒辦法解決了。因此這種方式也存在問題。
通過為React.Component增加一個特別的標記
寫過React
的類元件的人都知道,我們每一個類元件都是要繼承於React.Component
的。因此,如果我們在React.Component
增加一個標記isReactComponent
,這樣通過繼承的方式,我們就可以根據這個標記來判斷是不是類元件了。
// React 內部
class Component {}
Component.prototype.isReactComponent = {};
// 檢查
class Greeting extends Component {};
console.log(Greeting.prototype.isReactComponent);
複製程式碼
事實上,React目前就是通過這種方式來進行檢查的。如果你沒有extends React.Component
,React不會在原型上找到isReactComponent
,因此不會把元件當做類元件來處理。
React Elements為什麼要有一個$typeof屬性
假如我們的jsx長這個樣子:
<Button type="primary">點選</Button>
複製程式碼
實際上,在經過babel後,它會變成下面這段程式碼:
React.createElement(
/* type */ 'Button',
/* props */ { type: 'primary' },
/* children */ '點選'
)
複製程式碼
之後,這個函式執行結果會返回一個物件,這個物件我們稱為React Element
。它是一個用來描述我們將要渲染的頁面結構的一個不可變物件。想了解更多與React Component
,Elements
和Inastances
的可以點選這裡。
// React Element
{
type: 'Button',
props: {
type: 'primary',
children: '點選',
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'), // 為什麼有這個東西
}
複製程式碼
對於React開發者來說,上面這些屬性大部分都是比較常見的。可是為什麼混進了一個奇怪的$$typeof
??它是幹嘛的呢?它的值為什麼是一個Symbol
呢?
這個屬性的引入,其實要從一個安全漏洞說起。
假如我們要顯示一個變數,如果你使用純js來寫的話,可能是這樣:
const messageEl = document.getElementById('message');
messageEl.innerHTML = `<div>${message}</div>`;
複製程式碼
這一段程式碼,對於熟悉或者瞭解過XSS攻擊的人來說,一看就知道會有問題,存在著XSS攻擊。如果message
是使用者可以控制的變數(比如說是使用者輸入的評論)的話,那麼使用者就可以進行攻擊了。比如使用者可以構造下面的程式碼來進行攻擊:
message = '<img onerror="alert(2)" src="" />';
複製程式碼
如果我們明確知道,我們只想單純的渲染文字,不想把它當成html來渲染的話,那麼我們可以通過textContent來避免這個問題。
const messageEl = document.getElementById('message');
messageEl.textContent = `<div>${message}</div>`;
複製程式碼
而對於React而言的話,想要實現相同的效果,只需要:
<div>{message}</div>
複製程式碼
即使message裡面含有img
、script
類似的標籤,它們最終也不會以實際上的標籤顯示。React會對渲染的內容進行轉譯,比如說上面的攻擊程式碼會被轉譯為:
message = '<img onerror="alert(2)" src=""/>';
// 轉譯為
message = '<img onerror="alert(2)" src=""/>'
複製程式碼
因此,這樣就可以避免大部分場景下的XSS攻擊了。
當然,React也提供了另一種方式來將使用者輸入的內容當成html來渲染:
<div dangerouslySetInnerHTML={{ __html: message }}></div>
複製程式碼
前面說了這麼多,那麼跟$$typeof
又有什麼關係呢?別急,重點來了。
對於下面這種寫法,我們一般都知道,message可以傳基本型別、自定義元件和jsx片段。
<div>{message}</div>
複製程式碼
可是,其實我們還可以直接傳React Element。比如,我們可以直接這樣寫
class App extends React.Component {
render() {
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
$$typeof: Symbol.for("react.element")
};
return <>{message}</>;
}
}
複製程式碼
這樣在執行的時候,就會彈出一個alert框了。檢視demo。那麼,這樣會有什麼風險呢?
考慮一個場景,比如一個部落格網站的評論資訊message
是由使用者提供的,並且支援傳入JSON。那麼如果使用者直接將上文的message傳送給後臺儲存。之後,通過下面這種方式展示的話,使用者就可以進行XSS攻擊了。
<div>{message}</div>
複製程式碼
假設如果沒有$$typeof屬性的話,這種攻擊確實可行。因為其他的屬性都是可序列化的。
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
};
JSON.stringify(message);
複製程式碼
事實上,React 0.13當時就存在著這個漏洞。之後,React 0.14就修復了這個問題,修復方式就是通過引入$$typeof屬性,並且用Symbol來作為它的值。
// 引入 $$typeof
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
$$typeof: Symbol.for("react.element")
};
JSON.stringify(message); // Symbol無法被序列化
複製程式碼
這是一個有效的方法,因為JSON是不支援Symbol
型別的。所以,即使使用者提交了如上的message
資訊,到最後服務端也不會儲存$$typeof屬性。而在渲染的時候,React 會檢測是否有$$typeof
屬性。如果沒有這個屬性,則拒絕處理該元素。
那麼如果瀏覽器不支援Symbol
怎麼辦?
是的,那這種保護方案就沒用了。React 依然會加上$$typeof欄位,並且將其值設定為0xeac7
。(為什麼是這個數字呢,因為這個數字看起來有點像React
)。
想檢視具體的攻擊流程,可以檢視這篇部落格。
總結
React
會給React.Component.prototype
增加一個isReactElement
標誌。這樣,React
就可以在渲染的時候判斷當前渲染的元件是類元件還是函式元件。React Element
是一個用於描述要渲染的頁面結構的一個不可變物件。React
函式元件和類元件執行到最後,其實都是生成一個React Elements樹。之後再由實際的渲染層(react-dom、react-native)根據這個React Elements
樹渲染為實際的頁面。<div>{message}</div>
這種方式不僅可以傳原型型別、jsx和元件,還可以直接傳React Element物件。$$typeof
的出現就是為了防止服務端允許儲存JSON而引起的XSS攻擊。可是對於不支援Symbol
的瀏覽器,這個問題依然存在。
本文地址在->本人部落格地址, 歡迎給個 start 或 follow。
參考資料
Why Do React Elements Have a $$typeof Property?