成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

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

chengjianhua / 1276人閱讀

摘要:假如以的作用域鏈作為類(lèi)比,組件提供的對(duì)象其實(shí)就好比一個(gè)提供給子組件訪(fǎng)問(wèn)的作用域,而對(duì)象的屬性可以看成作用域上的活動(dòng)對(duì)象。所以,我借鑒了作用域鏈的思路,把當(dāng)成是組件的作用域來(lái)使用。

前言

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

在React的官方文檔中,Context被歸類(lèi)為高級(jí)部分(Advanced),屬于React的高級(jí)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ò),這并非意味著我們不需要關(guān)注Context。事實(shí)上,很多優(yōu)秀的React組件都通過(guò)Context來(lái)完成自己的功能,比如react-redux的,就是通過(guò)Context提供一個(gè)全局態(tài)的store,拖拽組件react-dnd,通過(guò)Context在組件中分發(fā)DOM的Drag和Drop事件,路由組件react-router通過(guò)Context管理路由狀態(tài)等等。在React組件開(kāi)發(fā)中,如果用好Context,可以讓你的組件變得強(qiáng)大,而且靈活。

今天就想跟大家聊一聊,我在開(kāi)發(fā)當(dāng)中,所認(rèn)識(shí)到的這個(gè)Context,以及我是如何使用它來(lái)進(jìn)行組件開(kāi)發(fā)的。

注:本文中所有提到的App皆指Web端App。
初識(shí)React Context 官方對(duì)于Context的定義

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

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

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.

簡(jiǎn)單說(shuō)就是,當(dāng)你不想在組件樹(shù)中通過(guò)逐層傳遞props或者state的方式來(lái)傳遞數(shù)據(jù)時(shí),可以使用Context來(lái)實(shí)現(xiàn)跨層級(jí)的組件數(shù)據(jù)傳遞。

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

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

如何使用Context

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

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

import React from "react"
import PropTypes from "prop-types"

class MiddleComponent extends React.Component {
  render () {
    return 
  }
}

class ParentComponent extends React.Component {
  // 聲明Context對(duì)象屬性
  static childContextTypes = {
    propA: PropTypes.string,
    methodA: PropTypes.func
  }
  
  // 返回Context對(duì)象,方法名是約定好的
  getChildContext () {
    return {
      propA: "propA",
      methodA: () => "methodA"
    }
  }
  
  render () {
    return 
  }
}

而對(duì)于Context的消費(fèi)者,通過(guò)如下方式訪(fǎng)問(wèn)父組件提供的Context。

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 ...
  }
}

子組件需要通過(guò)一個(gè)靜態(tài)屬性contextTypes聲明后,才能訪(fǎng)問(wèn)父組件Context對(duì)象的屬性,否則,即使屬性名沒(méi)寫(xiě)錯(cuò),拿到的對(duì)象也是undefined。

對(duì)于無(wú)狀態(tài)子組件(Stateless Component),可以通過(guò)如下方式訪(fǎng)問(wèn)父組件的Context

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    
}

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

import React from "react";
import ReactDOM from "react-dom";

const ThemeContext = React.createContext({
  background: "red",
  color: "white"
});

通過(guò)靜態(tài)方法React.createContext()創(chuàng)建一個(gè)Context對(duì)象,這個(gè)Context對(duì)象包含兩個(gè)組件,。

class App extends React.Component {
  render () {
    return (
      
        
); } }

value相當(dāng)于現(xiàn)在的getChildContext()

class Header extends React.Component {
  render () {
    return (
      Hello React Context API
    );
  }
}
 
class Title extends React.Component {
  render () {
    return (
      
        {context => (
          

{this.props.children}

)}
); } }

children必須是一個(gè)函數(shù),通過(guò)函數(shù)的參數(shù)獲取提供的Context

可見(jiàn),Context的新API更加貼近React的風(fēng)格。

幾個(gè)可以直接獲取Context的地方

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

constructor(props, context)

比如生命周期:

componentWillReceiveProps(nextProps, nextContext)

shouldComponentUpdate(nextProps, nextState, nextContext)

componetWillUpdate(nextProps, nextState, nextContext)

對(duì)于面向函數(shù)的無(wú)狀態(tài)組件,可以通過(guò)函數(shù)的參數(shù)直接訪(fǎng)問(wèn)組件的Context。

const StatelessComponent = (props, context) => (
  ......
)

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

我對(duì)Context的理解

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

把Context當(dāng)做組件作用域

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

例如上圖,的父組件鏈?zhǔn)?b> -- -- ,的父組件鏈?zhǔn)?b> -- 的父組件鏈只有一個(gè)組件節(jié)點(diǎn),就是。

這些以樹(shù)狀連接的組件節(jié)點(diǎn),實(shí)際上也組成了一棵Context樹(shù),每個(gè)節(jié)點(diǎn)的Context,來(lái)自父組件鏈上所有組件節(jié)點(diǎn)通過(guò)getChildContext()所提供的Context對(duì)象組合而成的對(duì)象。

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

假如以JS的作用域鏈作為類(lèi)比,React組件提供的Context對(duì)象其實(shí)就好比一個(gè)提供給子組件訪(fǎng)問(wèn)的作用域,而Context對(duì)象的屬性可以看成作用域上的活動(dòng)對(duì)象。由于組件的Context由其父節(jié)點(diǎn)鏈上所有組件通過(guò)getChildContext()返回的Context對(duì)象組合而成,所以,組件通過(guò)Context是可以訪(fǎng)問(wèn)到其父組件鏈上所有節(jié)點(diǎn)組件提供的Context的屬性。

所以,我借鑒了JS作用域鏈的思路,把Context當(dāng)成是組件的作用域來(lái)使用。

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

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

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

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

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

通過(guò)上圖可以看到,原本線(xiàn)性依賴(lài)的組件樹(shù),由于子組件使用了父組件的Context,導(dǎo)致組件對(duì)都產(chǎn)生了依賴(lài)關(guān)系。一旦脫離了這兩個(gè)組件,的可用性就無(wú)法保障了,減低了的復(fù)用性。

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

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

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

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

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

Context目前還處于實(shí)驗(yàn)階段,可能會(huì)在后面的發(fā)行版本中有大的變化,事實(shí)上這種情況已經(jīng)發(fā)生了,所以為了避免給今后升級(jí)帶來(lái)較大影響和麻煩,不建議在App中使用Context。

盡管不建議在App中使用Context,但對(duì)于組件而言,由于影響范圍小于App,如果可以做到高內(nèi)聚,不破壞組件樹(shù)的依賴(lài)關(guān)系,那么還是可以考慮使用Context的。

對(duì)于組件之間的數(shù)據(jù)通信或者狀態(tài)管理,優(yōu)先考慮用props或者state解決,然后再考慮用其他第三方成熟庫(kù)解決的,以上方法都不是最佳選擇的時(shí)候,那么再考慮使用Context。

Context的更新需要通過(guò)setState()觸發(fā),但是這并不是可靠的。Context支持跨組件訪(fǎng)問(wèn),但是,如果中間的子組件通過(guò)一些方法不響應(yīng)更新,比如shouldComponentUpdate()返回false,那么不能保證Context的更新一定可達(dá)使用Context的子組件。因此,Context的可靠性需要關(guān)注。不過(guò)更新的問(wèn)題,在新版的API中得以解決。

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

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

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

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

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

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的根組件用組件包裹后,本質(zhì)上就為App提供了一個(gè)全局的屬性store,相當(dāng)于在整個(gè)App范圍內(nèi),共享store屬性。當(dāng)然,組件也可以包裹在其他組件中,在組件級(jí)的全局范圍內(nèi)共享store

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

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

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

下面截取以及這些相關(guān)的組件部分源碼,以便更好的理解上述所說(shuō)的。

// 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)
      });
    });
  }

  // ......
}

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

// Link.js

/**
 * The public API for rendering a history-aware .
 */
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實(shí)例
      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 (
      
    );
  }
}

的核心就是渲染標(biāo)簽,攔截標(biāo)簽的點(diǎn)擊事件,然后通過(guò)共享的router對(duì)history進(jìn)行路由操作,進(jìn)而通知重新渲染。

// 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)
  };

  // 計(jì)算匹配的路徑,匹配的話(huà),會(huì)返回一個(gè)匹配對(duì)象,否則返回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;
  }
}

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

通過(guò)上述的分析,可以看出,整個(gè)react-router其實(shí)就是圍繞著Context來(lái)構(gòu)建的。

使用Context開(kāi)發(fā)組件

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

插槽分發(fā)組件

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

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

對(duì)于提供的插槽的組件,模板如下:

我是子組件的標(biāo)題

只有在沒(méi)有要分發(fā)的內(nèi)容時(shí)顯示

對(duì)于父組件,模板如下:

我是父組件的標(biāo)題

這是一些初始內(nèi)容

這是更多的初始內(nèi)容

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

我是父組件的標(biāo)題

我是子組件的標(biāo)題

這是一些初始內(nèi)容

這是更多的初始內(nèi)容

可以看到組件 節(jié)點(diǎn)最終被父組件中節(jié)點(diǎn)下的內(nèi)容所替換。

Vuejs還支持具名插槽。

例如,一個(gè)布局組件

而在父組件模板中:


  

這里可能是一個(gè)頁(yè)面標(biāo)題

主要內(nèi)容的一個(gè)段落。

另一個(gè)段落。

這里有一些聯(lián)系信息

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

這里可能是一個(gè)頁(yè)面標(biāo)題

主要內(nèi)容的一個(gè)段落。

另一個(gè)段落。

這里有一些聯(lián)系信息

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

對(duì)于組件,我希望可以寫(xiě)成下面這樣:

class AppLayout extends React.Component {
  static displayName = "AppLayout"
  
  render () {
    return (
      
) } }

在外層使用時(shí),可以寫(xiě)成這樣:


  
    

這里可能是一個(gè)頁(yè)面標(biāo)題

主要內(nèi)容的一個(gè)段落。

另一個(gè)段落。

這里有一些聯(lián)系信息

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

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

不難看出,插槽分發(fā)組件需要依靠?jī)蓚€(gè)子組件——插槽組件和分發(fā)組件。插槽組件,負(fù)責(zé)打樁,提供分發(fā)內(nèi)容的坑位。分發(fā)組件,負(fù)責(zé)收集分發(fā)內(nèi)容,并提供給插槽組件去渲染分發(fā)內(nèi)容,相當(dāng)于插槽的消費(fèi)者。

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

對(duì)于組件,希望寫(xiě)成下面這樣:

class AppLayout extends React.Component {
  static displayName = "AppLayout"
  
  render () {
    return (
      
) } }

在外層使用時(shí),寫(xiě)成這樣:


  
    

這里可能是一個(gè)頁(yè)面標(biāo)題

主要內(nèi)容的一個(gè)段落。

另一個(gè)段落。

這里有一些聯(lián)系信息

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

前面提到了這座橋梁。在外層組件,負(fù)責(zé)通過(guò)收集為插槽填充的內(nèi)容。自身借助Context定義一個(gè)獲取填充內(nèi)容的接口。在渲染的時(shí)候,因?yàn)?b>渲染的節(jié)點(diǎn),所以,可以通過(guò)Context獲取到定義的獲取填充內(nèi)容的接口,然后通過(guò)這個(gè)接口,獲取到填充內(nèi)容進(jìn)行渲染。

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

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

對(duì)于,代碼大致如下:

class AppLayout extends React.Component {
  
  static childContextTypes = {
    requestAddOnRenderer: PropTypes.func
  }
  
  // 用于緩存每個(gè)的內(nèi)容
  addOnRenderers = {}
  
  // 通過(guò)Context為子節(jié)點(diǎn)提供接口
  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的方式緩存的內(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 (
      
) } }

定義了一個(gè)Context接口requestAddOnRenderer(),requestAddOnRenderer()接口根據(jù)name返回一個(gè)函數(shù),這個(gè)返回的函數(shù)會(huì)根據(jù)name訪(fǎng)問(wèn)addOnRenderers的屬性,addOnRenderers就是的內(nèi)容緩存對(duì)象。

的實(shí)現(xiàn)很簡(jiǎn)單,代碼如下:

//            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" }

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

更簡(jiǎn)單:

const AddOn = () => null

AddOn.propTypes = { slot: PropTypes.string }
AddOn.defaultTypes = { slot: "$$default" }
AddOn.displayName = "AddOn"

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

可以讓更具通用性

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

我給這個(gè)組件命名為SlotProvider

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
    }
  
    // 用于緩存每個(gè)的內(nèi)容
    addOnRenderers = {}
  
    // 通過(guò)Context為子節(jié)點(diǎn)提供接口
    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的方式緩存的內(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 ()
    }
  }
}

export const SlotProvider = slotProviderHoC

使用React的高階組件對(duì)原來(lái)的進(jìn)行改造,將其轉(zhuǎn)變?yōu)橐粋€(gè)獨(dú)立通用的組件。對(duì)于原來(lái)的,可以使用這個(gè)SlotProvider高階組件,轉(zhuǎn)換成一個(gè)具備插槽分發(fā)能力的組件。

import { SlotProvider } from "./SlotProvider.js"

class AppLayout extends React.Component {
  static displayName = "AppLayout"
  
  render () {
    return (
      
) } } export default SlotProvider(AppLayout)

通過(guò)以上的經(jīng)歷,可以看到,當(dāng)設(shè)計(jì)開(kāi)發(fā)一個(gè)組件時(shí),

組件可能需要由一個(gè)根組件和多個(gè)子組件一起合作來(lái)完成組件功能。比如插槽分發(fā)組件實(shí)際上需要SlotProvider一起配合使用,SlotProvider作為根組件,而都算是子組件。

子組件相對(duì)于根組件的位置或者子組件之間的位置是不確定。對(duì)于SlotProvider而言,的位置是不確定的,它會(huì)處在被SlotProvider這個(gè)高階組件所包裹的組件的模板的任何位置,而對(duì)于,他們直接的位置也不確定,一個(gè)在SlotProvider包裝的組件的內(nèi)部,另一個(gè)是SlotProviderchildren

子組件之間需要依賴(lài)一些全局態(tài)的API或者數(shù)據(jù),比如實(shí)際渲染的內(nèi)容來(lái)自于SlotProvider收集到的的內(nèi)容。

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

嘗試一下新版本的Context API

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

// 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)})`

    // 用于緩存每個(gè)的內(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的方式緩存的內(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 (
        
          
        
      )
    }
  }
}

export const SlotProvider = slotProviderHoC

移除了之前的childContextTypesgetChildContext(),除了局部的調(diào)整,整體核心的東西沒(méi)有大變化。

// Slot.js

import { SlotContext } from "./SlotProvider.js"

const Slot = ({ name, children }) => {
  return (
    
      {(context) => {
        const addOnRenderer = requestAddOnRenderer(name)
          return (addOnRenderer && addOnRenderer()) ||
            children ||
            null
      }}
    
  )
}

Slot.displayName = "Slot"
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: "$$default" }

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

總結(jié)

相比propsstate,React的Context可以實(shí)現(xiàn)跨層級(jí)的組件通信。

Context API的使用基于生產(chǎn)者消費(fèi)者模式。生產(chǎn)者一方,通過(guò)組件靜態(tài)屬性childContextTypes聲明,然后通過(guò)實(shí)例方法getChildContext()創(chuàng)建Context對(duì)象。消費(fèi)者一方,通過(guò)組件靜態(tài)屬性contextTypes申請(qǐng)要用到的Context屬性,然后通過(guò)實(shí)例的context訪(fǎng)問(wèn)Context的屬性。

使用Context需要多一些思考,不建議在App中使用Context,但如果開(kāi)發(fā)組件過(guò)程中可以確保組件的內(nèi)聚性,可控可維護(hù),不破壞組件樹(shù)的依賴(lài)關(guān)系,影響范圍小,可以考慮使用Context解決一些問(wèn)題。

通過(guò)Context暴露API或許在一定程度上給解決一些問(wèn)題帶來(lái)便利,但個(gè)人認(rèn)為不是一個(gè)很好的實(shí)踐,需要慎重。

舊版本的Context的更新需要依賴(lài)setState(),是不可靠的,不過(guò)這個(gè)問(wèn)題在新版的API中得以解決。

可以把Context當(dāng)做組件的作用域來(lái)看待,但是需要關(guān)注Context的可控性和影響范圍,使用之前,先分析是否真的有必要使用,避免過(guò)度使用所帶來(lái)的一些副作用。

可以把Context當(dāng)做媒介,進(jìn)行App級(jí)或者組件級(jí)的數(shù)據(jù)共享。

設(shè)計(jì)開(kāi)發(fā)一個(gè)組件,如果這個(gè)組件需要多個(gè)組件關(guān)聯(lián)組合的,使用Context或許可以更加優(yōu)雅。

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

引用

Context - https://reactjs.org/docs/cont...

React 16.3來(lái)了:帶著全新的Context API - http://cnodejs.org/topic/5a7b...

Content Distribution with Slots - https://vuejs.org/v2/guide/co...

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/107611.html

相關(guān)文章

  • [一聊系列]一聊前端模板與渲染那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼作為現(xiàn)代應(yīng)用,的大量使用,使得前端工程師們?nèi)粘5拈_(kāi)發(fā)少不了拼裝模板,渲染模板。我們今天就來(lái)聊聊,拼裝與渲染模板的那些事兒。一改俱改,一板兩用。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog...

    UCloud 評(píng)論0 收藏0
  • [一聊系列]一聊前端模板與渲染那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼作為現(xiàn)代應(yīng)用,的大量使用,使得前端工程師們?nèi)粘5拈_(kāi)發(fā)少不了拼裝模板,渲染模板。我們今天就來(lái)聊聊,拼裝與渲染模板的那些事兒。一改俱改,一板兩用。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog...

    Yangder 評(píng)論0 收藏0
  • [一聊系列]一聊前端模板與渲染那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼作為現(xiàn)代應(yīng)用,的大量使用,使得前端工程師們?nèi)粘5拈_(kāi)發(fā)少不了拼裝模板,渲染模板。我們今天就來(lái)聊聊,拼裝與渲染模板的那些事兒。一改俱改,一板兩用。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog...

    褰辯話(huà) 評(píng)論0 收藏0
  • 一聊Vue組件模版,你知道它有幾種定義方式嗎?

    摘要:活動(dòng)結(jié)束單文件組件使用構(gòu)建工具創(chuàng)建項(xiàng)目,綜合來(lái)看單文件組件應(yīng)該是最好的定義組件的方式,而且不會(huì)帶來(lái)額外的模版語(yǔ)法的學(xué)習(xí)成本。 前端組件化開(kāi)發(fā)已經(jīng)是一個(gè)老生常談的話(huà)題了,組件化讓我們的開(kāi)發(fā)效率以及維護(hù)成本帶來(lái)了質(zhì)的提升。 當(dāng)然因?yàn)楝F(xiàn)在的系統(tǒng)越來(lái)越復(fù)雜龐大,所以開(kāi)發(fā)與維護(hù)成本就變得必須要考慮的問(wèn)題,因此滋生出了目前的三大前端框架 Vue、Angular、React。 那今天我們就來(lái)看看 V...

    instein 評(píng)論0 收藏0
  • 一聊koa

    摘要:在的方法的最后將執(zhí)行上下文對(duì)象和方法返回的匿名函數(shù)作為參數(shù)調(diào)用方法。在方法的最后執(zhí)行匿名函數(shù),并傳入和函數(shù)分別處理正常請(qǐng)求響應(yīng)流程和異常情況。 目標(biāo) 本文主要通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)解釋koa的內(nèi)部原理。 koa的一個(gè)簡(jiǎn)單例子 const Koa = require(koa); const app = new Koa(); app.use(async ctx => { ctx.bod...

    Towers 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<