前端 | React深入:从Mixin到HOC再到Hook

开发 前端
本文介绍了React采用的三种实现状态逻辑复用的技术,并分析了他们的实现原理、使用方法、实际应用以及如何选择使用他们。

 导读

前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。

本文介绍了React采用的三种实现状态逻辑复用的技术,并分析了他们的实现原理、使用方法、实际应用以及如何选择使用他们。

本文略长,下面是本文的思维导图,您可以从头开始阅读,也可以选择感兴趣的部分阅读:

Mixin设计模式

Mixin(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上面去,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题。

很多开源库提供了Mixin的实现,如Underscore的_.extend方法、JQuery的extend方法。

使用_.extend方法实现代码复用: 

  1. var LogMixin = {  
  2.   actionLog: function() {  
  3.     console.log('action...');  
  4.   },  
  5.   requestLog: function() {  
  6.     console.log('request...');  
  7.   },  
  8. };  
  9. function User() {  /*..*/  }  
  10. function Goods() {  /*..*/ }  
  11. _.extend(User.prototype, LogMixin);  
  12. _.extend(Goods.prototype, LogMixin);  
  13. var user = new User();  
  14. var good = new Goods();  
  15. user.actionLog();  
  16. good.requestLog(); 

我们可以尝试手动写一个简单的Mixin方法: 

  1. function setMixin(target, mixin) {  
  2.   if (arguments[2]) {  
  3.     for (var i = 2len = arguments.length; i < len; i++) {  
  4.       target.prototype[arguments[i]] = mixin.prototype[arguments[i]];  
  5.     }  
  6.   }  
  7.   else {  
  8.     for (var methodName in mixin.prototype) {  
  9.       if (!Object.hasOwnProperty(target.prototype, methodName)) {  
  10.         target.prototype[methodName] = mixin.prototype[methodName];  
  11.       }  
  12.     }  
  13.   }  
  14.  
  15. setMixin(User,LogMixin,'actionLog');  
  16. setMixin(Goods,LogMixin,'requestLog'); 

您可以使用setMixin方法将任意对象的任意方法扩展到目标对象上。

React中应用Mixin

React也提供了Mixin的实现,如果完全不同的组件有相似的功能,我们可以引入来实现代码复用,当然只有在使用createClass来创建React组件时才可以使用,因为在React组件的es6写法中它已经被废弃掉了。

例如下面的例子,很多组件或页面都需要记录用户行为,性能指标等。如果我们在每个组件都引入写日志的逻辑,会产生大量重复代码,通过Mixin我们可以解决这一问题: 

  1. var LogMixin = {  
  2.   log: function() {  
  3.     console.log('log');  
  4.   },  
  5.   componentDidMount: function() {  
  6.     console.log('in');  
  7.   },  
  8.   componentWillUnmount: function() {  
  9.     console.log('out');  
  10.   }  
  11. }; 
  12. var User = React.createClass({  
  13.   mixins: [LogMixin],  
  14.   render: function() {  
  15.     return (<div>...</div> 
  16.   }  
  17. });  
  18. var Goods = React.createClass({  
  19.   mixins: [LogMixin],  
  20.   render: function() {  
  21.     return (<div>...</div> 
  22.   }  
  23. }); 

Mixin带来的危害

React官方文档在Mixins Considered Harmful一文中提到了Mixin带来了危害:

  •  Mixin 可能会相互依赖,相互耦合,不利于代码维护
  •  不同的 Mixin 中的方法可能会相互冲突
  •  Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

React现在已经不再推荐使用Mixin来解决代码复用问题,因为Mixin带来的危害比他产生的价值还要巨大,并且React全面推荐使用高阶组件来替代它。另外,高阶组件还能实现更多其他更强大的功能,在学习高阶组件之前,我们先来看一个设计模式。

装饰模式

[[262121]]

装饰者(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责。与继承相比,装饰者是一种更轻便灵活的做法。

高阶组件(HOC)

[[262122]]

高阶组件可以看作React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。 

  1. function visible(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       const { visible, ...props } = this.props;  
  5.       if (visible === false) return null;  
  6.       return <WrappedComponent {...props} /> 
  7.     }  
  8.   }  

上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染Visible。

下面我们从以下几方面来具体探索HOC。

HOC的实现方式

属性代理

函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,这样我们就可以代理所有传入的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。 

  1. function proxyHOC(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       return <WrappedComponent {...this.props} /> 
  5.     }  
  6.   }  

对比原生组件增强的项:

  •  可操作所有传入的props
  •  可操作组件的生命周期
  •  可操作组件的static方法
  •  获取refs

反向继承

返回一个组件,继承原组件,在render中调用原组件的render。由于继承了原组件,能通过this访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。 

  1. function inheritHOC(WrappedComponent) {  
  2.   return class extends WrappedComponent {  
  3.     render() {  
  4.       return super.render();  
  5.     }  
  6.   }  

对比原生组件增强的项:

  •  可操作所有传入的props
  •  可操作组件的生命周期
  •  可操作组件的static方法
  •  获取refs
  •  可操作state
  •  可以渲染劫持

HOC可以实现什么功能

组合渲染

可使用任何其他组件和原组件进行组合渲染,达到样式、布局复用等效果。

通过属性代理实现:

  1. function stylHOC(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       return (<div>  
  5.         <div className="title">{this.props.title}</div>  
  6.         <WrappedComponent {...this.props} />  
  7.       </div>);  
  8.     }  
  9.   }  

通过反向继承实现:

  1. function styleHOC(WrappedComponent) {  
  2.   return class extends WrappedComponent {  
  3.     render() {  
  4.       return <div>  
  5.         <div className="title">{this.props.title}</div>  
  6.         {super.render()}  
  7.       </div>  
  8.     }  
  9.   }  

条件渲染

根据特定的属性决定原组件是否渲染。

通过属性代理实现:

  1. function visibleHOC(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       if (this.props.visible === false) return null;  
  5.       return <WrappedComponent {...props} /> 
  6.     }  
  7.   }  

通过反向继承实现:

  1. function visibleHOC(WrappedComponent) {  
  2.   return class extends WrappedComponent {  
  3.     render() {  
  4.       if (this.props.visible === false) {  
  5.         return null  
  6.       } else {  
  7.         return super.render()  
  8.       }  
  9.     }  
  10.   }  

操作props

可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。

通过属性代理实现:

  1. function proxyHOC(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       const newProps = {  
  5.         ...this.props,  
  6.         user: 'ConardLi'  
  7.       }  
  8.       return <WrappedComponent {...newProps} /> 
  9.     }  
  10.   }  

获取refs

高阶组件中可获取原组件的ref,通过ref获取组件实力,如下面的代码,当程序初始化完成后调用原组件的log方法。(不知道refs怎么用,请👇Refs & DOM)

通过属性代理实现:

  1. function refHOC(WrappedComponent) {  
  2.   return class extends Component {  
  3.     componentDidMount() {  
  4.       this.wapperRef.log()  
  5.     }  
  6.     render() {  
  7.       return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} /> 
  8.     }  
  9.   }  

这里注意:调用高阶组件的时候并不能获取到原组件的真实ref,需要手动进行传递,具体请看传递refs

状态管理

将原组件的状态提取到HOC中进行管理,如下面的代码,我们将Input的value提取到HOC中进行管理,使它变成受控组件,同时不影响它使用onChange方法进行一些其他操作。基于这种方式,我们可以实现一个简单的双向绑定,具体请看双向绑定。

通过属性代理实现:

  1. function proxyHoc(WrappedComponent) {  
  2.   return class extends Component {  
  3.     constructor(props) {  
  4.       super(props);  
  5.       this.state = { value: '' };  
  6.     }  
  7.     onChange = (event) => {  
  8.       const { onChange } = this.props;  
  9.       this.setState({  
  10.         value: event.target.value,  
  11.       }, () => {  
  12.         if(typeof onChange ==='function'){  
  13.           onChange(event);  
  14.         }  
  15.       })  
  16.     }  
  17.     render() {  
  18.       const newProps = {  
  19.         value: this.state.value,  
  20.         onChange: this.onChange,  
  21.       }  
  22.       return <WrappedComponent {...this.props} {...newProps} /> 
  23.     }  
  24.   }  
  25.  
  26. class HOC extends Component {  
  27.   render() {  
  28.     return <input {...this.props}></input>  
  29.   }  
  30.  
  31. export default proxyHoc(HOC); 

操作state

上面的例子通过属性代理利用HOC的state对原组件进行了一定的增强,但并不能直接控制原组件的state,而通过反向继承,我们可以直接操作原组件的state。但是并不推荐直接修改或添加原组件的state,因为这样有可能和组件内部的操作构成冲突。

通过反向继承实现:

  1. function debugHOC(WrappedComponent) {  
  2.   return class extends WrappedComponent {  
  3.     render() {  
  4.       console.log('props', this.props);  
  5.       console.log('state', this.state);  
  6.       return (  
  7.         <div className="debuging">  
  8.           {super.render()}  
  9.         </div>  
  10.       )  
  11.     }  
  12.   }  

上面的HOC在render中将props和state打印出来,可以用作调试阶段,当然你可以在里面写更多的调试代码。想象一下,只需要在我们想要调试的组件上加上@debug就可以对该组件进行调试,而不需要在每次调试的时候写很多冗余代码。(如果你还不知道怎么使用HOC,请👇如何使用HOC)

渲染劫持

高阶组件可以在render函数中做非常多的操作,从而控制原组件的渲染输出。只要改变了原组件的渲染,我们都将它称之为一种渲染劫持。

实际上,上面的组合渲染和条件渲染都是渲染劫持的一种,通过反向继承,不仅可以实现以上两点,还可直接增强由原组件render函数产生的React元素。

通过反向继承实现:

  1. function hijackHOC(WrappedComponent) {  
  2.   return class extends WrappedComponent {  
  3.     render() {  
  4.       const tree = super.render();  
  5.       let newProps = {};  
  6.       if (tree && tree.type === 'input') {  
  7.         newProps = { value: '渲染被劫持了' };  
  8.       }  
  9.       const props = Object.assign({}, tree.props, newProps);  
  10.       const newTree = React.cloneElement(tree, props, tree.props.children);  
  11.       return newTree;  
  12.     }  
  13.   }  

注意上面的说明我用的是增强而不是更改。render函数内实际上是调用React.creatElement产生的React元素:

虽然我们能拿到它,但是我们不能直接修改它里面的属性,我们通过getOwnPropertyDescriptors函数来打印下它的配置项:

可以发现,所有的writable属性均被配置为了false,即所有属性是不可变的。(对这些配置项有疑问,请👇defineProperty)

不能直接修改,我们可以借助cloneElement方法来在原组件的基础上增强一个新组件:

React.cloneElement()克隆并返回一个新的React元素,使用 element 作为起点。生成的元素将会拥有原始元素props与新props的浅合并。新的子级会替换现有的子级。来自原始元素的 key 和 ref 将会保留。

React.cloneElement() 几乎相当于: 

  1. <element.type {...element.props} {...props}>{children}</element.type> 

如何使用HOC

上面的示例代码都写的是如何声明一个HOC,HOC实际上是一个函数,所以我们将要增强的组件作为参数调用HOC函数,得到增强后的组件。 

  1. class myComponent extends Component {  
  2.   render() {  
  3.     return (<span>原组件</span> 
  4.   }  
  5.  
  6. export default inheritHOC(myComponent); 

compose

在实际应用中,一个组件可能被多个HOC增强,我们使用的是被所有的HOC增强后的组件,借用一张装饰模式的图来说明,可能更容易理解:

假设现在我们有logger,visible,style等多个HOC,现在要同时增强一个Input组件: 

  1. logger(visible(style(Input))) 

这种代码非常的难以阅读,我们可以手动封装一个简单的函数组合工具,将写法改写如下: 

  1. const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));  
  2. compose(logger,visible,style)(Input); 

compose函数返回一个所有函数组合后的函数,compose(f, g, h) 和 (...args) => f(g(h(...args)))是一样的。

很多第三方库都提供了类似compose的函数,例如lodash.flowRight,Redux提供的combineReducers函数等。

Decorators

我们还可以借助ES7为我们提供的Decorators来让我们的写法变的更加优雅: 

  1. @logger  
  2. @visible  
  3. @style  
  4. class Input extends Component {  
  5.   // ...  

Decorators是ES7的一个提案,还没有被标准化,但目前Babel转码器已经支持,我们需要提前配置babel-plugin-transform-decorators-legacy: 

  1. "plugins": ["transform-decorators-legacy"] 

还可以结合上面的compose函数使用: 

  1. const hoc = compose(logger, visible, style);  
  2. @hoc  
  3. class Input extends Component {  
  4.   // ...  

HOC的实际应用

下面是一些我在生产环境中实际对HOC的实际应用场景,由于文章篇幅原因,代码经过很多简化,如有问题欢迎在评论区指出:

日志打点

实际上这属于一类最常见的应用,多个组件拥有类似的逻辑,我们要对重复的逻辑进行复用,官方文档中CommentList的示例也是解决了代码复用问题,写的很详细,有兴趣可以👇使用高阶组件(HOC)解决横切关注点。

某些页面需要记录用户行为,性能指标等等,通过高阶组件做这些事情可以省去很多重复代码。 

  1. function logHoc(WrappedComponent) {  
  2.   return class extends Component {  
  3.     componentWillMount() {  
  4.       this.start = Date.now();  
  5.     }  
  6.     componentDidMount() {  
  7.       this.end = Date.now();  
  8.       console.log(`${WrappedComponent.dispalyName} 渲染时间:${this.end - this.start} ms`);  
  9.       console.log(`${user}进入${WrappedComponent.dispalyName}`);  
  10.     }  
  11.     componentWillUnmount() {  
  12.       console.log(`${user}退出${WrappedComponent.dispalyName}`);  
  13.     }  
  14.     render() {  
  15.       return <WrappedComponent {...this.props} />  
  16.     }  
  17.   }  

可用、权限控制 

  1. function auth(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       const { visible, auth, display = null, ...props } = this.props;  
  5.       if (visible === false || (auth && authList.indexOf(auth) === -1)) {  
  6.         return display  
  7.       }  
  8.       return <WrappedComponent {...props} /> 
  9.     }  
  10.   }  

authList是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不列表中,或者设置的visible是false,我们将其显示为传入的组件样式,或者null。我们可以将任何需要进行权限校验的组件应用HOC: 

  1. @auth  
  2. class Input extends Component {  ...  }  
  3. @auth  
  4. class Button extends Component {  ...  }  
  5. <Button auth="user/addUser">添加用户</Button>  
  6. <Input auth="user/search" visible={false} >添加用户</Input> 

双向绑定

在vue中,绑定一个变量后可实现双向数据绑定,即表单中的值改变后绑定的变量也会自动改变。而React中没有做这样的处理,在默认情况下,表单元素都是非受控组件。给表单元素绑定一个状态后,往往需要手动书写onChange方法来将其改写为受控组件,在表单元素非常多的情况下这些重复操作是非常痛苦的。

我们可以借助高阶组件来实现一个简单的双向绑定,代码略长,可以结合下面的思维导图进行理解。

首先我们自定义一个Form组件,该组件用于包裹所有需要包裹的表单组件,通过contex向子组件暴露两个属性:

  •  model:当前Form管控的所有数据,由表单name和value组成,如{name:'ConardLi',pwd:'123'}。model可由外部传入,也可自行管控。
  •  changeModel:改变model中某个name的值。 
  1. class Form extends Component {  
  2.   static childContextTypes = {  
  3.     model: PropTypes.object,  
  4.     changeModel: PropTypes.func  
  5.   }  
  6.   constructor(props, context) {  
  7.     super(props, context);  
  8.     this.state = {  
  9.       model: props.model || {}  
  10.     };  
  11.   }  
  12.   componentWillReceiveProps(nextProps) {  
  13.     if (nextProps.model) {  
  14.       this.setState({  
  15.         model: nextProps.model  
  16.       })  
  17.     }  
  18.   }  
  19.   changeModel = (name, value) => {  
  20.     this.setState({  
  21.       model: { ...this.state.model, [name]: value }  
  22.     })  
  23.   }  
  24.   getChildContext() {  
  25.     return {  
  26.       changeModel: this.changeModel,  
  27.       model: this.props.model || this.state.model  
  28.     };  
  29.   }  
  30.   onSubmit = () => {  
  31.     console.log(this.state.model);  
  32.   }  
  33.   render() {  
  34.     return <div>  
  35.       {this.props.children}  
  36.       <button onClick={this.onSubmit}>提交</button>  
  37.     </div>  
  38.   }  

下面定义用于双向绑定的HOC,其代理了表单的onChange属性和value属性:

  •  发生onChange事件时调用上层Form的changeModel方法来改变context中的model。
  •  在渲染时将value改为从context中取出的值。 
  1. function proxyHoc(WrappedComponent) {  
  2.   return class extends Component {  
  3.     static contextTypes = {  
  4.       model: PropTypes.object,  
  5.       changeModel: PropTypes.func  
  6.     }  
  7.     onChange = (event) => {  
  8.       const { changeModel } = this.context;  
  9.       const { onChange } = this.props;  
  10.       const { v_model } = this.props;  
  11.       changeModel(v_model, event.target.value);  
  12.       if(typeof onChange === 'function'){onChange(event);}  
  13.     }  
  14.     render() {  
  15.       const { model } = this.context;  
  16.       const { v_model } = this.props;  
  17.       return <WrappedComponent  
  18.         {...this.props}  
  19.         value={model[v_model]}  
  20.         onChange={this.onChange}  
  21.       /> 
  22.     }  
  23.   }  
  24.  
  25. @proxyHoc  
  26. class Input extends Component {  
  27.   render() {  
  28.     return <input {...this.props}></input>  
  29.   }  

上面的代码只是简略的一部分,除了input,我们还可以将HOC应用在select等其他表单组件,甚至还可以将上面的HOC兼容到span、table等展示组件,这样做可以大大简化代码,让我们省去了很多状态管理的工作,使用如下: 

  1. export default class extends Component {  
  2.   render() {  
  3.     return (  
  4.       <Form >  
  5.         <Input v_model="name"></Input>  
  6.         <Input v_model="pwd"></Input>  
  7.       </Form>  
  8.     )  
  9.   }  

表单校验

基于上面的双向绑定的例子,我们再来一个表单验证器,表单验证器可以包含验证函数以及提示信息,当验证不通过时,展示错误信息: 

  1. function validateHoc(WrappedComponent) {  
  2.   return class extends Component {  
  3.     constructor(props) {  
  4.       super(props);  
  5.       this.state = { error: '' }  
  6.     }  
  7.     onChange = (event) => {  
  8.       const { validator } = this.props;  
  9.       if (validator && typeof validator.func === 'function') {  
  10.         if (!validator.func(event.target.value)) {  
  11.           this.setState({ error: validator.msg })  
  12.         } else {  
  13.           this.setState({ error: '' })  
  14.         }  
  15.       }  
  16.     }  
  17.     render() {  
  18.       return <div>  
  19.         <WrappedComponent onChange={this.onChange}  {...this.props} />  
  20.         <div>{this.state.error || ''}</div>  
  21.       </div>  
  22.     }  
  23.   }  
  24.  
  1. const validatorName = {  
  2.   func: (val) => val && !isNaN(val),  
  3.   msg: '请输入数字'  
  4.  
  5. const validatorPwd = {  
  6.   func: (val) => val && val.length > 6,  
  7.   msg: '密码必须大于6位'  
  8.  
  9. <HOCInput validator={validatorName} v_model="name"></HOCInput>  
  10. <HOCInput validator={validatorPwd} v_model="pwd"></HOCInput> 

当然,还可以在Form提交的时候判断所有验证器是否通过,验证器也可以设置为数组等等,由于文章篇幅原因,代码被简化了很多,有兴趣的同学可以自己实现。

Redux的connect

redux中的connect,其实就是一个HOC,下面就是一个简化版的connect实现: 

  1. export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {  
  2.   class Connect extends Component {  
  3.     static contextTypes = {  
  4.       store: PropTypes.object  
  5.     }  
  6.     constructor () {  
  7.       super()  
  8.       this.state = {  
  9.         allProps: {}  
  10.       }  
  11.     }  
  12.     componentWillMount () {  
  13.       const { store } = this.context  
  14.       this._updateProps()  
  15.       store.subscribe(() => this._updateProps())  
  16.     }  
  17.     _updateProps () {  
  18.       const { store } = this.context  
  19.       let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {}   
  20.       let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}   
  21.       this.setState({  
  22.         allProps: {  
  23.           ...stateProps,  
  24.           ...dispatchProps,  
  25.           ...this.props  
  26.         }  
  27.       })  
  28.     }  
  29.     render () {  
  30.       return <WrappedComponent {...this.state.allProps} />  
  31.     }  
  32.   }  
  33.   return Connect  

代码非常清晰,connect函数其实就做了一件事,将mapStateToProps和mapDispatchToProps分别解构后传给原组件,这样我们在原组件内就可以直接用props获取state以及dispatch函数了。

使用HOC的注意事项

告诫—静态属性拷贝

当我们应用HOC去增强另一个组件时,我们实际使用的组件已经不是原组件了,所以我们拿不到原组件的任何静态属性,我们可以在HOC的结尾手动拷贝他们: 

  1. function proxyHOC(WrappedComponent) {  
  2.   class HOCComponent extends Component {  
  3.     render() {  
  4.       return <WrappedComponent {...this.props} /> 
  5.     }  
  6.   }  
  7.   HOCComponent.staticMethod = WrappedComponent.staticMethod;  
  8.   // ...   
  9.   return HOCComponent;  

如果原组件有非常多的静态属性,这个过程是非常痛苦的,而且你需要去了解需要增强的所有组件的静态属性是什么,我们可以使用hoist-non-react-statics来帮助我们解决这个问题,它可以自动帮我们拷贝所有非React的静态方法,使用方式如下: 

  1. import hoistNonReactStatic from 'hoist-non-react-statics';  
  2. function proxyHOC(WrappedComponent) {  
  3.   class HOCComponent extends Component {  
  4.     render() {  
  5.       return <WrappedComponent {...this.props} /> 
  6.     }  
  7.   }  
  8.   hoistNonReactStatic(HOCComponent,WrappedComponent);  
  9.   return HOCComponent;  

告诫—传递refs

使用高阶组件后,获取到的ref实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的ref。

高阶组件并不能像透传props那样将refs透传,我们可以用一个回调函数来完成ref的传递: 

  1. function hoc(WrappedComponent) {  
  2.   return class extends Component {  
  3.     getWrappedRef = () => this.wrappedRef;  
  4.     render() {  
  5.       return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {...this.props} /> 
  6.     }  
  7.   }  
  8.  
  9. @hoc  
  10. class Input extends Component {  
  11.   render() { return <input></input> }  
  12.  
  13. class App extends Component {  
  14.   render() {  
  15.     return (  
  16.       <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input>  
  17.     );  
  18.   }  

React 16.3版本提供了一个forwardRef API来帮助我们进行refs传递,这样我们在高阶组件上获取的ref就是原组件的ref了,而不需要再手动传递,如果你的React版本大于16.3,可以使用下面的方式:

  1. function hoc(WrappedComponent) {  
  2.   class HOC extends Component {  
  3.     render() {  
  4.       const { forwardedRef, ...props } = this.props;  
  5.       return <WrappedComponent ref={forwardedRef} {...props} /> 
  6.     }  
  7.   }  
  8.   return React.forwardRef((props, ref) => {  
  9.     return <HOC forwardedRef={ref} {...props} /> 
  10.   });  

告诫—不要在render方法内使用高阶组件

React Diff算法的原则是:

  •  使用组件标识确定是卸载还是更新组件
  •  如果组件的和前一次渲染时标识是相同的,递归更新子组件
  •  如果标识不同卸载组件重新挂载新组件

每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。

约定-不要改变原始组件

官方文档对高阶组件的说明:

高阶组件就是一个没有副作用的纯函数。

我们再来看看纯函数的定义:

如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。

该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变。

如果我们在高阶组件对原组件进行了修改,例如下面的代码: 

  1. InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... } 

这样就破坏了我们对高阶组件的约定,同时也改变了使用高阶组件的初衷:我们使用高阶组件是为了增强而非改变原组件。

约定-透传不相关的props

使用高阶组件,我们可以代理所有的props,但往往特定的HOC只会用到其中的一个或几个props。我们需要把其他不相关的props透传给原组件,如下面的代码: 

  1. function visible(WrappedComponent) {  
  2.   return class extends Component {  
  3.     render() {  
  4.       const { visible, ...props } = this.props;  
  5.       if (visible === false) return null;  
  6.       return <WrappedComponent {...props} /> 
  7.     }  
  8.   }  

我们只使用visible属性来控制组件的显示可隐藏,把其他props透传下去。

约定-displayName

在使用React Developer Tools进行调试时,如果我们使用了HOC,调试界面可能变得非常难以阅读,如下面的代码: 

  1. @visible  
  2. class Show extends Component {  
  3.   render() {  
  4.     return <h1>我是一个标签</h1>  
  5.   }  
  6.  
  7. @visible  
  8. class Title extends Component {  
  9.   render() {  
  10.     return <h1>我是一个标题</h1>  
  11.   }  

为了方便调试,我们可以手动为HOC指定一个displayName,官方推荐使用HOCName(WrappedComponentName): 

  1. static displayName = `Visible(${WrappedComponent.displayName})` 

这个约定帮助确保高阶组件***程度的灵活性和可重用性。

使用HOC的动机

回顾下上文提到的 Mixin 带来的风险:

  •  Mixin 可能会相互依赖,相互耦合,不利于代码维护
  •  不同的 Mixin 中的方法可能会相互冲突
  •  Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

而HOC的出现可以解决这些问题:

  •  高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合
  •  高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为
  •  高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担

HOC的缺陷

  •  HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  •  HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。

Hooks

[[262124]]

Hooks是React v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。

使用Hooks,你可以在将含有state的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试。同时,Hooks可以帮助你在不重写组件结构的情况下复用这些逻辑。所以,它也可以作为一种实现状态逻辑复用的方案。

阅读下面的章节使用Hook的动机你可以发现,它可以同时解决Mixin和HOC带来的问题。

官方提供的Hooks

State Hook

我们要使用class组件实现一个计数器功能,我们可能会这样写: 

  1. export default class Count extends Component {  
  2.   constructor(props) {  
  3.     super(props);  
  4.     this.state = { count: 0 }  
  5.   }  
  6.   render() {  
  7.     return (  
  8.       <div>  
  9.         <p>You clicked {this.state.count} times</p>  
  10.         <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}>  
  11.           Click me  
  12.         </button>  
  13.       </div>  
  14.     )  
  15.   }  

通过useState,我们使用函数式组件也能实现这样的功能: 

  1. export default function HookTest() {  
  2.   const [count, setCount] = useState(0);  
  3.   return (  
  4.     <div>  
  5.       <p>You clicked {count} times</p>  
  6.       <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>  
  7.         Click me  
  8.         </button>  
  9.     </div>  
  10.   );  

useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。

Effect Hook

Effect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作

参数

useEffect方法接收传入两个参数:

  •  1.回调函数:在第组件一次render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。
  •  2.状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。 
  1. useEffect(() => {  
  2.    // 只要组件render后就会执行  
  3.  });  
  4.  useEffect(() => {  
  5.    // 只有count改变时才会执行  
  6.  },[count]); 

回调返回值

useEffect的***个参数可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect之前,会调用这个函数。这个函数常常用来对上一次调用useEffect进行清理。 

  1. export default function HookTest() {  
  2.   const [count, setCount] = useState(0);  
  3.   useEffect(() => {  
  4.     console.log('执行...', count);  
  5.     return () => {  
  6.       console.log('清理...', count);  
  7.     }  
  8.   }, [count]);  
  9.   return (  
  10.     <div>  
  11.       <p>You clicked {count} times</p>  
  12.       <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>  
  13.         Click me  
  14.         </button>  
  15.     </div>  
  16.   );  

执行上面的代码,并点击几次按钮,会得到下面的结果:

注意,如果加上浏览器渲染的情况,结果应该是这样的: 

  1. 页面渲染...1  
  2.  执行... 1  
  3.  页面渲染...2  
  4.  清理... 1  
  5.  执行... 2  
  6.  页面渲染...3  
  7.  清理... 2  
  8.  执行... 3  
  9.  页面渲染...4  
  10.  清理... 3  
  11.  执行... 4 

那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。

你可以尝试下面的代码可能更好理解。

  1. var flag = 1 
  2.     var clean;  
  3.     function effect(flag) {  
  4.       return function () {  
  5.         console.log(flag);  
  6.       }  
  7.     }  
  8.     clean = effect(flag);  
  9.     flag = 2 
  10.     clean();  
  11.     clean = effect(flag);  
  12.     flag = 3 
  13.     clean();  
  14.     clean = effect(flag);  
  15.     // 执行结果  
  16.     effect... 1  
  17.     clean... 1  
  18.     effect... 2  
  19.     clean... 2  
  20.     effect... 3 

模拟componentDidMount

componentDidMount等价于useEffect的回调仅在页面初始化完成后执行一次,当useEffect的第二个参数传入一个空数组时可以实现这个效果。 

  1. function useDidMount(callback) {  
  2.   useEffect(callback, []);  

官方不推荐上面这种写法,因为这有可能导致一些错误。

模拟componentWillUnmount 

  1. function useUnMount(callback) {  
  2.   useEffect(() => callback, []);  

不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。

ref Hook

使用useRef Hook,你可以轻松的获取到dom的ref。 

  1. export default function Input() {  
  2.   const inputEl = useRef(null);  
  3.   const onButtonClick = () => {  
  4.     inputEl.current.focus();  
  5.   };  
  6.   return (  
  7.     <div>  
  8.       <input ref={inputEl} type="text" />  
  9.       <button onClick={onButtonClick}>Focus the input</button>  
  10.     </div>  
  11.   );  

注意useRef()并不仅仅可以用来当作获取ref使用,使用useRef产生的ref的current属性是可变的,这意味着你可以用它来保存一个任意值。

模拟componentDidUpdate

componentDidUpdate就相当于除去***次调用的useEffect,我们可以借助useRef生成一个标识,来记录是否为***次执行: 

  1. function useDidUpdate(callback, prop) {  
  2.   const init = useRef(true);  
  3.   useEffect(() => {  
  4.     if (init.current) {  
  5.       init.current = false 
  6.     } else {  
  7.       return callback();  
  8.     }  
  9.   }, prop);  

使用Hook的注意事项

使用范围

  •  只能在React函数式组件或自定义Hook中使用Hook。

Hook的提出主要就是为了解决class组件的一系列问题,所以我们能在class组件中使用它。

声明约束

  •  不要在循环,条件或嵌套函数中调用Hook。

Hook通过数组实现的,每次 useState 都会改变下标,React需要利用调用顺序来正确更新相应的状态,如果 useState 被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误。

我们可以安装一个eslint插件来帮助我们避免这些问题。 

  1. // 安装  
  2. npm install eslint-plugin-react-hooks --save-dev  
  3. // 配置  
  4.  
  5.   "plugins": [  
  6.     // ...  
  7.     "react-hooks"  
  8.   ],  
  9.   "rules": {  
  10.     // ...  
  11.     "react-hooks/rules-of-hooks": "error"  
  12.   }  

自定义Hook

像上面介绍的HOC和mixin一样,我们同样可以通过自定义的Hook将组件中类似的状态逻辑抽取出来。

自定义Hook非常简单,我们只需要定义一个函数,并且把相应需要的状态和effect封装进去,同时,Hook之间也是可以相互引用的。使用use开头命名自定义Hook,这样可以方便eslint进行检查。

下面我们看几个具体的Hook封装:

日志打点

我们可以使用上面封装的生命周期Hook。 

  1. const useLogger = (componentName, ...params) => {  
  2.   useDidMount(() => {  
  3.     console.log(`${componentName}初始化`, ...params);  
  4.   });  
  5.   useUnMount(() => {  
  6.     console.log(`${componentName}卸载`, ...params);  
  7.   })  
  8.   useDidUpdate(() => {  
  9.     console.log(`${componentName}更新`, ...params);  
  10.   });  
  11. };  
  12. function Page1(props){  
  13.   useLogger('Page1',props);  
  14.   return (<div>...</div> 

修改title

根据不同的页面名称修改页面title:

  1. function useTitle(title) {  
  2.   useEffect(  
  3.     () => {  
  4.       document.title = title;  
  5.       return () => (document.title = "主页");  
  6.     },  
  7.     [title]  
  8.   );  
  9.  
  10. function Page1(props){  
  11.   useTitle('Page1');  
  12.   return (<div>...</div> 

双向绑定

我们将表单onChange的逻辑抽取出来封装成一个Hook,这样所有需要进行双向绑定的表单组件都可以进行复用: 

  1. function useBind(init) {  
  2.   let [value, setValue] = useState(init);  
  3.   let onChange = useCallback(function(event) {  
  4.     setValue(event.currentTarget.value);  
  5.   }, []);  
  6.   return {  
  7.     value,  
  8.     onChange  
  9.   };  
  10.  
  11. function Page1(props){  
  12.   let value = useBind('');  
  13.   return <input {...value} /> 

当然,你可以向上面的HOC那样,结合context和form来封装一个更通用的双向绑定,有兴趣可以手动实现一下。

使用Hook的动机

减少状态逻辑复用的风险

Hook和Mixin在用法上有一定的相似之处,但是Mixin引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。

在不遵守约定的情况下使用HOC也有可能带来一定冲突,比如props覆盖等等,使用Hook则可以避免这些问题。

避免地狱式嵌套

大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用HOC,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。

让组件更容易理解

在使用class组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。

使用函数代替class

相比函数,编写一个class可能需要掌握更多的知识,需要注意的点也越多,比如this指向、绑定事件等等。另外,计算机理解一个class比理解一个函数更快。Hooks让你可以在classes之外使用更多React的新特性。

理性的选择

实际上,Hook在react 16.8.0才正式发布Hook稳定版本,笔者也还未在生产环境下使用,目前笔者在生产环境下使用的最多的是`HOC`。

React官方完全没有把classes从React中移除的打算,class组件和Hook完全可以同时存在,官方也建议避免任何“大范围重构”,毕竟这是一个非常新的版本,如果你喜欢它,可以在新的非关键性的代码中使用Hook。

小结

mixin已被抛弃,HOC正当壮年,Hook初露锋芒,前端圈就是这样,技术迭代速度非常之快,但我们在学习这些知识之时一定要明白为什么要学,学了有没有用,要不要用。不忘初心,方得始终。

文中如有错误,欢迎在评论区指正,谢谢阅读。

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2016-11-28 16:23:23

戴尔

2023-12-20 14:44:33

软件开发DevOpsNoOps

2023-04-06 09:42:00

LispHTMLQwit

2011-05-25 14:59:35

if elseswitch case

2020-05-17 13:59:37

物联网工业物联网工业4.0

2020-08-13 17:18:20

Kubernetes边缘容器

2017-09-12 15:26:44

2013-04-08 17:13:14

2017-01-03 16:57:58

2022-12-23 08:34:30

HookReact

2021-08-12 18:48:31

响应式编程Bio

2009-07-01 10:11:04

.NETLINQ

2023-05-24 09:00:28

DTW边缘计算APEX

2021-08-27 12:59:59

React前端命令

2019-09-09 16:33:10

华为

2022-04-14 11:50:39

函数组件hook

2020-09-24 08:45:10

React架构源码

2017-09-21 10:58:05

显示器凸面凹面

2017-04-07 11:15:49

原型链原型Javascript

2021-11-18 07:39:41

Json 序列化Vue
点赞
收藏

51CTO技术栈公众号