轉:http://www.lxweimin.com/p/9ab8a2e0403a
在多個不同的組件中需要用到相同的功能,其解決辦法有兩種:mixin和高階組件。
1、mixin
mixin一直被廣泛用于各種面向對象語言中,其作用是為單繼承語言創造一種類似多重繼承的效果。
廣義的mixin方法,就是用賦值的方式將mixin對象中的方法都掛載到原對象上,來實現對象的混入,類似ES6中的Object.assign()的作用。原理如下:
const mixin = function(obj, mixins){
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for(let prop in mixins){ // 遍歷mixins的屬性
if(mixins.hasOwnPrototype(prop)){ // 判斷是否為mixin的自身屬性
newObj.prototype[prop] = mixins[prop]; // 賦值
}
}
return newObj;
}
實質上就是把任意多個源對象擁有的自身可枚舉屬性復制給目標對象,然后返回目標對象。那么React中的mixin是這樣的么?
在React中使用mixin
React在使用createClass構建組件時提供了mixin屬性,比如官方封裝的PureRenderMixin:
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
React.createClass({
mixins: [PureRenderMixin],
render(){
return <div>foo</div>;
}
});
可以看出,mixins是一個數組,封裝了我們需要的模塊。不同mixin的方法或許會有重合,如何處理視重合部分是普通方法還是生命周期方法而定。
在不同mixin里實現兩個名字一樣的普通方法:并不會覆蓋,且控制臺會報錯。
重合的是生命周期方法:將各個mixin的生命周期方法疊加在一起順序執行。
可以看到,使用createClass實現的mixin為組件做了兩件事:
工具方法:mixin的基本功能。用來定義共享的工具類方法,以便在各個組件中使用。
生命周期繼承,props與state合并:mixin可以合并生命周期方法。如果有多個mixin定義了componentDidMount(),React會自動將它們合并處理。同樣,mixin也可以作用在getInitialState的結果上,作state的合并,而props的合并也是這樣的。
然而,使用ES6 classes構建組件時,并不支持mixin。這就不得不說到decorator語法糖。
ES6 classes 與 decorator
decorator是運用在運行時的方法,用以對組件進行“修飾”。現在,使用decorator來實現mixin:
function handleClass(target, mixins){
if(mixins.length){
for(let i=0, l=mixins.length; i<l; i++){
// 獲取mixins的attribute對象
const decs = getOwnPropertyDescriptors(mixins[i]);
}
// 定義mixins的attribute對象
for(const key in decs){
if(!(key in target.prototype)){
defineProperty(target.prototype, key, decs[key]);
}
}
}
}
function mixin(...mixins){
if(typeof mixins[0] === 'function'){
return handleClass(mixins[0], []);
}else{
return target=>{
return handleClass(target,mixins);
}
}
}
不難看出,這個mixin與本文開頭createClass的mixin的實現是不一樣的:createClass的mixin是直接給對象的prototype屬性賦值,而這里是使用getOwnPropertyDescriptors和defineProperty進行定義。賦值與定義的區別在于賦值會覆蓋已有的定義,而后者不會。兩者在本質上都與官方的mixin方法存在區別,除了定義方法級別不能覆蓋之外,還得加上對生命周期方法的繼承以及對state的合并。
當然,decorator除作用在類上,還可以作用在方法上,但不在此處討論。
minxin的缺陷
破壞了原有組件的封裝:可能會帶來新的state和props,意味著會有些“不可見”的狀態需維護。
命名沖突:不同mixin中的命名不可知,故非常容易發生沖突,需要花一定成本解決。
增加了復雜性,難以維護。
2、高階組件
由于mixin存在上述缺陷,故React剝離了mixin。改用高階組件來取代它。
高階組件其實是一個函數,接收一個組件作為參數,返回一個新的組件作為返回值,類似于高階函數。高階組件和decorator是同一模式,因此,因此高階組件可以作為decorator來使用。高階組件基本形式:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
decorator形式:
@higherOrderComponent
WrappedComponent
高階組件有以下好處:
適用范圍廣,它不需要es6或者其它需要編譯的特性,有函數的地方,就有HOC。
Debug友好,它能夠被React組件樹顯示,所以可以很清楚地知道有多少層,每層做了什么。
高階組件實現的方法有兩種:
屬性代理:通過被包裹組件的props來進行相關操作。主要進行組件的復用。
反向繼承:繼承被包裹的組件。主要進行渲染的劫持。
1、屬性代理
屬性代理主要是四個作用:操作props、通過refs訪問組件實例、抽象state、使用其他元素包裹WrappedComponent。
(1)操作props
包括對props的讀取、增加、刪除、修改。刪除和修改要注意不能影響原組件。
示例:增加一個props
function compHOC(WrappedComponent) {
return class Comp extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
(2)通過refs訪問組件實例
可以通過ref回調函數的形式來訪問傳入組件的實例,進而調用組件相關方法或其他操作(如實例的props操作)。
//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。高階組件可以通過原組件抽象為展示型組件,分離內部狀態。
示例:抽象 <Input />的 value 和 onChange 方法。
function compHOC(WrappedComponent) {
return class Comp extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ''
}
this.onNameChange = this.onNameChange.bind(this)
}
// 將對name屬性的onChange方法提取到此處=>提取到高階組件,有效的抽象了同樣的state操作
onNameChange(event) {
this.setState({
name: event.target.value
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange
}
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
//使用方式如下
@compHOC
class Example extends React.Component {
render() {
//使用ppHOC裝飾器之后,組件的props被添加了name屬性,可以通過下面的方法,將 value 和 onChange方法 添加到input上面
return <input name="name" {...this.props.name}/>
// 變成<input name="name" value={this.state.name} onChange={this.onNameChange} />,這樣我們就得到了一個受控組件。
}
}
(4)使用其他元素包裹組件
用于加樣式、布局等。
function compHOC(WrappedComponent) {
return class Comp extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
2、反向繼承
高階組件繼承了WrappedComponent,意味著可以訪問并使用WrappedComponent的state,props,生命周期和render方法,但它不能保證完整的子組件樹被解析。如果在高階組件中定義了與WrappedComponent中同名的方法,將會發生覆蓋,就必須手動通過super進行調用。反向繼承有兩個比較大的特點:渲染劫持和控制state。
(1)渲染劫持
渲染劫持指的就是高階組件可以控制 WrappedComponent 的渲染過程,并渲染各種各樣的結果。我們可以在這個過程中在任何React元素輸出的結果中讀取、增加、修改、刪除props,或讀取或修改React元素樹,或條件顯示元素樹,又或者是用元素包裹元素樹。
大致形式如下:
function compHOC(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>
}
}
正如前面所說,反向繼承不能保證完整的子組件樹被解析,這意味著會限制渲染劫持功能。渲染劫持的經驗法則是:我們可以操控 WrappedComponent 的元素樹,并輸出正確的結果。但如果元素樹中包括了函數類型的React組件,就不能操作組件的子組件。
(2)控制state
高階組件可以讀取,編輯和刪除WrappedComponent實例的state,可以添加state。不過這個可能會破壞WrappedComponent的state,所以,要限制高階組件讀取或添加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、組件命名
用HOC包裹的組件會丟失原先的名字,影響開發和調試。可以通過在WrappedComponent的名字上加一些前綴來作為HOC的名字,以方便調試。
參考react-redux實現:
HOC.displayName = HOC(${getDisplayName(WrappedComponent)})
;
//或
class HOC extends ... {
static displayName = HOC(${getDisplayName(WrappedComponent)})
;
...
}
//getDisplayName
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
4、組件參數
有時候,在調用高階組件時,需要傳入一些參數。可以這樣實現:
function HocFactoryFactory(...params){
// 可以做一些改變params的事
return function HocFactory(WrappedCompinent){
return class Hoc extends Component {
render(){
return <WrappedComponent {...this.props} />
}
}
}
}
使用方式如下:
HocFactoryFactory(params)(WrappedComponent);
或者:
@HocFactoryFactory(params)
class WrappedComponent extends Component{
...
}
作者:南風知我意ZD
鏈接:http://www.lxweimin.com/p/9ab8a2e0403a
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。