React高級(jí)特性Context萬(wàn)字詳細(xì)解讀
Context提供了一種不需要手動(dòng)地通過(guò)props來(lái)層層傳遞的方式來(lái)傳遞數(shù)據(jù)。
正文
在典型的React應(yīng)用中,數(shù)據(jù)是通過(guò)props,自上而下地傳遞給子組件的。但是對(duì)于被大量組件使用的固定類型的數(shù)據(jù)(比如說(shuō),本地的語(yǔ)言環(huán)境,UI主題等)來(lái)說(shuō),這么做就顯得十分的累贅和笨拙。Context提供了一種在組件之間(上下層級(jí)關(guān)系的組件)共享這種類型數(shù)據(jù)的方式。這種方式不需要你手動(dòng)地,顯式地通過(guò)props將數(shù)據(jù)層層傳遞下去。
什么時(shí)候用Context?
這一小節(jié),講的是context適用的業(yè)務(wù)場(chǎng)景。
Context是為那些可以認(rèn)定為【整顆組件樹(shù)范圍內(nèi)可以共用的數(shù)據(jù)】而設(shè)計(jì)的。比如說(shuō),當(dāng)前已認(rèn)證的用戶數(shù)據(jù),UI主題數(shù)據(jù),當(dāng)前用戶的偏好語(yǔ)言設(shè)置數(shù)據(jù)等。舉個(gè)例子,下面的代碼中,為了裝飾Button component我們手動(dòng)地將一個(gè)叫“theme”的prop層層傳遞下去。 傳遞路徑是:App -> Toolbar -> ThemedButton -> Button
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// The Toolbar component must take an extra "theme" prop
// and pass it to the ThemedButton. This can become painful
// if every single button in the app needs to know the theme
// because it would have to be passed through all components.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
使用context,我們可以跳過(guò)層層傳遞所經(jīng)過(guò)的中間組件?,F(xiàn)在我們的傳遞路徑是這樣的:App -> Button。
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
在你用Context之前
這一小節(jié),講的是我們要慎用context。在用context之前,我們得考慮一下當(dāng)前的業(yè)務(wù)場(chǎng)景有沒(méi)有第二種技術(shù)方案可用。只有在確實(shí)想不出來(lái)了,才去使用context。
Context主要用于這種業(yè)務(wù)場(chǎng)景:大量處在組件樹(shù)不同層級(jí)的組件需要共享某些數(shù)據(jù)。實(shí)際開(kāi)發(fā)中,我們對(duì)context要常懷敬畏之心,謹(jǐn)慎使用。因?yàn)樗q如潘多拉的盒子,一旦打開(kāi)了,就造成很多難以控制的現(xiàn)象(在這里特指,context一旦濫用了,就會(huì)造成很多組件難以復(fù)用)。參考React實(shí)戰(zhàn)視頻講解:進(jìn)入學(xué)習(xí)
如果你只是單純想免去數(shù)據(jù)層層傳遞時(shí)對(duì)中間層組件的影響,那么組件組合是一個(gè)相比context更加簡(jiǎn)單的技術(shù)方案。
舉個(gè)例子來(lái)說(shuō),假如我們有一個(gè)叫Page的組件,它需要將user和avatarSize這兩個(gè)prop傳遞到下面好幾層的Link組件和Avatar組件:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
我們大費(fèi)周章地將user和avatarSize這兩個(gè)prop傳遞下去,最終只有Avatar組件才真正地用到它。這種做法顯得有點(diǎn)低效和多余的。假如,到后面Avatar組件需要從頂層組件再獲取一些格外的數(shù)據(jù)的話,你還得手動(dòng)地,逐層地將這些數(shù)據(jù)用prop的形式來(lái)傳遞下去。實(shí)話說(shuō),這真的很煩人。
不考慮使用context的前提下,另外一種可以解決這種問(wèn)題的技術(shù)方案是:將Avatar組件作為prop傳遞下去。這樣一來(lái),其他中間層的組件就不要知道user這個(gè)prop的存在了。
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// Now, we have:
<Page user={user} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
通過(guò)這個(gè)改動(dòng),只有最頂層的組件Page需要知道Link組件和Avatar組件需要用到“user”和“avatarSize”這兩個(gè)數(shù)據(jù)集。
在很多場(chǎng)景下,這種通過(guò)減少需要傳遞prop的個(gè)數(shù)的“控制反轉(zhuǎn)”模式讓你的代碼更干凈,并賦予了最頂層組件更多的控制權(quán)限。然而,它并不適用于每一個(gè)業(yè)務(wù)場(chǎng)景。因?yàn)檫@種方案會(huì)增加高層級(jí)組件的復(fù)雜性,并以此為代價(jià)來(lái)使得低層家的組件來(lái)變得更加靈活。而這種靈活性往往是過(guò)度的。
在“組件組合”這種技術(shù)方案中,也沒(méi)有說(shuō)限定你一個(gè)組件只能有一個(gè)子組件,你可以讓父組件擁有多個(gè)的子組件。或者甚至給每個(gè)單獨(dú)的子組件設(shè)置一個(gè)單獨(dú)的“插槽(slots)”,正如這里所介紹的那樣。
function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
這種模式對(duì)于大部分需要將子組件從它的父組件中分離開(kāi)來(lái)的場(chǎng)景是足夠有用的了。如果子組件在渲染之前需要與父組件通訊的話,你可以進(jìn)一步考慮使用render props技術(shù)。
然而,有時(shí)候你需要在不同的組件,不同的層級(jí)中去訪問(wèn)同一份數(shù)據(jù),這種情況下,還是用context比較好。Context負(fù)責(zé)集中分發(fā)你的數(shù)據(jù),在數(shù)據(jù)改變的同時(shí),能將新數(shù)據(jù)同步給它下面層級(jí)的組件。第一小節(jié)給出的范例中,使用context比使用本小節(jié)所說(shuō)的“組件組合”方案更加的簡(jiǎn)單。適用context的場(chǎng)景還包括“本地偏好設(shè)置數(shù)據(jù)”共享,“UI主題數(shù)據(jù)”共享和“緩存數(shù)據(jù)”共享等。
相關(guān)API
React.createContext
const MyContext = React.createContext(defaultValue);
該API是用于創(chuàng)建一個(gè)context object(在這里是指Mycontext)。當(dāng)React渲染一個(gè)訂閱了這個(gè)context object的組件的時(shí)候,將會(huì)從離這個(gè)組件最近的那個(gè)Provider組件讀取當(dāng)前的context值。
創(chuàng)建context object時(shí)傳入的默認(rèn)值只有組件在上層級(jí)組件樹(shù)中沒(méi)有找到對(duì)應(yīng)的的Provider組件的時(shí)候時(shí)才會(huì)使用。這對(duì)于脫離Provider組件去單獨(dú)測(cè)試組件功能是很有幫助的。注意:如果你給Provider組件value屬性提供一個(gè)undefined值,這并不會(huì)引用React使用defaultValue作為當(dāng)前的value值。也就是說(shuō),undefined仍然是一個(gè)有效的context value。
Context.Provider
<MyContext.Provider value={<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->/* some value */}>每一個(gè)context object都有其對(duì)應(yīng)的Provider組件。這個(gè)Provider組件使得Consumer組件能夠訂閱并追蹤context數(shù)據(jù)。
它接受一個(gè)叫value的屬性。這個(gè)value屬性的值將會(huì)傳遞給Provider組件所有的子孫層級(jí)的Consumer組件。這些Consumer組件會(huì)在Provider組件的value值發(fā)生變化的時(shí)候得到重新渲染。從Provider組件到其子孫Consumer組件的這種數(shù)據(jù)傳播不會(huì)受到shouldComponentUpdate(這個(gè)shouldComponentUpdate應(yīng)該是指Cousumer組件的shouldComponentUpdate)這個(gè)生命周期方法的影響。所以,只要父Provider組件發(fā)生了更新,那么作為子孫組件的Consumer組件也會(huì)隨著更新。
判定Provider組件的value值是否已經(jīng)發(fā)生了變化是通過(guò)使用類似于Object.is算法來(lái)對(duì)比新舊值實(shí)現(xiàn)的。
注意:當(dāng)你給在Provider組件的value屬性傳遞一個(gè)object的時(shí)候,用于判定value是否已經(jīng)發(fā)生改變的法則會(huì)導(dǎo)致一些問(wèn)題,見(jiàn)注意點(diǎn)。
Class.contextType
譯者注:官方文檔給出的關(guān)于這個(gè)API的例子我并沒(méi)有跑通。不知道是我理解錯(cuò)誤還是官方的文檔有誤,讀者誰(shuí)知道this.context在new context API中是如何使用的,麻煩在評(píng)論區(qū)指教一下。
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* perform a side-effect at mount using the value of MyContext */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* render something based on the value of MyContext */
}
}
MyClass.contextType = MyContext;
組件(類)的contextType靜態(tài)屬性可以賦值為一個(gè)context object。這使得這個(gè)組件類可以通過(guò)this.context來(lái)消費(fèi)離它最近的context value。this.context在組件的各種生命周期方法都是可訪問(wèn)的。
注意:
使用這個(gè)API,你只可以訂閱一個(gè)context object。如果你需要讀取多個(gè)context object,那么你可以查看Consuming Multiple Contexts。
如果你想使用ES7的實(shí)驗(yàn)性特征public class fields syntax,你可以使用static關(guān)鍵字來(lái)初始化你的contextType屬性:
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* render something based on the value */
}
}
Context.Consumer
<MyContext.Consumer>
{value => /* render something based on the context value */}
</MyContext.Consumer>
Consumer組件是負(fù)責(zé)訂閱context,并跟蹤它的變化的組件。有了它,你就可以在一個(gè)function component里面對(duì)context發(fā)起訂閱。
如上代碼所示,Consumer組件的子組件要求是一個(gè)function(注意,這里不是function component)。這個(gè)function會(huì)接收一個(gè)context value,返回一個(gè)React node。這個(gè)context value等同于離這個(gè)Consumer組件最近的Provider組件的value屬性值。假如Consumer組件在上面層級(jí)沒(méi)有這個(gè)context所對(duì)應(yīng)的Provider組件,則function接收到的context value就是創(chuàng)建context object時(shí)所用的defaultValue。
注意:這里所說(shuō)的“function as a child”就是我們所說(shuō)的render props模式。
示例
1. 動(dòng)態(tài)context
我在這個(gè)例子里面涉及到this.context的組件的某個(gè)生命周期方法里面打印console.log(this.context),控制臺(tái)打印出來(lái)是空對(duì)象。從界面來(lái)看,DOM元素button也沒(méi)有background。
這是一個(gè)關(guān)于動(dòng)態(tài)設(shè)置UI主題類型的context的更加復(fù)雜的例子:
theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
export const ThemeContext = React.createContext(
themes.dark // default value
);themed-button.js
import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
// An intermediate component that uses the ThemedButton
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
Change Theme </ThemedButton>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
}
render() {
// The ThemedButton button inside the ThemeProvider
// uses the theme from state while the one outside uses
// the default dark theme
// 以上注釋所說(shuō)的結(jié)果,我并沒(méi)有看到。
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);
2. 在內(nèi)嵌的組件中更新context
組件樹(shù)的底層組件在很多時(shí)候是需要更新Provider組件的context value的。面對(duì)這種業(yè)務(wù)場(chǎng)景,你可以在創(chuàng)建context object的時(shí)候傳入一個(gè)function類型的key-value,然后伴隨著context把它傳遞到Consumer組件當(dāng)中:
theme-context.js
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// The Theme Toggler Button receives not only the theme
// but also a toggleTheme function from the context
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => ( <button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme </button>
)} </ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;app.js
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
// State also contains the updater function so it will
// be passed down into the context provider
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// The entire state is passed to the provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
3. 同時(shí)消費(fèi)多個(gè)context
為了使得context所導(dǎo)致的重新渲染的速度更快,React要求我們對(duì)context的消費(fèi)要在單獨(dú)的Consumer組件中去進(jìn)行。
// Theme context, default to light theme
const ThemeContext = React.createContext('light');
// Signed-in user context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// App component that provides initial context values
// 兩個(gè)context的Provider組件嵌套
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// A component may consume multiple contexts
function Content() {
return (
// 兩個(gè)context的Consumer組件嵌套
<ThemeContext.Consumer>
{theme => ( <UserContext.Consumer>
{user => ( <ProfilePage user={user} theme={theme} />
)} </UserContext.Consumer>
)} </ThemeContext.Consumer>
);
}但是假如兩個(gè)或以上的context經(jīng)常被一同消費(fèi),這個(gè)時(shí)候你得考慮合并它們,使之成為一個(gè)context,并創(chuàng)建一個(gè)接受多個(gè)context作為參數(shù)的render props component。
注意點(diǎn)
因?yàn)閏ontext是使用引用相等(reference identity)來(lái)判斷是否需要re-redner的,所以當(dāng)你給Provider組件的value屬性提供一個(gè)字面量javascript對(duì)象值時(shí),這就會(huì)導(dǎo)致一些性能問(wèn)題-consumer組件發(fā)生不必要的渲染。舉個(gè)例子,下面的示例代碼中,所有的consumer組件將會(huì)在Provider組件重新渲染的時(shí)候跟著一起re-render。這是因?yàn)槊恳淮蝪alue的值都是一個(gè)新對(duì)象。
class App extends React.Component {
render() {
return (
// {something: 'something'} === {something: 'something'}的值是false
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
為了避免這個(gè)問(wèn)題,我們可以把這種引用類型的值提升到父組件的state中去:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}遺留的API
React在先前的版本中引入了一個(gè)實(shí)驗(yàn)性質(zhì)的context API。相比當(dāng)前介紹的這個(gè)context API,我們稱它為老的context API。這個(gè)老的API將會(huì)被支持到React 16.x版本結(jié)束前。但是你的app最好將它升級(jí)為上文中所介紹的新context API。這個(gè)遺留的API將會(huì)在未來(lái)的某個(gè)大版本中去除掉。
到此這篇關(guān)于React高級(jí)特性Context萬(wàn)字詳細(xì)解讀的文章就介紹到這了,更多相關(guān)React Context內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React過(guò)渡動(dòng)畫(huà)組件基礎(chǔ)使用介紹
在開(kāi)發(fā)中,我們想要給一個(gè)組件的顯示和消失添加某種過(guò)渡動(dòng)畫(huà),可以很好的增加用戶體驗(yàn)。 當(dāng)然,我們可以通過(guò)原生的CSS來(lái)實(shí)現(xiàn)這些過(guò)渡動(dòng)畫(huà),這篇文章主要介紹了React過(guò)渡動(dòng)畫(huà)組件使用2022-09-09
詳解create-react-app 2.0版本如何啟用裝飾器語(yǔ)法
這篇文章主要介紹了詳解create-react-app 2.0版本如何啟用裝飾器語(yǔ)法,cra2.0時(shí)代如何啟用裝飾器語(yǔ)法呢? 我們依舊采用的是react-app-rewired, 通過(guò)劫持webpack cofig對(duì)象, 達(dá)到修改的目的2018-10-10
React封裝高階組件實(shí)現(xiàn)路由權(quán)限的控制詳解
這篇文章主要介紹了React封裝高階組件實(shí)現(xiàn)路由權(quán)限的控制,在React中,為了實(shí)現(xiàn)安全可靠的路由權(quán)限控制,可以通過(guò)多種方式來(lái)確保只有經(jīng)過(guò)授權(quán)的用戶才能訪問(wèn)特定路徑下的資源,下面來(lái)介紹封裝高階組件控制的方法,需要的朋友可以參考下2025-02-02
react axios配置代理(proxy),如何解決本地開(kāi)發(fā)時(shí)的跨域問(wèn)題
這篇文章主要介紹了react axios配置代理(proxy),如何解決本地開(kāi)發(fā)時(shí)的跨域問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
TypeScript在React項(xiàng)目中的使用實(shí)踐總結(jié)
這篇文章主要介紹了TypeScript在React項(xiàng)目中的使用總結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
詳解React中的useMemo和useCallback的區(qū)別
React中的useMemo和useCallback是兩個(gè)重要的Hooks。常常被用于優(yōu)化組件的性能。雖然這兩個(gè)Hooks看起來(lái)很相似,但它們彼此之間還是有很大的區(qū)別的,隨著小編一起來(lái)學(xué)習(xí)吧2023-04-04
React 數(shù)據(jù)獲取與性能優(yōu)化詳解
這篇文章主要為大家介紹了React 數(shù)據(jù)獲取與性能優(yōu)化方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
react使用節(jié)流函數(shù)防止重復(fù)點(diǎn)擊問(wèn)題
這篇文章主要介紹了react使用節(jié)流函數(shù)防止重復(fù)點(diǎn)擊問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06

