概況:
什麼是高階元件?
高階部件是一種用於複用元件邏輯的高階技術,它並不是 React API的一部分,而是從React 演化而來的一種模式。 具體地說,高階元件就是一個接收一個元件並返回另外一個新元件的函式!
這是官方文件說的,我沒有截全,因為後面的解釋會造成誤解,但簡單講高階元件(函式)就好比一個加工廠,同樣的,螢幕、cpu、揚聲器、電池,小米手機工廠組裝完就是小米手機,魅族手機組裝完就是魅族手機,基本材料都是相同的,不同工廠(高階元件)有不同的實現及產出,當然這個工廠(高階元件)也可能是針對某個基本材料的處理。 總之產出的結果擁有了輸入元件不具備的功能,輸入的元件可以是一個元件的例項,也可以是一個元件類,還可以是一個無狀態元件的函式。
解決什麼問題?
隨著專案越來越複雜,開發過程中,多個元件需要某個功能,而且這個功能和頁面並沒有關係,所以也不能簡單的抽取成一個新的元件,但是如果讓同樣的邏輯在各個元件裡各自實現,無疑會導致重複的程式碼。比如頁面有三種彈窗一個有title,一個沒有,一個又有右上角關閉按鈕,除此之外別無它樣,你總不能整好幾個彈窗元件吧,這裡除了tilte,關閉按鈕其他的就可以做為上面說的基本材料。
高階元件總共分為兩大類
- 代理方式
- 操縱prop
- 訪問ref(不推薦)
- 抽取狀態
- 包裝元件
- 繼承方式
- 操縱生命週期
- 操縱prop
代理方式之 操縱prop
刪除prop
1 2 3 4 5 6 7 8 9 10 |
import React from 'react' function HocRemoveProp(WrappedComponent) { return class WrappingComPonent extends React.Component { render() { const { user, ...otherProps } = this.props; return WrappedComponent {...otherProps} /> } } } export default HocRemoveProp; |
增加prop
接下來我把簡化了寫法,把匿名函式去掉,同時換成箭頭函式
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; const HocAddProp = (WrappedComponent,uid) => class extends React.Component { render() { const newProps = { uid, }; return <WrappedComponent {...this.props} {...newProps} /> } } export default HocAddProp; |
上面HocRemoveProp高階元件中,所做的事情和輸入元件WrappedComponent功能一樣,只是忽略了名為user的prop。也就是說,如果WrappedComponent能處理名為user的prop,這個高階元件返回的元件則完全無視這個prop。
1 |
const { user, ...otherProps } = this.props; |
這是一個利用es6語法技巧,經過上面的語句,otherProps裡面就有this.props中所有的欄位除了user. 假如我們現在不希望某個元件接收user的prop,那麼我們就不要直接使用這個元件,而是把這個元件作為引數傳遞給HocRemoveProp,然後我們把這個函式的返回結果當作元件來使用 兩個高階元件的使用方法:
1 2 |
const newComponent = HocRemoveProp(SampleComponent); const newComponent = HocAddProp(SampleComponent,'1111111'); |
也可以利用decorator語法糖這樣使用
1 2 3 4 5 6 |
import React, { Component } from 'React'; @HocRemoveProp class SampleComponent extends Component { render() {} } export default SampleComponent; |
代理方式之 抽取狀態
將所有的狀態的管理交給外面的容器元件,這個模式就是 抽取狀態 外面的容器就是這個高階元件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const HocContainer = (WrappedComponent) => class extends React.Component { constructor(props) { super(props) this.state = { name: '' } } onNameChange = (event) => { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {...this.props} {...newProps} /> } } |
1 2 3 4 5 6 |
@HocContainer class SampleComponent extends React.Component { render() { return <input name="name" {...this.props.name}/> } } |
這樣當我們在使用這個已經被包裹的input元件(SampleComponent)時候 它的值就被放在了HocContainer高階元件中,當很多這樣的input元件都用這個HocContainer高階元件時,那麼它們的值都將儲存在這個HocContainer高階元件中
代理方式之 包裝元件
1 2 3 4 5 6 7 8 9 10 |
const HocStyleComponent = (WrappedComponent, style) => class extends React.Component { render() { return ( <div style={style}> <WrappedComponent {...this.props} {...newProps} /> </div> ) } } |
這樣使用
1 2 3 |
import HocStyleComponent from './HocStyleComponent'; const colorSytle ={color:'#ff5555'} const newComponent = HocStyleComponent(SampleComponent, colorSytle); |
-代理方式的生命週期的過程類似於堆疊呼叫:
1 |
didmount 一> HOC didmount 一>(HOCs didmount) 一>(HOCs will unmount) 一>HOC will unmount一>unmount |
在說繼承方式之前先看一個例子
1 2 3 4 5 6 |
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { return super.render(); } } |
這個例子很簡單,相當於把WrappedComponent元件的render方法,
通過super.render()方法吐到了MyContainer 中,可以順序呼叫。
繼承方式的生命週期的過程類似佇列呼叫:
1 |
didmount 一> HOC didmount 一>(HOCs didmount) 一>will unmount一>HOC will unmount一> (HOCs will unmount) |
1.代理方式下WrappedComponent會經歷一個完整的生命週期,產生的新元件和引數元件是兩個不同的元件,一次渲染,兩個元件都會經歷各自的生命週期,
2.而在繼承方式下,產生的新元件和引數元件合二為一,super.render只是生命週期中的函式,變成一個生命週期。
來看下面的例子你就會明白了。
繼承方式之 操縱生命週期(渲染劫持)
首先建立一個高階,在建立一個使用高階元件的元件,也就是是輸入元件,最後我在改變這個輸入元件props
1 2 3 4 5 6 7 8 9 10 |
import * as React from 'react'; const HocComponent = (WrappedComponent) => class MyContainer extends WrappedComponent { render() { if (this.props.time && this.state.success) { return super.render() } return <div>倒數計時完成了...</div> } } |
這個高階元件會直接讀取輸入元件中的props,state,然後控制了輸入元件的render展示 只有在props.time和state.success同時為真的時候才會展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import * as React from 'react'; import HocComponent from './HocComponent' @HocComponent class DemoComponent extends React.Component { constructor(props) { super(props); this.state = { success: true, }; } render() { return <div>我是一個元件</div> } } export default DemoComponent; |
然後呼叫,遞減time數值直到變為0最後頁面的效果就是,當然他不是迴圈的。先展示”我是一個元件“,我設定了兩秒,之後展示”倒數計時完成“
由此可以看出高階元件也可以控制state
但是最好要限制這樣做,可能會讓WrappedComponent元件內部狀態變得一團糟。建議可以通過重新命名state,以防止混淆。
繼承方式之 操縱prop
1 2 3 4 5 6 7 8 9 10 11 12 |
const HOCPropsComponent = (WrappedComponent) => class extends WrappedComponent { render() { const elementsTree = super.render(); let newProps = { color: (elementsTree && elementsTree.type === 'div') ? '#fff' : '#ff5555' }; const props = Object.assign({}, elementsTree.props, newProps) const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children) return newElementsTree } } |
這樣就傳入了新的props
1 2 |
React.cloneElement( element, [props], [...children]) 引數:TYPE(ReactElement),[PROPS(object)],[CHILDREN(ReactElement) |
克隆並返回一個新的 ReactElement ,新返回的元素會保留有舊元素的 props、ref、key,也會整合新的 props。
還有一個方式,在傳遞props上有著強於高階元件的優勢不用關心命名,
1 2 3 4 5 6 |
class addProps extends React.Component { render() { const newProps = 'uid' return this.props.children(newProps) } } |
使用方式
1 2 3 4 5 |
<addProps> { (argument) => <div>{argument}</div> } </addProps> |
感覺很方便,但是每次渲染都會重新定義一個新的函式,如果不想的話就不要定義匿名函式,
1 2 3 |
showUid(argument) { return <div>{argument}</div> } |
彩蛋recompose庫
recompose是一個很流行的庫,它提供了很多很有用的高階元件(小工具),而且也可以優雅的組合它們。
Step 1 扁平props.
我們有這樣一個元件
1 2 3 4 5 6 |
const Profile = ({ user }) => ( <div> <div>Username: {user.username}</div> <div>Age: {user.age}</div> </div> ) |
如果想要改變元件介面來接收單個 prop 而不是整個使用者物件,可以用 recompose 提供的高 階元件 flattenProp 來實現。
1 2 3 4 5 6 |
const Profile = ({ username,age }) => ( <div> <div>Username: {username},/div> <div>Age: {age}</div> </div> ) |
const ProfileWithFlattenUser = flattenProp(‘user’)(Profile); 現在我們希望同時使用多個高階元件:一個用於扁平化處理使用者 prop,另一個用於重新命名用 戶物件的單個 prop。 此時 recompose 庫提供的 compose 函式就派上用場了。
1 2 3 4 |
const enhance = compose( flattenProp('user'), renameProp('username', 'name') ) |
然後按照以下方式將它應用於原有元件:
1 |
const EnhancedProfile = enhance(Profile) |
還可以將 compose 函式用 在我們自己的高階元件上,甚至結合使用都可以:
1 2 3 4 5 |
const enhance = compose( flattenProp('user'), renameProp('username', 'name'), withInnerWidth ) |
Step 2 提取輸入表單的State
我們將從Recompose庫中使用withStateHandlers高階元件。 它將允許我們將元件狀態與元件本身隔離開來。 我們將使用它為電子郵件,密碼和確認密碼欄位新增表單狀態,以及上述欄位的事件處理程式。
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 |
import { withStateHandlers, compose } from "recompose"; const initialState = { email: { value: "" }, password: { value: "" }, confirmPassword: { value: "" } }; const onChangeEmail = props => event => ({ email: { value: event.target.value, isDirty: true } }); const onChangePassword = props => event => ({ password: { value: event.target.value, isDirty: true } }); const onChangeConfirmPassword = props => event => ({ confirmPassword: { value: event.target.value, isDirty: true } }); const withTextFieldState = withStateHandlers(initialState, { onChangeEmail, onChangePassword, onChangeConfirmPassword }); export default withTextFieldState; |
withStateHandlers它接受初始狀態和包含狀態處理程式的物件。呼叫時,每個狀態處理程式將返回新的狀態。
好了,很辛苦也很感謝你能看到這裡,關於recompose介紹就到這裡了,喜歡的朋友可以深入研究recompose其它的方法和原始碼。
不準確的地方歡迎拍磚