在多個不同的組件中需要用到相同的功能,這個解決方法,通常有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不再被支持。
高階組件其實是一個函數,接收一個組件作為參數,返回一個包裝組件作為返回值,類似于高階函數。高階組件和裝飾器就是一個模式,因此,高階組件可以作為裝飾器來使用。
高階組件有如下好處:
- 適用范圍廣,它不需要es6或者其它需要編譯的特性,有函數的地方,就有HOC。
- 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 {
//...
}
高階組件有兩種常見的用法:
- 屬性代理(Props Proxy): 高階組件通過ComponentClass的props來進行相關操作
- 繼承反轉(Inheritance Inversion)): 高階組件繼承自ComponentClass
1. 屬性代理(Props Proxy)
屬性代理有如下4點常見作用:
- 操作props
- 通過refs訪問組件實例
- 提取state
- 用其他元素包裹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。
提取 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。
export const hocInversion2 = config => (ComponentClass) => {
return class hoc extends ComponentClass {
render() {
const { style = {} } = config;
const elementsTree = super.render();
if (config.type === 'add-style') {
return <div style={{...style}}>
{elementsTree}
</div>;
}
return elementsTree;
}
}
}
@hocInversion2({type: 'add-style', style: { color: 'red'}})
(2) 操作state
HOC可以讀取,編輯和刪除WrappedComponent實例的state,可以添加state。不過這個可能會破壞WrappedComponent的state,所以,要限制HOC讀取或添加state,添加的state應該放在單獨的命名空間里,而不是和WrappedComponent的state混在一起。
例如:通過訪問WrappedComponent的props和state來做調試
export const 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;//可以在這之前完成渲染劫持
}
}