"小和山的菜鳥們",爲前端開發者提供技術相關資訊以及系列基礎文章。爲更好的用戶體驗,請您移至咱們官網小和山的菜鳥們 ( xhs-rookies.com/ ) 進行學習,及時獲取最新文章。css
"Code tailor" ,若是您對咱們文章感興趣、或是想提一些建議,微信關注 「小和山的菜鳥們」 公衆號,與咱們取的聯繫,您也能夠在微信上觀看咱們的文章。每個建議或是贊同都是對咱們極大的鼓勵!html
這節咱們將介紹 React
中高階組件,以及高階組件到底有什麼用,以及對高階組件的補充。前端
本文會向你介紹如下內容:react
refs
Portals
Fragment
StrictMode
什麼是高階組件呢?相信不少同窗都據說過,也用過高階函數,它們很是類似,因此咱們能夠先來回顧一下什麼是高階函數。git
高階函數的維基百科定義:至少知足如下條件之一:github
JavaScript
中比較常見的 filter
、map
、reduce
都是高階函數。web
那麼什麼是高階組件?算法
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 屬性
假如咱們有以下案例:
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>
)
}
}
複製代碼
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
,在不遵照約定的狀況下也可能形成衝突;合理利用高階組件,會對咱們開發有很大的幫助。
React
的 diff
算法(稱爲協調)使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹。 若是從 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
。你能夠在組件的生命週期方法或其構造函數中進行調用。
雖然高階組件的約定是將全部 props
傳遞給被包裝組件,但這對於 refs
並不適用。那是由於 ref
實際上並非一個 prop
,就像 key
同樣,它是由 React
專門處理的。若是將 ref
添加到 HOC
的返回組件中,則 ref
引用指向容器組件,而不是被包裝組件。
前面咱們提到了在高階組件中,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
接受一個渲染函數,其接收 props
和 ref
參數並返回一個 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
了。
某些狀況下,咱們但願渲染的內容獨立於父組件,甚至是獨立於當前掛載到的 DOM
元素中(默認都是掛載到 id 爲 root
的 DOM
元素上的)。
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>
)
}
}
複製代碼
在以前的開發中,咱們老是在一個組件中返回內容時包裹一個 div 元素:
export default class App extends PureComponent {
render() {
return (
<div> <h2>微信公衆號:小和山的菜鳥們</h2> <button>點贊</button> <button>關注</button> </div>
)
}
}
複製代碼
渲染結果
咱們會發現多了一個 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>
)
}
}
複製代碼
渲染效果以下:
React
還提供了 Fragment
它看起來像空標籤 <></>
export default class App extends PureComponent {
render() {
return (
<> <h2>微信公衆號:小和山的菜鳥們</h2> <button>點贊</button> <button>關注</button> </>
)
}
}
複製代碼
**注意:**若是咱們須要在
Fragment
中添加屬性,好比key
,咱們就不能使用段語法了
StrictMode
是一個用來突出顯示應用程序中潛在問題的工具,與 Fragment
同樣,StrictMode
不會渲染任何可見的 UI。它爲其後代元素觸發額外的檢查和警告。
**注意:**嚴格模式檢查僅在開發模式下運行;它們不會影響生產構建。
你能夠爲應用程序的任何部分啓用嚴格模式。例如:
import React from 'react'
function ExampleApplication() {
return (
<div> <Header /> <React.StrictMode> <div> <ComponentOne /> <ComponentTwo /> </div> </React.StrictMode> <Footer /> </div>
)
}
複製代碼
在上述的示例中,不會對 Header
和 Footer
組件運行嚴格模式檢查。可是,ComponentOne
和 ComponentTwo
以及它們的全部後代元素都將進行檢查。
StrictMode
目前有助於:
findDOMNode
方法的警告context API
ref API
的警告一、識別不安全的生命週期
某些過期的生命週期方法在異步 React
應用程序中使用是不安全的。可是,若是你的應用程序使用了第三方庫,很難確保它們不使用這些生命週期方法。
當啓用嚴格模式時,React
會列出使用了不安全生命週期方法的全部 class
組件,並打印一條包含這些組件信息的警告消息,以下所示:
二、關於使用過期字符串 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
版本中它仍然有效,但在嚴格模式下,將顯示如下警告:
本節咱們學習了 React
中高階組件以及組件補充的內容,在下一個章節咱們將開啓新的學習 React-Router
,敬請期待!