一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導航

node.js|vue.js|jquery|angularjs|React|json|js教程|

服務(wù)器之家 - 編程語言 - JavaScript - React - 聊一聊我對 React Context 的理解以及應(yīng)用

聊一聊我對 React Context 的理解以及應(yīng)用

2022-02-24 16:16張國鈺 React

這篇文章主要介紹了聊一聊我對 React Context 的理解以及應(yīng)用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

前言

Context被翻譯為上下文,在編程領(lǐng)域,這是一個經(jīng)常會接觸到的概念,React中也有。

在React的官方文檔中,Context被歸類為高級部分(Advanced),屬于React的高級API,但官方并不建議在穩(wěn)定版的App中使用Context。

The vast majority of applications do not need to use content.

If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.

不過,這并非意味著我們不需要關(guān)注Context。事實上,很多優(yōu)秀的React組件都通過Context來完成自己的功能,比如react-redux的<Provider />,就是通過Context提供一個全局態(tài)的store,拖拽組件react-dnd,通過Context在組件中分發(fā)DOM的Drag和Drop事件,路由組件react-router通過Context管理路由狀態(tài)等等。在React組件開發(fā)中,如果用好Context,可以讓你的組件變得強大,而且靈活。

今天就想跟大家聊一聊,我在開發(fā)當中,所認識到的這個Context,以及我是如何使用它來進行組件開發(fā)的。

注:本文中所有提到的App皆指Web端App。

初識React Context

官方對于Context的定義

React文檔官網(wǎng)并未對Context給出“是什么”的定義,更多是描述使用的Context的場景,以及如何使用Context。

官網(wǎng)對于使用Context的場景是這樣描述的:

In Some Cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful "context" API.

簡單說就是,當你不想在組件樹中通過逐層傳遞props或者state的方式來傳遞數(shù)據(jù)時,可以使用Context來實現(xiàn)跨層級的組件數(shù)據(jù)傳遞。

聊一聊我對 React Context 的理解以及應(yīng)用

使用props或者state傳遞數(shù)據(jù),數(shù)據(jù)自頂下流。

聊一聊我對 React Context 的理解以及應(yīng)用

使用Context,可以跨越組件進行數(shù)據(jù)傳遞。

如何使用Context

如果要Context發(fā)揮作用,需要用到兩種組件,一個是Context生產(chǎn)者(Provider),通常是一個父節(jié)點,另外是一個Context的消費者(Consumer),通常是一個或者多個子節(jié)點。所以Context的使用基于生產(chǎn)者消費者模式。

對于父組件,也就是Context生產(chǎn)者,需要通過一個靜態(tài)屬性childContextTypes聲明提供給子組件的Context對象的屬性,并實現(xiàn)一個實例getChildContext方法,返回一個代表Context的純對象 (plain object) 。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react'
import PropTypes from 'prop-types'
 
class MiddleComponent extends React.Component {
 render () {
 return <ChildComponent />
 }
}
 
class ParentComponent extends React.Component {
 // 聲明Context對象屬性
 static childContextTypes = {
 propA: PropTypes.string,
 methodA: PropTypes.func
 }
 
 // 返回Context對象,方法名是約定好的
 getChildContext () {
 return {
  propA: 'propA',
  methodA: () => 'methodA'
 }
 }
 
 render () {
 return <MiddleComponent />
 }
}

而對于Context的消費者,通過如下方式訪問父組件提供的Context。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'
import PropTypes from 'prop-types'
 
class ChildComponent extends React.Component {
 // 聲明需要使用的Context屬性
 static contextTypes = {
 propA: PropTypes.string
 }
 
 render () {
 const {
  propA,
  methodA
 } = this.context
 
 console.log(`context.propA = ${propA}`) // context.propA = propA
 console.log(`context.methodA = ${methodA}`) // context.methodA = undefined
 
 return ...
 }
}

子組件需要通過一個靜態(tài)屬性contextTypes聲明后,才能訪問父組件Context對象的屬性,否則,即使屬性名沒寫錯,拿到的對象也是undefined。

對于無狀態(tài)子組件(Stateless Component),可以通過如下方式訪問父組件的Context

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import PropTypes from 'prop-types'
 
const ChildComponent = (props, context) => {
 const {
 propA
 } = context
 
 console.log(`context.propA = ${propA}`) // context.propA = propA
 
 return ...
}
 
ChildComponent.contextProps = {
 propA: PropTypes.string
}

而在接下來的發(fā)行版本中,React對Context的API做了調(diào)整,更加明確了生產(chǎn)者消費者模式的使用方式。

?
1
2
3
4
5
6
7
import React from 'react';
import ReactDOM from 'react-dom';
 
const ThemeContext = React.createContext({
 background: 'red',
 color: 'white'
});

通過靜態(tài)方法React.createContext()創(chuàng)建一個Context對象,這個Context對象包含兩個組件,<Provider />和<Consumer />。

?
1
2
3
4
5
6
7
8
9
class App extends React.Component {
 render () {
 return (
  <ThemeContext.Provider value={{background: 'green', color: 'white'}}>
  <Header />
  </ThemeContext.Provider>
 );
 }
}

<Provider />的value相當于現(xiàn)在的getChildContext()。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Header extends React.Component {
 render () {
 return (
  <Title>Hello React Context API</Title>
 );
 }
}
 
class Title extends React.Component {
 render () {
 return (
  <ThemeContext.Consumer>
  {context => (
   <h1 style={{background: context.background, color: context.color}}>
   {this.props.children}
   </h1>
  )}
  </ThemeContext.Consumer>
 );
 }
}

<Consumer />的children必須是一個函數(shù),通過函數(shù)的參數(shù)獲取<Provider />提供的Context。

可見,Context的新API更加貼近React的風格。

幾個可以直接獲取Context的地方

實際上,除了實例的context屬性(this.context),React組件還有很多個地方可以直接訪問父組件提供的Context。比如構(gòu)造方法:

  • constructor(props, context)

比如生命周期:

  • componentWillReceiveProps(nextProps, nextContext)
  • shouldComponentUpdate(nextProps, nextState, nextContext)
  • componetWillUpdate(nextProps, nextState, nextContext)

對于面向函數(shù)的無狀態(tài)組件,可以通過函數(shù)的參數(shù)直接訪問組件的Context。

?
1
2
3
const StatelessComponent = (props, context) => (
 ......
)

以上是Context的基礎(chǔ),更具體的指南內(nèi)容可參見這里

我對Context的理解

OK,說完基礎(chǔ)的東西,現(xiàn)在聊一聊我對React的Context的理解。

把Context當做組件作用域

使用React的開發(fā)者都知道,一個React App本質(zhì)就是一棵React組件樹,每個React組件相當于這棵樹上的一個節(jié)點,除了App的根節(jié)點,其他每個節(jié)點都存在一條父組件鏈。

聊一聊我對 React Context 的理解以及應(yīng)用

例如上圖,<Child />的父組件鏈是<SubNode /> -- <Node /> -- <App />,<SubNode />的父組件鏈是<Node /> -- <App />,<Node />的父組件鏈只有一個組件節(jié)點,就是<App />。

這些以樹狀連接的組件節(jié)點,實際上也組成了一棵Context樹,每個節(jié)點的Context,來自父組件鏈上所有組件節(jié)點通過getChildContext()所提供的Context對象組合而成的對象。

聊一聊我對 React Context 的理解以及應(yīng)用

有了解JS作用域鏈概念的開發(fā)者應(yīng)該都知道,JS的代碼塊在執(zhí)行期間,會創(chuàng)建一個相應(yīng)的作用域鏈,這個作用域鏈記錄著運行時JS代碼塊執(zhí)行期間所能訪問的活動對象,包括變量和函數(shù),JS程序通過作用域鏈訪問到代碼塊內(nèi)部或者外部的變量和函數(shù)。

假如以JS的作用域鏈作為類比,React組件提供的Context對象其實就好比一個提供給子組件訪問的作用域,而Context對象的屬性可以看成作用域上的活動對象。由于組件的Context由其父節(jié)點鏈上所有組件通過getChildContext()返回的Context對象組合而成,所以,組件通過Context是可以訪問到其父組件鏈上所有節(jié)點組件提供的Context的屬性。

所以,我借鑒了JS作用域鏈的思路,把Context當成是組件的作用域來使用。

關(guān)注Context的可控性和影響范圍

不過,作為組件作用域來看待的Context與常見的作用域的概念 (就我個人目前接觸到的編程語言而言) 是有所區(qū)別的。我們需要關(guān)注Context的可控性和影響范圍。

在我們平時的開發(fā)中,用到作用域或者上下文的場景是很常見,很自然,甚至是無感知的,然而,在React中使用Context并不是那么容易。父組件提供Context需要通過childContextTypes進行“聲明”,子組件使用父組件的Context屬性需要通過contextTypes進行“申請”,所以,我認為React的Context是一種“帶權(quán)限”的組件作用域。

這種“帶權(quán)限”的方式有何好處?就我個人的理解,首先是保持框架API的一致性,和propTypes一樣,使用聲明式編碼風格。另外就是,可以在一定程度上確保組件所提供的Context的可控性和影響范圍。

React App的組件是樹狀結(jié)構(gòu),一層一層延伸,父子組件是一對多的線性依賴。隨意的使用Context其實會破壞這種依賴關(guān)系,導致組件之間一些不必要的額外依賴,降低組件的復用性,進而可能會影響到App的可維護性。

聊一聊我對 React Context 的理解以及應(yīng)用

通過上圖可以看到,原本線性依賴的組件樹,由于子組件使用了父組件的Context,導致<Child />組件對<Node />和<App />都產(chǎn)生了依賴關(guān)系。一旦脫離了這兩個組件,<Child />的可用性就無法保障了,減低了<Child />的復用性。

聊一聊我對 React Context 的理解以及應(yīng)用

在我看來,通過Context暴露數(shù)據(jù)或者API不是一種優(yōu)雅的實踐方案,盡管react-redux是這么干的。因此需要一種機制,或者說約束,去降低不必要的影響。

通過childContextTypes和contextTypes這兩個靜態(tài)屬性的約束,可以在一定程度保障,只有組件自身,或者是與組件相關(guān)的其他子組件才可以隨心所欲的訪問Context的屬性,無論是數(shù)據(jù)還是函數(shù)。因為只有組件自身或者相關(guān)的子組件可以清楚它能訪問Context哪些屬性,而相對于那些與組件無關(guān)的其他組件,無論是內(nèi)部或者外部的 ,由于不清楚父組件鏈上各父組件的childContextTypes“聲明”了哪些Context屬性,所以沒法通過contextTypes“申請”相關(guān)的屬性。所以我理解為,給組件的作用域Context“帶權(quán)限”,可以在一定程度上確保Context的可控性和影響范圍。

在開發(fā)組件過程中,我們應(yīng)該時刻關(guān)注這一點,不要隨意的使用Context。

不需要優(yōu)先使用Context

作為React的高級API,React并不推薦我們優(yōu)先考慮使用Context。我的理解是:

  • Context目前還處于實驗階段,可能會在后面的發(fā)行版本中有大的變化,事實上這種情況已經(jīng)發(fā)生了,所以為了避免給今后升級帶來較大影響和麻煩,不建議在App中使用Context。
  • 盡管不建議在App中使用Context,但對于組件而言,由于影響范圍小于App,如果可以做到高內(nèi)聚,不破壞組件樹的依賴關(guān)系,那么還是可以考慮使用Context的。
  • 對于組件之間的數(shù)據(jù)通信或者狀態(tài)管理,優(yōu)先考慮用props或者state解決,然后再考慮用其他第三方成熟庫解決的,以上方法都不是最佳選擇的時候,那么再考慮使用Context。
  • Context的更新需要通過setState()觸發(fā),但是這并不是可靠的。Context支持跨組件訪問,但是,如果中間的子組件通過一些方法不響應(yīng)更新,比如shouldComponentUpdate()返回false,那么不能保證Context的更新一定可達使用Context的子組件。因此,Context的可靠性需要關(guān)注。不過更新的問題,在新版的API中得以解決。

簡而言之,只要你能確保Context是可控的,使用Context并無大礙,甚至如果能夠合理的應(yīng)用,Context其實可以給React組件開發(fā)帶來很強大的體驗。

用Context作為共享數(shù)據(jù)的媒介

官方所提到Context可以用來進行跨組件的數(shù)據(jù)通信。而我,把它理解為,好比一座橋,作為一種作為媒介進行數(shù)據(jù)共享。數(shù)據(jù)共享可以分兩類:App級與組件級。

  • App級的數(shù)據(jù)共享

App根節(jié)點組件提供的Context對象可以看成是App級的全局作用域,所以,我們利用App根節(jié)點組件提供的Context對象創(chuàng)建一些App級的全局數(shù)據(jù)。現(xiàn)成的例子可以參考react-redux,以下是<Provider />組件源碼的核心實現(xiàn):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export function createProvider(storeKey = 'store', subKey) {
 const subscriptionKey = subKey || `${storeKey}Subscription`
 
 class Provider extends Component {
  getChildContext() {
   return { [storeKey]: this[storeKey], [subscriptionKey]: null }
  }
 
  constructor(props, context) {
   super(props, context)
   this[storeKey] = props.store;
  }
 
  render() {
   return Children.only(this.props.children)
  }
 }
 
 // ......
 
 Provider.propTypes = {
  store: storeShape.isRequired,
  children: PropTypes.element.isRequired,
 }
 Provider.childContextTypes = {
  [storeKey]: storeShape.isRequired,
  [subscriptionKey]: subscriptionShape,
 }
 
 return Provider
}
 
export default createProvider()

App的根組件用<Provider />組件包裹后,本質(zhì)上就為App提供了一個全局的屬性store,相當于在整個App范圍內(nèi),共享store屬性。當然,<Provider />組件也可以包裹在其他組件中,在組件級的全局范圍內(nèi)共享store。

  • 組件級的數(shù)據(jù)共享

如果組件的功能不能單靠組件自身來完成,還需要依賴額外的子組件,那么可以利用Context構(gòu)建一個由多個子組件組合的組件。例如,react-router。

react-router的<Router />自身并不能獨立完成路由的操作和管理,因為導航鏈接和跳轉(zhuǎn)的內(nèi)容通常是分離的,因此還需要依賴<Link />和<Route />等子組件來一同完成路由的相關(guān)工作。為了讓相關(guān)的子組件一同發(fā)揮作用,react-router的實現(xiàn)方案是利用Context在<Router />、<Link />以及<Route />這些相關(guān)的組件之間共享一個router,進而完成路由的統(tǒng)一操作和管理。

下面截取<Router />、<Link />以及<Route />這些相關(guān)的組件部分源碼,以便更好的理解上述所說的。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Router.js
 
/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
 static propTypes = {
 history: PropTypes.object.isRequired,
 children: PropTypes.node
 };
 
 static contextTypes = {
 router: PropTypes.object
 };
 
 static childContextTypes = {
 router: PropTypes.object.isRequired
 };
 
 getChildContext() {
 return {
  router: {
  ...this.context.router,
  history: this.props.history,
  route: {
   location: this.props.history.location,
   match: this.state.match
  }
  }
 };
 }
 
 // ......
 
 componentWillMount() {
 const { children, history } = this.props;
 
 // ......
 
 this.unlisten = history.listen(() => {
  this.setState({
  match: this.computeMatch(history.location.pathname)
  });
 });
 }
 
 // ......
}

盡管源碼還有其他的邏輯,但<Router />的核心就是為子組件提供一個帶有router屬性的Context,同時監(jiān)聽history,一旦history發(fā)生變化,便通過setState()觸發(fā)組件重新渲染。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Link.js
 
/**
 * The public API for rendering a history-aware <a>.
 */
class Link extends React.Component {
 
 // ......
 
 static contextTypes = {
 router: PropTypes.shape({
  history: PropTypes.shape({
  push: PropTypes.func.isRequired,
  replace: PropTypes.func.isRequired,
  createHref: PropTypes.func.isRequired
  }).isRequired
 }).isRequired
 };
 
 handleClick = event => {
 if (this.props.onClick) this.props.onClick(event);
 
 if (
  !event.defaultPrevented &&
  event.button === 0 &&
  !this.props.target &&
  !isModifiedEvent(event)
 ) {
  event.preventDefault();
  // 使用<Router />組件提供的router實例
  const { history } = this.context.router;
  const { replace, to } = this.props;
 
  if (replace) {
  history.replace(to);
  } else {
  history.push(to);
  }
 }
 };
 
 render() {
 const { replace, to, innerRef, ...props } = this.props;
 
 // ...
 
 const { history } = this.context.router;
 const location =
  typeof to === "string"
  ? createLocation(to, null, null, history.location)
  : to;
 
 const href = history.createHref(location);
 return (
  <a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
 );
 }
}

<Link />的核心就是渲染<a>標簽,攔截<a>標簽的點擊事件,然后通過<Router />共享的router對history進行路由操作,進而通知<Router />重新渲染。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Route.js
 
/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
 
 // ......
 
 state = {
 match: this.computeMatch(this.props, this.context.router)
 };
 
 // 計算匹配的路徑,匹配的話,會返回一個匹配對象,否則返回null
 computeMatch(
 { computedMatch, location, path, strict, exact, sensitive },
 router
 ) {
 if (computedMatch) return computedMatch;
 
 // ......
 
 const { route } = router;
 const pathname = (location || route.location).pathname;
 
 return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
 }
 
 // ......
 
 render() {
 const { match } = this.state;
 const { children, component, render } = this.props;
 const { history, route, staticContext } = this.context.router;
 const location = this.props.location || route.location;
 const props = { match, location, history, staticContext };
 
 if (component) return match ? React.createElement(component, props) : null;
 
 if (render) return match ? render(props) : null;
 
 if (typeof children === "function") return children(props);
 
 if (children && !isEmptyChildren(children))
  return React.Children.only(children);
 
 return null;
 }
}

<Route />有一部分源碼與<Router />相似,可以實現(xiàn)路由的嵌套,但其核心是通過Context共享的router,判斷是否匹配當前路由的路徑,然后渲染組件。

通過上述的分析,可以看出,整個react-router其實就是圍繞著<Router />的Context來構(gòu)建的。

使用Context開發(fā)組件

之前,通過Context開發(fā)過一個簡單的組件,插槽分發(fā)組件。本章就借著這個插槽分發(fā)組件的開發(fā)經(jīng)歷,聊聊如何使用Context進行組件的開發(fā)。

插槽分發(fā)組件

首先說說什么是插槽分發(fā)組件,這個概念最初是在Vuejs中認識的。插槽分發(fā)是一種通過組件的組合,將父組件的內(nèi)容插入到子組件模板的技術(shù),在Vuejs中叫做Slot。

為了讓大家更加直觀的理解這個概念,我從Vuejs搬運了一段關(guān)于插槽分發(fā)的Demo。

對于提供的插槽的組件<my-component />,模板如下:

?
1
2
3
4
5
6
<div>
 <h2>我是子組件的標題</h2>
 <slot>
 只有在沒有要分發(fā)的內(nèi)容時顯示
 </slot>
</div>

對于父組件,模板如下:

?
1
2
3
4
5
6
7
<div>
 <h1>我是父組件的標題</h1>
 <my-component>
 <p>這是一些初始內(nèi)容</p>
 <p>這是更多的初始內(nèi)容</p>
 </my-component>
</div>

最終渲染的結(jié)果:

?
1
2
3
4
5
6
7
8
<div>
 <h1>我是父組件的標題</h1>
 <div>
 <h2>我是子組件的標題</h2>
 <p>這是一些初始內(nèi)容</p>
 <p>這是更多的初始內(nèi)容</p>
 </div>
</div>

可以看到組件<my-component /> 的<slot />節(jié)點最終被父組件中<my-component />節(jié)點下的內(nèi)容所替換。

Vuejs還支持具名插槽。

例如,一個布局組件<app-layout />:

?
1
2
3
4
5
6
7
8
9
10
11
<div class="container">
 <header>
 <slot name="header"></slot>
 </header>
 <main>
 <slot></slot>
 </main>
 <footer>
 <slot name="footer"></slot>
 </footer>
</div>

而在父組件模板中:

?
1
2
3
4
5
6
<app-layout>
 <h1 slot="header">這里可能是一個頁面標題</h1>
 <p>主要內(nèi)容的一個段落。</p>
 <p>另一個段落。</p>
 <p slot="footer">這里有一些聯(lián)系信息</p>
</app-layout>

最終渲染的結(jié)果:

?
1
2
3
4
5
6
7
8
9
10
11
12
<div class="container">
 <header>
 <h1>這里可能是一個頁面標題</h1>
 </header>
 <main>
 <p>主要內(nèi)容的一個段落。</p>
 <p>另一個段落。</p>
 </main>
 <footer>
 <p>這里有一些聯(lián)系信息</p>
 </footer>
</div>

插槽分發(fā)的好處體現(xiàn)在,它可以讓組件具有可抽象成模板的能力。組件自身只關(guān)心模板結(jié)構(gòu),具體的內(nèi)容交給父組件去處理,同時,不打破HTML描述DOM結(jié)構(gòu)的語法表達方式。我覺得這是一項很有意義的技術(shù),可惜,React對于這項技術(shù)的支持不是那么友好。于是我便參考Vuejs的插槽分發(fā)組件,開發(fā)了一套基于React的插槽分發(fā)組件,可以讓React組件也具模板化的能力。

對于<AppLayout />組件,我希望可以寫成下面這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AppLayout extends React.Component {
 static displayName = 'AppLayout'
 
 render () {
 return (
  <div class="container">
  <header>
   <Slot name="header"></Slot>
  </header>
  <main>
   <Slot></Slot>
  </main>
  <footer>
   <Slot name="footer"></Slot>
  </footer>
  </div>
 )
 }
}

在外層使用時,可以寫成這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
<AppLayout>
 <AddOn slot="header">
 <h1>這里可能是一個頁面標題</h1>
 </AddOn>
 <AddOn>
 <p>主要內(nèi)容的一個段落。</p>
 <p>另一個段落。</p>
 </AddOn>
 <AddOn slot="footer">
 <p>這里有一些聯(lián)系信息</p>
 </AddOn>
</AppLayout>

組件的實現(xiàn)思路

根據(jù)前面所想的,先整理一下實現(xiàn)思路。

不難看出,插槽分發(fā)組件需要依靠兩個子組件——插槽組件<Slot />和分發(fā)組件<AddOn />。插槽組件,負責打樁,提供分發(fā)內(nèi)容的坑位。分發(fā)組件,負責收集分發(fā)內(nèi)容,并提供給插槽組件去渲染分發(fā)內(nèi)容,相當于插槽的消費者。

顯然,這里遇到了一個問題,<Slot />組件與<AddOn />組件是獨立的,如何將<AddOn />的內(nèi)容填充到<Slot />中呢?解決這個問題不難,兩個獨立的模塊需要建立聯(lián)系,就給他們建立一個橋梁。那么這個橋梁要如何搭建呢?回過頭來看看之前的設(shè)想的代碼。

對于<AppLayout />組件,希望寫成下面這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AppLayout extends React.Component {
 static displayName = 'AppLayout'
 
 render () {
 return (
  <div class="container">
  <header>
   <Slot name="header"></Slot>
  </header>
  <main>
   <Slot></Slot>
  </main>
  <footer>
   <Slot name="footer"></Slot>
  </footer>
  </div>
 )
 }
}

在外層使用時,寫成這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
<AppLayout>
 <AddOn slot="header">
 <h1>這里可能是一個頁面標題</h1>
 </AddOn>
 <AddOn>
 <p>主要內(nèi)容的一個段落。</p>
 <p>另一個段落。</p>
 </AddOn>
 <AddOn slot="footer">
 <p>這里有一些聯(lián)系信息</p>
 </AddOn>
</AppLayout>

無論是<Slot />還是<AddOn />,其實都在<AppLayout />的作用域內(nèi)。<Slot />是<AppLayout />組件render()方法返回的組件節(jié)點,而<AddOn />則是<AppLayout />的children節(jié)點,所以,可以將<AppLayout />視為<Slot />與<AddOn />的橋梁的角色。那么,<AppLayout />通過什么給<Slot />和<AddOn />建立聯(lián)系呢?這里就用到本文的主角——Context。接下來的問題就是,如何使用Context給<Slot />和<AddOn />建立聯(lián)系?

前面提到了<AppLayout />這座橋梁。在外層組件,<AppLayout />負責通過<AddOn />收集為插槽填充的內(nèi)容。<AppLayout />自身借助Context定義一個獲取填充內(nèi)容的接口。在渲染的時候,因為<Slot />是<AppLayout />渲染的節(jié)點,所以,<Slot />可以通過Context獲取到<AppLayout />定義的獲取填充內(nèi)容的接口,然后通過這個接口,獲取到填充內(nèi)容進行渲染。

按照思路實現(xiàn)插槽分發(fā)組件

由于<AddOn />是<AppLayout />的children節(jié)點,并且<AddOn />是特定的組件,我們可以通過name或者displayName識別出來,所以,<AppLayout />在渲染之前,也就是render()的return之前,對children進行遍歷,以slot的值作為key,將每一個<AddOn />的children緩存下來。如果<AddOn />沒有設(shè)置slot,那么將其視為給非具名的<Slot />填充內(nèi)容,我們可以給這些非具名的插槽定一個key,比如叫$$default。

對于<AppLayout />,代碼大致如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class AppLayout extends React.Component {
 
 static childContextTypes = {
 requestAddOnRenderer: PropTypes.func
 }
 
 // 用于緩存每個<AddOn />的內(nèi)容
 addOnRenderers = {}
 
 // 通過Context為子節(jié)點提供接口
 getChildContext () {
 const requestAddOnRenderer = (name) => {
  if (!this.addOnRenderers[name]) {
  return undefined
  }
  return () => (
  this.addOnRenderers[name]
  )
 }
 return {
  requestAddOnRenderer
 }
 }
 
 render () {
 const {
  children,
  ...restProps
 } = this.props
 
 if (children) {
  // 以k-v的方式緩存<AddOn />的內(nèi)容
  const arr = React.Children.toArray(children)
  const nameChecked = []
  this.addOnRenderers = {}
  arr.forEach(item => {
  const itemType = item.type
  if (item.type.displayName === 'AddOn') {
   const slotName = item.props.slot || '$$default'
   // 確保內(nèi)容唯一性
   if (nameChecked.findIndex(item => item === stubName) !== -1) {
   throw new Error(`Slot(${slotName}) has been occupied`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(stubName)
  }
  })
 }
 
 return (
  <div class="container">
  <header>
   <Slot name="header"></Slot>
  </header>
  <main>
   <Slot></Slot>
  </main>
  <footer>
   <Slot name="footer"></Slot>
  </footer>
  </div>
 )
 }
}

<AppLayout />定義了一個Context接口requestAddOnRenderer(),requestAddOnRenderer()接口根據(jù)name返回一個函數(shù),這個返回的函數(shù)會根據(jù)name訪問addOnRenderers的屬性,addOnRenderers就是<AddOn />的內(nèi)容緩存對象。

<Slot />的實現(xiàn)很簡單,代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
//   props,    context
const Slot = ({ name, children }, { requestAddOnRenderer }) => {
 const addOnRenderer = requestAddOnRenderer(name)
 return (addOnRenderer && addOnRenderer()) ||
 children ||
 null
}
 
Slot.displayName = 'Slot'
Slot.contextTypes = { requestAddOnRenderer: PropTypes.func }
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: '$$default' }

可以看到<Slot />通過context獲取到<AppLayout />提供的接口requestAddOnRenderer(),最終渲染的主要對象就是緩存在<AppLayout />中的<AddOn />的內(nèi)容。如果沒有獲取到指定的<AddOn />的內(nèi)容,則渲染<Slot />自身的children。

<AddOn />更簡單:

?
1
2
3
4
5
const AddOn = () => null
 
AddOn.propTypes = { slot: PropTypes.string }
AddOn.defaultTypes = { slot: '$$default' }
AddOn.displayName = 'AddOn'

<AddOn />不做任何事情,僅僅返回null,它的作用就是讓<AppLayout />緩存分發(fā)給插槽的內(nèi)容。

可以讓<AppLayout />更具通用性

通過上文的代碼,基本將<AppLayout />改造成了一個具備插槽分發(fā)能力的組件,但是很明顯的,<AppLayout />并不具備通用性,我們可以將它提升成一個獨立通用的組件。

我給這個組件命名為SlotProvider

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function getDisplayName (component) {
 return component.displayName || component.name || 'component'
}
 
const slotProviderHoC = (WrappedComponent) => {
 return class extends React.Component {
 static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`
 
 static childContextTypes = {
  requestAddOnRenderer: PropTypes.func
 }
 
 // 用于緩存每個<AddOn />的內(nèi)容
 addOnRenderers = {}
 
 // 通過Context為子節(jié)點提供接口
 getChildContext () {
  const requestAddOnRenderer = (name) => {
  if (!this.addOnRenderers[name]) {
   return undefined
  }
  return () => (
   this.addOnRenderers[name]
  )
  }
  return {
  requestAddOnRenderer
  }
 }
 
 render () {
  const {
  children,
  ...restProps
  } = this.props
 
  if (children) {
  // 以k-v的方式緩存<AddOn />的內(nèi)容
  const arr = React.Children.toArray(children)
  const nameChecked = []
  this.addOnRenderers = {}
  arr.forEach(item => {
   const itemType = item.type
   if (item.type.displayName === 'AddOn') {
   const slotName = item.props.slot || '$$default'
   // 確保內(nèi)容唯一性
   if (nameChecked.findIndex(item => item === stubName) !== -1) {
    throw new Error(`Slot(${slotName}) has been occupied`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(stubName)
   }
  })
  }
  
  return (<WrappedComponent {...restProps} />)
 }
 }
}
 
export const SlotProvider = slotProviderHoC

使用React的高階組件對原來的<AppLayout />進行改造,將其轉(zhuǎn)變?yōu)橐粋€獨立通用的組件。對于原來的<AppLayout />,可以使用這個SlotProvider高階組件,轉(zhuǎn)換成一個具備插槽分發(fā)能力的組件。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { SlotProvider } from './SlotProvider.js'
 
class AppLayout extends React.Component {
 static displayName = 'AppLayout'
 
 render () {
 return (
  <div class="container">
  <header>
   <Slot name="header"></Slot>
  </header>
  <main>
   <Slot></Slot>
  </main>
  <footer>
   <Slot name="footer"></Slot>
  </footer>
  </div>
 )
 }
}
 
export default SlotProvider(AppLayout)

通過以上的經(jīng)歷,可以看到,當設(shè)計開發(fā)一個組件時,

  • 組件可能需要由一個根組件和多個子組件一起合作來完成組件功能。比如插槽分發(fā)組件實際上需要SlotProvider與<Slot />和<AddOn />一起配合使用,SlotProvider作為根組件,而<Slot />和<AddOn />都算是子組件。
  • 子組件相對于根組件的位置或者子組件之間的位置是不確定。對于SlotProvider而言,<Slot />的位置是不確定的,它會處在被SlotProvider這個高階組件所包裹的組件的模板的任何位置,而對于<Slot />和<AddOn />,他們直接的位置也不確定,一個在SlotProvider包裝的組件的內(nèi)部,另一個是SlotProvider的children。
  • 子組件之間需要依賴一些全局態(tài)的API或者數(shù)據(jù),比如<Slot />實際渲染的內(nèi)容來自于SlotProvider收集到的<AddOn />的內(nèi)容。

這時我們就需要借助一個中間者作為媒介來共享數(shù)據(jù),相比額外引入redux這些第三方模塊,直接使用Context可以更優(yōu)雅。

嘗試一下新版本的Context API

使用新版的Context API對之前的插槽分發(fā)組件進行改造。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// SlotProvider.js
 
function getDisplayName (component) {
 return component.displayName || component.name || 'component'
}
 
export const SlotContext = React.createContext({
 requestAddOnRenderer: () => {}
})
 
const slotProviderHoC = (WrappedComponent) => {
 return class extends React.Component {
 static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`
 
 // 用于緩存每個<AddOn />的內(nèi)容
 addOnRenderers = {}
 
 requestAddOnRenderer = (name) => {
  if (!this.addOnRenderers[name]) {
  return undefined
  }
  return () => (
  this.addOnRenderers[name]
  )
 }
 
 render () {
  const {
  children,
  ...restProps
  } = this.props
 
  if (children) {
  // 以k-v的方式緩存<AddOn />的內(nèi)容
  const arr = React.Children.toArray(children)
  const nameChecked = []
  this.addOnRenderers = {}
  arr.forEach(item => {
   const itemType = item.type
   if (item.type.displayName === 'AddOn') {
   const slotName = item.props.slot || '$$default'
   // 確保內(nèi)容唯一性
   if (nameChecked.findIndex(item => item === stubName) !== -1) {
    throw new Error(`Slot(${slotName}) has been occupied`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(stubName)
   }
  })
  }
  
  return (
  <SlotContext.Provider value={
   requestAddOnRenderer: this.requestAddOnRenderer
   }>
   <WrappedComponent {...restProps} />
  </SlotContext.Provider>
  )
 }
 }
}
 
export const SlotProvider = slotProviderHoC

移除了之前的childContextTypes和getChildContext(),除了局部的調(diào)整,整體核心的東西沒有大變化。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Slot.js
 
import { SlotContext } from './SlotProvider.js'
 
const Slot = ({ name, children }) => {
 return (
 <SlotContext.Consumer>
  {(context) => {
  const addOnRenderer = requestAddOnRenderer(name)
   return (addOnRenderer && addOnRenderer()) ||
   children ||
   null
  }}
 </SlotContext.Consumer>
 )
}
 
Slot.displayName = 'Slot'
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: '$$default' }

由于之前就按照生產(chǎn)者消費者的模式來使用Context,加上組件自身也比較簡單,因此使用新的API進行改造后,差別不大。

總結(jié)

  • 相比props和state,React的Context可以實現(xiàn)跨層級的組件通信。
  • Context API的使用基于生產(chǎn)者消費者模式。生產(chǎn)者一方,通過組件靜態(tài)屬性childContextTypes聲明,然后通過實例方法getChildContext()創(chuàng)建Context對象。消費者一方,通過組件靜態(tài)屬性contextTypes申請要用到的Context屬性,然后通過實例的context訪問Context的屬性。
  • 使用Context需要多一些思考,不建議在App中使用Context,但如果開發(fā)組件過程中可以確保組件的內(nèi)聚性,可控可維護,不破壞組件樹的依賴關(guān)系,影響范圍小,可以考慮使用Context解決一些問題。
  • 通過Context暴露API或許在一定程度上給解決一些問題帶來便利,但個人認為不是一個很好的實踐,需要慎重。
  • 舊版本的Context的更新需要依賴setState(),是不可靠的,不過這個問題在新版的API中得以解決。
  • 可以把Context當做組件的作用域來看待,但是需要關(guān)注Context的可控性和影響范圍,使用之前,先分析是否真的有必要使用,避免過度使用所帶來的一些副作用。
  • 可以把Context當做媒介,進行App級或者組件級的數(shù)據(jù)共享。
  • 設(shè)計開發(fā)一個組件,如果這個組件需要多個組件關(guān)聯(lián)組合的,使用Context或許可以更加優(yōu)雅。

以上是我的分享內(nèi)容,如有不足或者錯誤的地方,歡迎批評指正。

引用

Context - https://reactjs.org/docs/context.html
React 16.3來了:帶著全新的Context API - http://cnodejs.org/topic/5a7bd5c4497a08f571384f03
Content Distribution with Slots - https://vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots

到此這篇關(guān)于聊一聊我對 React Context 的理解以及應(yīng)用的文章就介紹到這了,更多相關(guān)React Context 內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!

原文鏈接:https://www.jianshu.com/p/eba2b76b290b

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 别停好爽好深好大好舒服视频 | 爱情岛永久成人免费网站 | 好男人资源免费播放 | 国产va欧美va在线观看 | 亚洲男人精品 | 欧美色综合高清免费 | 亚洲 欧美 国产 综合久久 | 国产精品久久亚洲一区二区 | 日韩风月片 | 久久黄色精品视频 | 亚洲精品在线免费 | 亚洲乱亚洲乱妇41p国产成人 | 国产资源一区 | 精品久久成人 | 日本情趣视频 | bl高h荡肉古代np | 精品国产91高清在线观看 | 成人国产网站v片免费观看 成人国产精品视频 | 亚洲 欧美 日韩 国产 视频 | 久久中文字幕乱码免费 | 精品麻豆 | 黄瓜视频导航 | 日本在线一区二区 | 亚洲男人天堂网址 | 国产999在线观看 | 扒开女人下面使劲桶屁股动漫 | 性色生活片在线观看 | 欧美精品超清在线播放 | 1717国产精品视频免费 | 999任你躁在线精品免费不卡 | chinesespank调教| 私人影院免费观看 | 国色天香社区视频在线观看免费完整版 | 亚洲天堂视频在线观看免费 | 亚洲乱亚洲乱妇41p国产成人 | 亚洲第一免费播放区 | 91精品国产一区 | 福利一区二区在线观看 | 精品湿| 丁香五香天堂网 | 亚洲黄色小视频 |