連結
blabla
翻譯水平有限,部分內容比較晦澀,因此可能錯誤較多,請理解或指教。
簡介
介紹用於解決現有侷限性的全新Context API。
基本例項
type Theme = 'light' | 'dark';
// Pass a default theme to ensure type correctness
//傳遞預設的主題確保型別的正確
const ThemeContext: Context<Theme> = React.createContext('light');
class ThemeToggler extends React.Component {
state = {theme: 'light'};
render() {
return (
/*傳遞現有的Context的值給Provider的props上的`value`屬性
資料變化使用Object.is來進行嚴格比較*/
<ThemeContext.Provider value={this.state.theme}>
<button
onClick={() =>
this.setState(state => ({
theme: state.theme === 'light' ? 'dark' : 'light',
}))
}>
Toggle theme
</button>
{this.props.children}
</ThemeContext.Provider>
);
}
}
class Title extends React.Component {
render() {
return (
//消費層使用一個渲染的prop API,以避免與props名稱空間衝突
<ThemeContext.Consumer>
{theme => (
<h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
}
複製程式碼
動機
通常情況下,React裡面的資料是按照top-down(parent to child)的順序,通過props來傳遞的。但有些時候,跳過多個抽象層級來傳遞一些值往往是很有用處的。就如例子中所傳遞的UI主題引數一樣。很多元件都依賴於此,可你卻不想在每一層元件上都使用prop來向下傳遞。
React裡面的Context API正是為了解決這一問題所誕生的。在本文中,我們將祖先元件成為Provider(提供者),把孩子元件稱為Consumer(消費者)。
現有版本的Context API的缺點
shouldComponentUpdate會阻止Context的更改
現在的Context API的主要問題就是如何與shouldComponentUpdate
互動。如果一箇中間元件使用了shouldComponentUpdate
來操作,那麼它的下方元件在沒有等待更新的時候,React將認為整個子樹都沒有發生改變。如果子樹包含了Context的消費者,那麼消費者將不會收到任何最新的Context。換句話來說,Context的更改將不會在shouldComponentUpdate
返回為false上的元件上傳播。
在React應用中,shouldComponentUpdate
是經常使用的優化操作方式。在共享元件與開源庫中它的使用往往很頻繁。在實踐中,這意味著Context在廣播變化的時候是不可靠的。
將使用者空間複雜化
現在,開發者們通過使用訂閱來繞過了shouldComponentUpdate
的問題
- 提供者從當事件發生器,它追蹤最新的Context,並且當它發生更改時通知訂閱了的使用者。
- 消費者使用Context API來訪問事件發生器(這種方法很好,因為事件發生器本身並不會發生更改)。
- 消費者向提供者註冊一個事件監聽器。
- 當提供者出發了一個變化事件,消費者會收到通知並呼叫
setState
來更新並觸發重渲染。訂閱被開源軟體廣發使用,例如Redux
和React Broadcast
。它很有用,但它也有一些明顯的缺點: - 不符合人體工程學。由於Context使用的普遍性,正確的實施起來應該不會特別困難。
- 啟動成本。為每個消費者訂閱的花費很高,特別是因為它們在初始掛載的時候並未使用。
- 鼓勵突變(注:這裡原文是Encourages mutation,不知道怎麼翻譯,直接直譯)和一些不常用的但會在非同步模式下造成一定的BUG的模式。
- 相同的程式碼在每個庫中都會被複制一次,這樣增加了包的大小。 最根本的問題在於,核心功能的所有權和責任已經從框架轉移到了使用者身上。
這個提案的主要目的
- 零成本(或接近於零)的初始安裝、提交和解除安裝,根據需要取捨的更新成本。
- 簡單使用的API
- 靜態的型別
- 鼓勵非同步友好的實踐,例如不可變性。
- 組織費非理想的做法,例如事件發生器與變異。
- 消除使用者級程式碼中重複的複雜性。
詳細的設計
介紹全新的元件型別:Provider
和Consumer
:
type Provider<T> = React.Component<{
value: T,
children?: React.Node,
}>;
type Consumer<T> = React.Component<{
children: (value: T) => React.Node,
}>;
複製程式碼
Provider
和Comsumer
是成對出現的,對於每一個Provider
,都會有一個對應的Consumer
。一個Provider
-Consumer
組合是用React.createContext()
來產生的:
type Context<T> = {
Provider: Provider<T>,
Consumer: Consumer<T>,
};
interface React {
createContext<T>(defaultValue: T): Context<T>;
}
複製程式碼
createContext
需要一個預設值來確保型別的正確。
請注意,即使任何提供者與任何消費者的值的型別相同,它們也不能聯合使用。它們必須是同一個createContext
產生的結果。
Provider
在props
上接收一個value
,無論巢狀的深度如何,它都能被提供者的任何匹配的消費者所訪問。
render() {
return (
<Provider value={this.state.contextValue}>
{this.props.children}
</Provider>
);
}
複製程式碼
為了更新Context的值,父級重新渲染並傳遞一個不同的值。Context的變化將被檢測到,檢測所使用的是Object.is
來比較的。這意味著鼓勵使用不可變或持久的資料結構。在典型的場景中,通過呼叫提供者父級的setState
來更新Context。
消費者使用一個render prop API
:
render() {
return (
<Consumer>
{contextValue => <Child arbitraryProp={contextValue} />}
</Consumer>
)
}
複製程式碼
請注意上面這個例子,Context的值可以傳遞給子元件上的任意prop。render prop API
的優點就是避免了破壞prop的名稱空間。
如果一個Consumer
沒有提供一個匹配的Provider
作為它的祖先,它會接收搭配傳遞給createContext
的預設值,確保型別安全。
缺點
依賴於Context的值的嚴格比較
該提案使用嚴格(參考)比較來檢測對Context的更改。這部分鼓勵使用不可變性或持久的資料結構。但許多常見的資料來源都依賴與突變。例如Flux
的某些實現,甚至像Relay Modern
這樣的更新的庫。
但是,與非同步渲染結合時,突變存在固有的問題,主要與撕裂有關。 對於依賴變異的體系結構,開發人員要麼決定某種程度的撕裂是可以接受的,要麼演變為更好地支援非同步。 無論如何,這些問題並不僅限於Context API。(From Google Translation)
一些依賴於突變的庫的一個技巧就是克隆產生一個全新的外部容器(或者甚至只是在他們之間交替)。React將檢測到一個新物件的引用並且觸發一個更改。
每個Consumer只有一個Provider
建議的API只允許消費者從單一提供者型別讀取值,這與當前的API不同,後者允許消費者從任意數量的提供者型別讀取。
解決方法是使用合成消費者(待定):
<FooConsumer>
{foo => (
<BarConsumer>
{bar => (
// Render using both foo and bar
<Child foo={foo} bar={bar} />
)}
</BarConsumer>
)}
</FooConsumer>;
複製程式碼
大多數圍繞上下文的抽象已經使用了類似的模式。
備用方案
setContext
我們可以使用像setState
一樣工作的setContext
API,而不是依賴於引用相等來檢測對上下文的更改。
但是,如果不考慮實現的開銷,這個API只有在與突變結合使用時才有價值,我們專門致力於阻止這種突變。
將context傳遞給shouldComponentUpdate
一個說法是,我們可以通過將context作為引數傳遞給該方法來避免shouldComponentUpdate問題,將傳入的Context與前一個Context進行比較,如果它們不同,則返回true。 問題是,與prop或state不同,我們沒有型別資訊。 上下文物件的型別取決於元件在React樹中的位置。 您可以對這兩個物件執行淺層比較,但只有在我們假定這些值是不可變的時才有效。 如果我們假設這些值是不可變的,那麼React可能會自動進行比較
基於類的API
我們可以使用我們現在使用的基於類的API,而不是render prop:
class ThemeToggler extends React.Component {
state = {theme: 'light'};
getChildContext() {
return this.state.theme;
}
render() {
return (
<>
<button
onClick={() =>
this.setState(state => ({
theme: state.theme === 'light' ? 'dark' : 'light',
}))
}>
Toggle theme
</button>
{this.props.children}
</>
);
}
}
class Title extends React.Component {
static contextType = ThemeContext;
componentDidUpdate(prevProps, prevState, prevContext) {
if (this.context !== prevContext) {
alert('Theme changed!');
}
}
render() {
return (
<h1 style={{color: this.context.theme === 'light' ? '#000' : '#fff'}}>
{this.props.children}
</h1>
);
}
}
複製程式碼
這個API的優點是您可以更輕鬆地訪問生命週期方法中的上下文,可能避免在樹中需要額外的元件。
但是,儘管增加React樹的深度會帶來一些開銷,但使用特殊元件型別的優勢在於,為消費者掃描樹會更快,因為我們可以快速跳過其他型別。 使用基於類的API時,我們必須檢查每個類元件,它稍微慢一些。 這足以抵消額外元件的成本。
與類API不同,render prop API還具有與現有Context API充分不同的優勢,我們可以在過渡時期支援這兩個版本,而不會造成太多混淆。
(注意,原文部分接下來的一些章節並未翻譯,這些章節涉及的內容是React官方的一些計劃)
未解決的問題
當Consumer沒有匹配Provider時,是否應該發出警告
對於依賴預設Context的值,這是很有效的。在很多情況下,這種錯誤應該是開發者所造成的,因此我們可以列印警告出來。為了關閉這個,你可以傳遞true給allowDetached
。
render() {
return (
// If there's no provider, this renders with the default theme.
// `allowDetached` suppresses the development warning
<ThemeContext.Consumer allowDetached={true}>
{theme => (
<h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
複製程式碼
將displayName引數新增到createContext以更好地進行除錯
對於警告和React DevTools,如果提供者和消費者具有displayName,則會有所幫助。 問題是這是否應該要求。 我們可以使其成為可選項,並使用Babel變換自動新增名稱。 這是我們用於createClass的策略。
其他
- 消費者是否應該使用
children
作為prop或命名prop? - 我們應該多快棄用和刪除現有的上下文API?
- 需要執行基準測試來確定這將是多快。
- 啟發式快取。
- 此功能的高優先順序版本用於動畫。 (可以單獨提交。)