在多個不同的元件中需要用到相同的功能,這個解決方法,通常有Mixin和高階元件。
Mixin方法例如:
//給所有元件新增一個name屬性 var defaultMixin = { getDefaultProps: function() { return { name: "Allen" } } } var Component = React.createClass({ mixins: [defaultMixin], render: function() { return <h1>Hello, {this.props.name}</h1> } })
但是由於Mixin過多會使得元件難以維護,在React ES6中Mixin不再被支援。
高階元件是一個接替Mixin實現抽象元件公共功能的好方法。高階元件其實是一個函式,接收一個元件作為引數,
返回一個包裝元件作為返回值,類似於高階函式。高階元件和裝飾器就是一個模式,因此,高階元件可以作為
裝飾器來使用。高階元件有如下好處:
1. 適用範圍廣,它不需要es6或者其它需要編譯的特性,有函式的地方,就有HOC。
2. Debug友好,它能夠被React元件樹顯示,所以可以很清楚地知道有多少層,每層做了什麼。
高階元件基本形式:const EnhancedComponent = higherOrderComponent(WrappedComponent);
詳細如下:
function hoc(ComponentClass) { return class HOC extends React.Component { componentDidMount() { console.log("hoc"); } render() { return <ComponentClass /> } } } //使用高階元件 class ComponentClass extends React.Component { render() { return <div></div> } } export default hoc(MyComponent); //作為裝飾器使用 @hoc export default class ComponentClass extends React.Component { //... }
高階元件有兩種常見的用法:
1. 屬性代理(Props Proxy): 高階元件通過ComponentClass的props來進行相關操作
2. 繼承反轉(Inheritance Inversion)): 高階元件繼承自ComponentClass
1. 屬性代理(Props Proxy)
屬性代理有如下4點常見作用:
1. 操作props
2. 通過refs訪問元件例項
3. 提取state
4. 用其他元素包裹WrappedComponent,實現佈局等目的
(1). 操作props
可以對原元件的props進行增刪改查,通常是查詢和增加,刪除和修改的話,需要考慮到不能破壞原元件。
下面是新增新的props:
function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { const newProps = { user: currentLoggedInUser } return <WrappedComponent {...this.props} {...newProps}/> } } }
(2). 通過refs訪問元件例項
可以通過ref回撥函式的形式來訪問傳入元件的例項,進而呼叫元件相關方法或其他操作。
例如:
//WrappedComponent初始渲染時候會呼叫ref回撥,傳入元件例項,在proc方法中,就可以呼叫元件方法 function refsHOC(WrappedComponent) { return class RefsHOC extends React.Component { proc(wrappedComponentInstance) { wrappedComponentInstance.method() } render() { const props = Object.assign({}, this.props, {ref: this.proc.bind(this)}) return <WrappedComponent {...props}/> } } }
(3). 提取state
你可以通過傳入 props 和回撥函式把 state 提取出來,類似於 smart component 與 dumb component。更多關於 dumb and smart component。
提取 state 的例子:提取了 input 的 value 和 onChange 方法。這個簡單的例子不是很常規,但足夠說明問題。
function ppHOC(WrappedComponent) { return class PP extends React.Component { constructor(props) { super(props) this.state = { name: '' } this.onNameChange = this.onNameChange.bind(this) } onNameChange(event) { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {...this.props} {...newProps}/> } } } //使用方式如下 @ppHOC class Example extends React.Component { render() { //使用ppHOC裝飾器之後,元件的props被新增了name屬性,可以通過下面的方法,將value和onChange新增到input上面 //input會成為受控元件 return <input name="name" {...this.props.name}/> } }
(4). 包裹WrappedComponent
為了封裝樣式、佈局等目的,可以將WrappedComponent用元件或元素包裹起來。
例如:
function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return ( <div style={{display: 'block'}}> <WrappedComponent {...this.props}/> </div> ) } } }
2. 繼承反轉(Inheritance Inversion)
HOC繼承了WrappedComponent,意味著可以訪問到WrappedComponent的state,props,生命週期和render方法。如果在HOC中定義了與WrappedComponent同名方法,將會發生覆蓋,就必須手動通過super進行呼叫。通過完全操作WrappedComponent的render方法返回的元素樹,可以真正實現渲染劫持。這種思想具有較強的入侵性。
大致形式如下:
function ppHOC(WrappedComponent) { return class ExampleEnhance extends WrappedComponent { ... componentDidMount() { super.componentDidMount(); } componentWillUnmount() { super.componentWillUnmount(); } render() { ... return super.render(); } } }
例如,實現一個顯示loading的請求。元件中存在網路請求,完成請求前顯示loading,完成後再顯示具體內容。
可以用高階元件實現如下:
function hoc(ComponentClass) { return class HOC extends ComponentClass { render() { if (this.state.success) { return super.render() } return <div>Loading...</div> } } } @hoc export default class ComponentClass extends React.Component { state = { success: false, data: null }; async componentDidMount() { const result = await fetch(...請求);
this.setState({ success: true, data: result.data }); } render() { return <div>主要內容</div> } }
(1) 渲染劫持
繼承反轉這種模式,可以劫持被繼承class的render內容,進行修改,過濾後,返回新的顯示內容。
之所以被稱為渲染劫持是因為 HOC 控制著 WrappedComponent 的渲染輸出,可以用它做各種各樣的事。
通過渲染劫持,你可以完成:
在由 render輸出的任何 React 元素中讀取、新增、編輯、刪除 props
讀取和修改由 render 輸出的 React 元素樹
有條件地渲染元素樹
把樣式包裹進元素樹,就行Props Proxy那樣包裹其他的元素
注:在 Props Proxy 中不能做到渲染劫持。
雖然通過 WrappedComponent.prototype.render 你可以訪問到 render 方法,不過還需要模擬 WrappedComponent 的例項和它的 props,還可能親自處理元件的生命週期,而不是交給 React。記住,React 在內部處理了元件例項,你處理例項的唯一方法是通過 this 或者 refs。
例如下面,過濾掉原元件中的ul元素:
function hoc(ComponentClass) { return class HOC extends ComponentClass { render() { const elementTree = super.render(); elementTree.props.children = elementTree.props.children.filter((z) => { return z.type !== "ul" && z; } const newTree = React.cloneElement(elementTree); return newTree; } } } @hoc export default class ComponentClass extends React.Component { render() { const divStyle = { width: '100px', height: '100px', backgroundColor: 'red' }; return ( <div> <p style={{color: 'brown'}}>啦啦啦</p> <ul> <li>1</li> <li>2</li> </ul> <h1>哈哈哈</h1> </div> ) } }
(2) 操作state
HOC可以讀取,編輯和刪除WrappedComponent例項的state,可以新增state。不過這個可能會破壞WrappedComponent的state,所以,要限制HOC讀取或新增state,新增的state應該放在單獨的名稱空間裡,而不是和WrappedComponent的state混在一起。
例如:通過訪問WrappedComponent的props和state來做除錯
export function IIHOCDEBUGGER(WrappedComponent) { return class II extends WrappedComponent { render() { return ( <div> <h2>HOC Debugger Component</h2> <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre> <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre> {super.render()} </div> ) } } }
(3) 條件渲染
當 this.props.loggedIn 為 true 時,這個 HOC 會完全渲染 WrappedComponent 的渲染結果。(假設 HOC 接收到了 loggedIn 這個 prop)
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { if (this.props.loggedIn) { return super.render() } else { return null } } } }
(4) 解決WrappedComponent名字丟失問題
用HOC包裹的元件會丟失原先的名字,影響開發和除錯。可以通過在WrappedComponent的名字上加一些字首來作為HOC的名字,以方便除錯。
例如:
//或 class HOC extends ... { static displayName = `HOC(${getDisplayName(WrappedComponent)})` ... } //getDisplayName function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || ‘Component’ }
(5) 實際應用
1. mobx-react就是高階元件是一個實際應用
@observer裝飾器將元件包裝為高階元件,傳入元件MyComponent後,mobx-react會對其生命週期進行各種處理,並通過呼叫forceUpdate來進行重新整理實現最小粒度的渲染。mobx提倡一份資料引用,而redux中則提倡immutable思想,每次返回新物件。
2. 實現一個從localStorage返回記錄的功能
//通過多重高階元件確定key並設定元件 const withStorage = (key) => (WrappedComponent) => { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { return <WrappedComponent data={this.state.data} {...this.props} /> } } } @withStorage('data') class MyComponent2 extends Component { render() { return <div>{this.props.data}</div> } } @withStorage('name') class MyComponent3 extends Component { render() { return <div>{this.props.data}</div> } }
3. 實現打點計時功能
(1). Props Proxy方式
//效能追蹤:渲染時間打點 export default (Target) => (props)=>{ let func1 = Target.prototype['componentWillMount'] let func2 = Target.prototype['componentDidMount']//Demo並沒有在prototype上定義該方法,func2為undefined,但是並不會有影響,這樣做只是為了事先提取出可能定義的邏輯,保持原函式的純淨 let begin, end; Target.prototype['componentWillMount'] = function (...argus){//do not use arrow funciton to bind 'this' object func1.apply(this,argus);//執行原有的邏輯 begin = Date.now(); } Target.prototype['componentDidMount'] = function (...argus){ func2.apply(this,argus);//執行原有的邏輯 end = Date.now(); console.log(Target.name+'元件渲染時間:'+(end-begin)+'毫秒') } return <Target {...props}/>//do not forget to pass props to the element of Target }
(2) Inheritance Inversion方式
// 另一種HOC的實現方式 Inheritance Inversion export default Target => class Enhancer extends Target { constructor(p){ super(p);//es6 繼承父類的this物件,並對其修改,所以this上的屬性也被繼承過來,可以訪問,如state this.end =0; this.begin=0; } componentWillMount(){ super.componentWilMount && super.componentWilMount();// 如果父類沒有定義該方法,直接呼叫會出錯 this.begin = Date.now(); } componentDidMount(){ super.componentDidMount && super.componentDidMount(); this.end=Date.now(); console.log(Target.name+'元件渲染時間'+(this.end-this.begin)+'ms') } render(){ let ele = super.render();//呼叫父類的render方法 return ele;//可以在這之前完成渲染劫持 } }
參考:https://zhuanlan.zhihu.com/p/24776678?group_id=802649040843051008
https://blog.csdn.net/cqm1994617/article/details/54800360
https://blog.csdn.net/xiangzhihong8/article/details/73459720
https://segmentfault.com/a/1190000004598113
https://blog.csdn.net/sinat_17775997/article/details/79087977
https://blog.csdn.net/neoveee/article/details/69212146