什么是装饰器(Decorator
)?
import React from 'react'
import {connect} from 'react-redux'
@connect()
export default class Demo extends Components{
//...
}
@connect
就是装饰器(Decorator
),如果熟悉react-redux
就一定会知道 @connect
的另一种写法。
import React from 'react'
import {connect} from 'react-redux'
class Demo extends Components{
//...
}
export default connect()(Demo)
所以显而易见装饰器的本质是函数,一个被柯里化的高阶组件(HOC
)。
function connect (){
//...
return function (WrappedComponent){
//...
return <WrappedComponent/>
}
}
什么是高阶组件(HOC
)?
概念:高阶组件是参数为组件,返回值为新组件的函数
高阶组件能做什么?
我们都知道自定义Hook可以将组件逻辑提取到可重用的函数中,从而使不同的组件更好的复用逻辑。那类组件又要如何做到逻辑复用呢?答案是高阶组件。
合并静态方法
静态方法:类内部的方法都会被子类继承,但是使用静态方法定义的不会被子类继承,也不会初始化到实例对象中
有时,需要在React组件上面定义静态方法,但当我们给一个组件添加一个HOC时,原来的组件会被一个container
的组件包裹。这意味着新的组件不会有原来组件任何静态方法。那么参考react-redux
源码。
import hoistStatics from 'hoist-non-react-statics';
//...
function wrapWithConnect(WrappedComponent) {
//...
if (forwardRef) {
const _forwarded = React.forwardRef(function forwardConnectRef(props,ref) {
return <Connect {...props} reactReduxForwardedRef={ref} />
})
const forwarded = _forwarded as ConnectedWrapperComponent
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
return hoistStatics(Connect, WrappedComponent)
}
不难发现react-redux
是使用hoist-non-react-statics
合并了静态方法。那我们来看看hoist-non-react-statics
做了什么。
// ...
function getStatics(component) {
// React v16.11 and below
if (reactIs.isMemo(component)) {
return MEMO_STATICS;
} // React v16.12 and above
// memo 组件与 forwardRef z组件静态属性及方法 || react class 组件静态属性及方法
return TYPE_STATICS[component['$$typeof']] || REACT_STATICS;
}
// ...
export default function hoistNonReactStatics(targetComponent, sourceComponent, excludelist) {
if (typeof sourceComponent !== 'string') { // don't hoist over string (html) components
if (objectPrototype) {
const inheritedComponent = getPrototypeOf(sourceComponent);
if (inheritedComponent && inheritedComponent !== objectPrototype) {
hoistNonReactStatics(targetComponent, inheritedComponent, excludelist);
}
}
// 获取所有原型属性名 Object.getOwnPropertyNames
let keys = getOwnPropertyNames(sourceComponent);
// 获取对象自身的所有 Symbol 属性的数组 Object.getOwnPropertySymbols
if (getOwnPropertySymbols) {
keys = keys.concat(getOwnPropertySymbols(sourceComponent));
}
// 获取目标组件与来源组件的静态方法(注意:这里返回的静态方法指的是定义好的不参与合并的方法及属性)
const targetStatics = getStatics(targetComponent);
const sourceStatics = getStatics(sourceComponent);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
// 排除过滤列表、 来源组件及目标组件中不需要参与合并的方法及属性
if (!KNOWN_STATICS[key] &&
!(excludelist && excludelist[key]) &&
!(sourceStatics && sourceStatics[key]) &&
!(targetStatics && targetStatics[key])
) {
// 获取对象自有属性对应的属性描述符 Object.getOwnPropertyDescriptor
const descriptor = getOwnPropertyDescriptor(sourceComponent, key);
try { // Avoid failures from read-only properties
// 合并方法及属性 Object.defineProperty
defineProperty(targetComponent, key, descriptor);
} catch (e) {}
}
}
}
return targetComponent;
};
业务案例
以商城业务出发,必然有各种不同的商品列表页,但有着相同的加购逻辑。在函数式组件中,我们可以用自定义Hook实现逻辑的抽离和复用,在类组件中就需要用到高阶组件。
// 高阶组件
import React from 'react'
function addCartHOC (WrappedComponent){
return class AddCart extends Components{
constructor(props) {
super(props);
}
/**
* 加购方法
*/
handleAddCart = ()=>{
...
}
render(){
return <WrappedComponent {...this.props} onAddCart={this.handleAddCart}/>
}
}
}
那么我们就可以在页面组件中这样使用
// 页面组件
import React from 'react'
class Page extends Components {
constructor(props) {
super(props);
}
// 静态方法
static staticMethod (){
}
render(){
const { onAddCart } = this.props
return (
<div>
<button onClick={onAddCart}>加购</button>
</div>
)
}
}
export default addCartHOC(Page)
而我们也可以进一步改进我们的高阶组件,实现装饰器并合并静态方法。
// 装饰器
import React from 'react';
import hoistStatics from 'hoist-non-react-statics';
function addCart() {
return function addCartHOC(WrappedComponent) {
return class AddCart extends Components {
constructor(props) {
super(props);
}
/**
* 加购方法
*/
handleAddCart = () => {
//...
}
render() {
return <WrappedComponent {...this.props} onAddCart={this.handleAddCart}/>
}
}
// 合并静态方法
return hoistStatics(AddCart,WrappedComponent)
}
}
那么我们在页面组件上就可以使用更简洁的使用高阶函数
// 页面组件
import React from 'react'
@addCart
export default class Page extends Components {
constructor(props) {
super(props);
}
render(){
const { onAddCart } = this.props
return (
<div>
<button onClick={onAddCart}>加购</button>
</div>
)
}
}
Taro
中使用
当我们使用上面开发的装饰器addCart
,复用至页面组件时会发现小程序的生命周期,例如:onLoad
、onShow
(即Taro
页面组件的生命周期 componentDidShow
)等无法触发。
那是因为Taro
处理生命周期的逻辑,如 onShow
的流程是由小程序触发 onShow
,找到页面组件实例,通过ref
调页面组件实例用上面的 componentDidShow
方法。那么结合Taro
与 react-redux
源码来看具体实现。
-
在
Taro
中通过props
传入reactReduxForwardedRef
,// packages/taro-runtime/src/dsl/react.ts import { isFunction, ensure, EMPTY_OBJ } from '@tarojs/shared' //...... const h = R.createElement // 这里对于 react 就是 react.createElement //....... // 是否为类组件由于 react-redux 是由函数式组件作为高阶函数返回的 const refs = isReactComponent ? { ref: inject } : { forwardedRef: inject, // react-redux 高阶组件的 ref 参数 reactReduxForwardedRef: inject } // ....... // 通过 props 传入组件 h(component, { ...this.props, ...refs })
-
在
react-redux
库connect
方法中function connect(mapStateToProps, mapDispatchToProps, mergeProps, option) { // ...... const wrapWithConnect = (WrappedComponent) => { // ...... function ConnectFunction() { // ...... const renderedWrappedComponent = useMemo(() => { return ( // @ts-ignore <WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} /> ) }, [ reactReduxForwardedRef, WrappedComponent, actualChildProps ] // If React sees the exact same element reference as last time, it bails out of re-rendering // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate. const renderedChild = useMemo(() => { if (shouldHandleStateChanges) { return ( <ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider> ) } return renderedWrappedComponent }, [ ContextToUse, renderedWrappedComponent, overriddenContextValue ]) return renderedChild } //....... const _Connect = React.memo(ConnectFunction) //....... const Connect = _Connect if (forwardRef) { const _forwarded = React.forwardRef(function forwardConnectRef( props, ref ) { // @ts-ignore return <Connect {...props} reactReduxForwardedRef={ref} /> }) const forwarded = _forwarded as ConnectedWrapperComponent forwarded.displayName = displayName forwarded.WrappedComponent = WrappedComponent return hoistStatics(forwarded, WrappedComponent) } return hoistStatics(Connect, WrappedComponent) } // ...... return wrapWithConnect }
其中
<WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
与Taro
中通过props
传入的reactReduxForwardedRef
相对应实现了小程序生命周期事件的转发与触发。
装饰器实现修改
那么我们修改装饰器代码
// 装饰器
import React, { Component } from 'react'
import hoistStatics from 'hoist-non-react-statics'
export function addCart() {
return function addCartHOC(WrappedComponent) {
class AddCart extends Component {
constructor(props) {
console.log('constructor', { props })
super(props)
}
/**
* 加购方法
*/
handleAddCart = () => {
//...
}
render() {
const { wrappedComponentRef } = this.props
// 这里考虑到页面可能使用 @connect 装饰器, 需要兼容故增加 reactReduxForwardedRef 传参
return <WrappedComponent {...this.props} ref={wrappedComponentRef} reactReduxForwardedRef={wrappedComponentRef} onAddCart={this.handleAddCart} />
}
}
const HoistComponent = hoistStatics(AddCart, WrappedComponent)
return React.forwardRef((props, ref) => <HoistComponent {...props} wrappedComponentRef={ref} />)
}
}
对应的我们页面组件则不需要改变
// 页面组件
import React from 'react'
@addCart
export default class Page extends Components {
constructor(props) {
super(props);
}
render(){
const { onAddCart } = this.props
return (
<div>
<button onClick={onAddCart}>加购</button>
</div>
)
}
}
最后
装饰器是可以传递参数的与react-redux
的connect
方法类似,只是太简单了没什么好写的。熟悉vue
的可以把高阶函数当成vue
里的mixin