React 系列十一:高階組件以及組件補充

快來加入咱們吧!

"小和山的菜鳥們",爲前端開發者提供技術相關資訊以及系列基礎文章。爲更好的用戶體驗,請您移至咱們官網小和山的菜鳥們 ( xhs-rookies.com/ ) 進行學習,及時獲取最新文章。css

"Code tailor" ,若是您對咱們文章感興趣、或是想提一些建議,微信關注 「小和山的菜鳥們」 公衆號,與咱們取的聯繫,您也能夠在微信上觀看咱們的文章。每個建議或是贊同都是對咱們極大的鼓勵!html

前言

這節咱們將介紹 React 中高階組件,以及高階組件到底有什麼用,以及對高階組件的補充。前端

本文會向你介紹如下內容:react

  • 認識高階組件
  • 高階組件的使用
  • 高階組件的意義
  • 高階組件的注意點
  • 高階組件中轉發 refs
  • Portals
  • Fragment
  • 嚴格模式-StrictMode

高階組件

認識高階組件

什麼是高階組件呢?相信不少同窗都據說過,也用過高階函數,它們很是類似,因此咱們能夠先來回顧一下什麼是高階函數git

高階函數的維基百科定義:至少知足如下條件之一:github

  • 接受一個或多個函數做爲輸入;
  • 輸出一個函數;

JavaScript 中比較常見的 filtermapreduce 都是高階函數。web

那麼什麼是高階組件?算法

  • 高階組件的英文是 Higher-Order Components,簡稱爲 HOC,是 React 中用於複用組件邏輯的一種高級技巧。
  • 官方的定義:高階組件是參數爲組件,返回值爲新組件的函數

由此,我麼能夠分析出:設計模式

  • 高階組件自己不是一個組件,而是一個函數
  • 這個函數的參數是一個組件,返回值也是一個組件

高階組件的調用過程相似於這樣:api

const EnhancedComponent = higherOrderComponent(WrappedComponent)
複製代碼

組件是將 props 轉換爲 UI,而高階組件是將組件轉換爲另外一個組件。

高階函數的編寫過程相似於這樣:

  • 返回類組件,適合有狀態處理、用到生命週期的需求
function higherOrderComponent(WrapperComponent) {
  return class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
}
複製代碼
  • 返回函數組件,適合簡單的邏輯處理
function higherOrderComponent(WrapperComponent) {
  return (props) => {
    if (props.token) {
      return <WrapperComponent />
    } else {
      return <></>
    }
  }
}
複製代碼

在 ES6 中,類表達式中類名是能夠省略的,因此有如下這種寫法:

function higherOrderComponent(WrapperComponent) {
  return class extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
}
複製代碼

組件名稱是能夠經過 displayName 來修改的:

function higherOrderComponent(WrapperComponent) {
  class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
  NewComponent.displayName = 'xhsRookies'
  return NewComponent
}
複製代碼

**注意:**高階組件並非 React API 的一部分,它是基於 React 的組合特性而造成的設計模式;

因此,在咱們的開發中,高階組件能夠幫助咱們作哪些事情呢?往下看吧!

高階組件的使用

props 的加強

一、不修改原有代碼的狀況下,添加新的 props 屬性

假如咱們有以下案例:

class XhsRookies extends PureComponent {
  render() {
    const { name, age } = this.props
    return <h2>XhsRookies {name + age}</h2>
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div> <XhsRookies name="xhsRookies" age={18} /> </div>
    )
  }
}
複製代碼

咱們能夠經過一個高階組件,在不破壞原有 props 的狀況下,對組件加強,假如須要爲 XhsRookies 組件的 props 增長一個 height 屬性,咱們能夠這樣作:

class XhsRookies extends PureComponent {
  render() {
    const { name, age } = this.props
    return <h2>XhsRookies {name + age}</h2>
  }
}

function enhanceProps(WrapperComponent, newProps) {
  return (props) => <WrapperComponent {...props} {...newProps} />
}

const EnhanceHeader = enhanceProps(XhsRookies, { height: 1.88 })

export default class App extends PureComponent {
  render() {
    return (
      <div> <EnhanceHeader name="xhsRookies" age={18} /> </div>
    )
  }
}
複製代碼

利用高階組件來共享 Context

import React, { PureComponent, createContext } from 'react'

const UserContext = createContext({
  nickname: '默認',
  level: -1,
})

function XhsRookies(props) {
  return (
    <UserContext.Consumer> {(value) => { const { nickname, level } = value return <h2>Header {'暱稱:' + nickname + '等級' + level}</h2> }} </UserContext.Consumer>
  )
}

export default class App extends PureComponent {
  render() {
    return (
      <div> <UserContext.Provider value={{ nickname: 'xhsRookies', level: 99 }}> <XhsRookies /> </UserContext.Provider> </div>
    )
  }
}
複製代碼

咱們定義一個高階組件 ShareContextHOC,來共享 context

import React, { PureComponent, createContext } from 'react'

const UserContext = createContext({
  nickname: '默認',
  level: -1,
})

function ShareContextHOC(WrapperCpn) {
  return (props) => {
    return (
      <UserContext.Consumer> {(value) => { return <WrapperCpn {...props} {...value} /> }} </UserContext.Consumer>
    )
  }
}

function XhsRookies(props) {
  const { nickname, level } = props
  return <h2>Header {'暱稱:' + nickname + '等級:' + level}</h2>
}

function Footer(props) {
  const { nickname, level } = props
  return <h2>Footer {'暱稱:' + nickname + '等級:' + level}</h2>
}

const NewXhsRookies = ShareContextHOC(Header)

export default class App extends PureComponent {
  render() {
    return (
      <div> <UserContext.Provider value={{ nickname: 'xhsRookies', level: 99 }}> <NewXhsRookies /> </UserContext.Provider> </div>
    )
  }
}
複製代碼

渲染判斷鑑權

在開發中,咱們會遇到如下場景:

  • 某些頁面是必須用戶登陸成功才能進入
  • 若是用戶沒有登陸成功,直接跳轉到登陸頁面

這種場景下咱們可使用高階組件來完成鑑權操做:

function LoginPage() {
  // 登陸頁面
  return <h2>LoginPage</h2>
}

function HomePage() {
  // 登陸成功可訪問頁面
  return <h2>HomePage</h2>
}

export default class App extends PureComponent {
  render() {
    return (
      <div> <HomePage /> </div>
    )
  }
}
複製代碼

使用鑑權組件:

import React, { PureComponent } from 'react'

function loginAuthority(Page) {
  return (props) => {
    if (props.isLogin) {
      // 若是登陸成功 返回成功頁面
      return <Page />
    } else {
      // 若是爲登陸成功 返回登陸頁面
      return <LoginPage />
    }
  }
}

function LoginPage() {
  return <h2>LoginPage</h2>
}

function HomePage() {
  return <h2>HomePage</h2>
}

const AuthorityPassPage = loginAuthority(HomePage)

export default class App extends PureComponent {
  render() {
    return (
      <div> <AuthorityPassPage isLogin={true} /> </div>
    )
  }
}
複製代碼

生命週期劫持

當多個組件,須要在生命週期中作一些事情,而這些事情都是相同的邏輯,咱們就能夠利用高階組件,統一幫助這些組件,完成這些工做,以下例子:

import React, { PureComponent } from 'react'

class Home extends PureComponent {
  componentDidMount() {
    const nowTime = Date.now()
    console.log(`Home渲染使用時間:${nowTime}`)
  }

  render() {
    return (
      <div> <h2>Home</h2> <p>我是home的元素,哈哈哈</p> </div>
    )
  }
}

class Detail extends PureComponent {
  componentDidMount() {
    const nowTime = Date.now()
    console.log(`Detail渲染使用時間:${nowTime}`)
  }

  render() {
    return (
      <div> <h2>Detail</h2> <p>我是detail的元素,哈哈哈</p> </div>
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div> <Home /> <Detail /> </div>
    )
  }
}
複製代碼

咱們能夠利用高階租價,幫助完成 home 組件和 detail 組件的 componentDidMount 生命週期函數:

import React, { PureComponent } from 'react'

function logRenderTime(WrapperCpn) {
  return class extends PureComponent {
    componentDidMount() {
      const nowTime = Date.now()
      console.log(`${WrapperCpn.name}渲染使用時間:${nowTime}`)
    }

    render() {
      return <WrapperCpn {...this.props} />
    }
  }
}

class Home extends PureComponent {
  render() {
    return (
      <div> <h2>Home</h2> <p>我是home的元素,哈哈哈</p> </div>
    )
  }
}

class Detail extends PureComponent {
  render() {
    return (
      <div> <h2>Detail</h2> <p>我是detail的元素,哈哈哈</p> </div>
    )
  }
}

const LogHome = logRenderTime(Home)
const LogDetail = logRenderTime(Detail)

export default class App extends PureComponent {
  render() {
    return (
      <div> <LogHome /> <LogDetail /> </div>
    )
  }
}
複製代碼

高階組件的意義

經過上面不一樣狀況對高階組件的使用,咱們能夠發現利用高階組件能夠針對某些 React 代碼進行更加優雅的處理。

其實早期的 React 有提供組件之間的一種複用方式是 mixin,目前已經再也不建議使用:

  • Mixin 可能會相互依賴,相互耦合,不利於代碼維護
  • 不一樣的Mixin中的方法可能會相互衝突
  • Mixin很是多時,組件是能夠感知到的,甚至還要爲其作相關處理,這樣會給代碼形成滾雪球式的複雜性

固然,HOC 也有本身的一些缺陷:

  • HOC須要在原組件上進行包裹或者嵌套,若是大量使用HOC,將會產生很是多的嵌套,這讓調試變得很是困難;
  • HOC能夠劫持props,在不遵照約定的狀況下也可能形成衝突;

合理利用高階組件,會對咱們開發有很大的幫助。

高階組件的注意點

不要在 render 方法中使用 HOC

Reactdiff 算法(稱爲協調)使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹。 若是從 render 返回的組件與前一個渲染中的組件相同(===),則 React 經過將子樹與新子樹進行區分來遞歸更新子樹。 若是它們不相等,則徹底卸載前一個子樹。

一般,你不須要考慮這點。但對 HOC 來講這一點很重要,由於這表明着你不該在組件的 render 方法中對一個組件應用 HOC

render() {
  // 每次調用 render 函數都會建立一個新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!
  return <EnhancedComponent />;
}
複製代碼

這不只僅是性能問題 - 從新掛載組件會致使該組件及其全部子組件的狀態丟失。

若是在組件以外建立 HOC,這樣一來組件只會建立一次。所以,每次 render 時都會是同一個組件。通常來講,這跟你的預期表現是一致的。

const EnhancedComponent = enhance(MyComponent)

class App extends PureComponent {
  render() {
    return <EnhancedComponent />
  }
}
複製代碼

在極少數狀況下,你須要動態調用 HOC。你能夠在組件的生命週期方法或其構造函數中進行調用。

refs 不會被傳遞

雖然高階組件的約定是將全部 props 傳遞給被包裝組件,但這對於 refs 並不適用。那是由於 ref 實際上並非一個 prop ,就像 key 同樣,它是由 React 專門處理的。若是將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。

組件的補充

高階組件中轉發 refs

前面咱們提到了在高階組件中,refs 不會被傳遞,但咱們在開發中有可能會遇到須要在高階組件中轉發 refs,那麼咱們該怎麼解決呢?幸運的是,咱們可使用React.forwardRef API 來幫助解決這個問題。

讓咱們從一個輸出組件 props 到控制檯的 HOC 示例開始:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps)
      console.log('new props:', this.props)
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }

  return LogProps
}
複製代碼

logProps HOC 透穿全部 props 到其包裹的組件,因此渲染結果將是相同的。例如:咱們可使用該 HOC 記錄全部傳遞到 「fancy button」 組件的 props

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 咱們導出 LogProps,而不是 FancyButton。
// 雖然它也會渲染一個 FancyButton。
export default logProps(FancyButton)
複製代碼

到此前,這個示例正如前面所說,refs 將不會透傳下去。若是你對 HOC 添加 ref,該 ref 將引用最外層的容器組件,而不是被包裹的組件。

import FancyButton from './FancyButton'

const ref = React.createRef()

// 咱們導入的 FancyButton 組件是高階組件(HOC)LogProps。
// 儘管渲染結果將是同樣的,
// 但咱們的 ref 將指向 LogProps 而不是內部的 FancyButton 組件!
// 這意味着咱們不能調用例如 ref.current.focus() 這樣的方法
;<FancyButton label="Click Me" handleClick={handleClick} ref={ref} />
複製代碼

這個時候,咱們就能夠利用 React.forwardRef API 明確的將 refs 轉發到內部的 FancyButton 組件。React.forwardRef 接受一個渲染函數,其接收 propsref 參數並返回一個 React 節點。

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps)
      console.log('new props:', this.props)
    }

    render() {
      const { forwardedRef, ...rest } = this.props

      // 將自定義的 prop 屬性 「forwardedRef」 定義爲 ref
      return <Component ref={forwardedRef} {...rest} />
    }
  }

  // 注意 React.forwardRef 回調的第二個參數 「ref」。
  // 咱們能夠將其做爲常規 prop 屬性傳遞給 LogProps,例如 「forwardedRef」
  // 而後它就能夠被掛載到被 LogProps 包裹的子組件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />
  })
}
複製代碼

這樣咱們就能夠在高階組件中傳遞 refs 了。

Portals

某些狀況下,咱們但願渲染的內容獨立於父組件,甚至是獨立於當前掛載到的 DOM 元素中(默認都是掛載到 id 爲 rootDOM 元素上的)。

Portal 提供了一種將子節點渲染到存在於父組件之外的 DOM 節點的優秀的方案:

  • 第一個參數(child)是任何可渲染的 React 子元素,例如一個元素,字符串或 fragment
  • 第二個參數(container)是一個 DOM 元素;
ReactDOM.createPortal(child, container)
複製代碼

一般來說,當你從組件的 render 方法返回一個元素時,該元素將被掛載到 DOM 節點中離其最近的父節點:

render() {
  // React 掛載了一個新的 div,而且把子元素渲染其中
  return (
    <div> {this.props.children} </div>
  );
}
複製代碼

然而,有時候將子元素插入到 DOM 節點中的不一樣位置也是有好處的:

render() {
  // React 並*沒有*建立一個新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一個能夠在任何位置的有效 DOM 節點。
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}
複製代碼

好比說,咱們準備開發一個 TabBar 組件,它能夠將它的子組件渲染到屏幕頂部位置:

  • 第一步:修改 index.html 添加新的節點
<div id="root"></div>
<!-- 新節點 -->
<div id="TabBar"></div>
複製代碼
  • 第二步:編寫這個節點的樣式
#TabBar {
  position: fixed;
  width: 100%;
  height: 44px;
  background-color: red;
}
複製代碼
  • 第三步:編寫組件代碼
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'

class TabBar extends PureComponent {
  constructor(props) {
    super(props)
  }

  render() {
    return ReactDOM.createPortal(this.props.children, document.getElementById('TabBar'))
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div> <TabBar> <button>按鈕1</button> <button>按鈕2</button> <button>按鈕3</button> <button>按鈕4</button> </TabBar> </div>
    )
  }
}
複製代碼

Fragment

在以前的開發中,咱們老是在一個組件中返回內容時包裹一個 div 元素:

export default class App extends PureComponent {
  render() {
    return (
      <div> <h2>微信公衆號:小和山的菜鳥們</h2> <button>點贊</button> <button>關注</button> </div>
    )
  }
}
複製代碼

渲染結果

7FB293B8-6095-44E9-B80E-1A2D1B3B90AF.png

咱們會發現多了一個 div 元素:

  • 這個 div 元素對於某些場景是須要的(好比咱們就但願放到一個 div 元素中,再針對性設置樣式)
  • 某些場景下這個 div 是沒有必要的,好比當前這裏我可能但願全部的內容直接渲染到 root 中便可;

當咱們刪除這個 div 時,會報錯,若是咱們但願不渲染這個 div 應該如何操做?

  • 使用 Fragment
  • Fragment 容許你將子列表分組,而無需向 DOM 添加額外節點;
export default class App extends PureComponent {
  render() {
    return (
      <Fragment> <h2>微信公衆號:小和山的菜鳥們</h2> <button>點贊</button> <button>關注</button> </Fragment>
    )
  }
}
複製代碼

渲染效果以下:

image.png

React 還提供了 Fragment

它看起來像空標籤 <></>

export default class App extends PureComponent {
  render() {
    return (
      <> <h2>微信公衆號:小和山的菜鳥們</h2> <button>點贊</button> <button>關注</button> </>
    )
  }
}
複製代碼

**注意:**若是咱們須要在 Fragment 中添加屬性,好比 key,咱們就不能使用段語法了

嚴格模式-StrictMode

StrictMode 是一個用來突出顯示應用程序中潛在問題的工具,與 Fragment 同樣,StrictMode 不會渲染任何可見的 UI。它爲其後代元素觸發額外的檢查和警告。

**注意:**嚴格模式檢查僅在開發模式下運行;它們不會影響生產構建。

你能夠爲應用程序的任何部分啓用嚴格模式。例如:

import React from 'react'

function ExampleApplication() {
  return (
    <div> <Header /> <React.StrictMode> <div> <ComponentOne /> <ComponentTwo /> </div> </React.StrictMode> <Footer /> </div>
  )
}
複製代碼

在上述的示例中,會對 HeaderFooter 組件運行嚴格模式檢查。可是,ComponentOneComponentTwo 以及它們的全部後代元素都將進行檢查。

StrictMode 目前有助於:

  • 識別不安全的生命週期
  • 關於使用廢棄的 findDOMNode 方法的警告
  • 檢測意外的反作用
  • 檢測過期的 context API
  • 關於使用過期字符串 ref API 的警告

一、識別不安全的生命週期

某些過期的生命週期方法在異步 React 應用程序中使用是不安全的。可是,若是你的應用程序使用了第三方庫,很難確保它們不使用這些生命週期方法。

當啓用嚴格模式時,React 會列出使用了不安全生命週期方法的全部 class 組件,並打印一條包含這些組件信息的警告消息,以下所示:

image.png


二、關於使用過期字符串 ref API 的警告

之前,React 提供了兩種方法管理 refs 的方式:

  • 已過期的字符串 ref API 的形式
  • 回調函數 API 的形式。

儘管字符串 ref API 在二者中使用更方便,可是它有一些缺點,所以官方推薦採用回調的方式

React 16.3 新增了第三種選擇,它提供了使用字符串 ref 的便利性,而且不存在任何缺點:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)

    this.inputRef = React.createRef()
  }

  render() {
    return <input type="text" ref={this.inputRef} />
  }

  componentDidMount() {
    this.inputRef.current.focus()
  }
}
複製代碼

因爲對象 ref 主要是爲了替換字符串 ref 而添加的,所以嚴格模式如今會警告使用字符串 ref


三、關於使用廢棄的 findDOMNode 方法的警告

React 支持用 findDOMNode 來在給定 class 實例的狀況下在樹中搜索 DOM 節點。一般你不須要這樣作,由於你能夠將 ref 直接綁定到 DOM 節點,因爲此方法已經廢棄,這裏就不展開細講了,如感興趣,可自行學習。


四、檢測意外的反作用

  • 這個組件的 constructor 會被調用兩次;
  • 這是嚴格模式下故意進行的操做,讓你來查看在這裏寫的一些邏輯代碼被調用屢次時,是否會產生一些反作用;
  • 在生產環境中,是不會被調用兩次的;
class Home extends PureComponent {
  constructor(props) {
    super(props)

    console.log('home constructor')
  }

  UNSAFE_componentWillMount() {}

  render() {
    return <h2 ref="home">Home</h2>
  }
}
複製代碼

五、檢測過期的 context API

早期的 Context 是經過 static 屬性聲明 Context 對象屬性,經過 getChildContext 返回 Context 對象等方式來使用 Context 的;不過目前這種方法已通過時,過期的 context API 容易出錯,將在將來的主要版本中刪除。在全部 16.x 版本中它仍然有效,但在嚴格模式下,將顯示如下警告:

img.png

下節預告

本節咱們學習了 React 中高階組件以及組件補充的內容,在下一個章節咱們將開啓新的學習 React-Router ,敬請期待!

相關文章
相關標籤/搜索