前端发展速度很是之快,页面和组件变得愈来愈复杂,如何更好的实现状态逻辑复用
一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。html
本文介绍了React
采用的三种实现状态逻辑复用
的技术,并分析了他们的实现原理、使用方法、实际应用以及如何选择使用他们。前端
本文略长,下面是本文的思惟导图,您能够从头开始阅读,也能够选择感兴趣的部分阅读:vue
Mixin
(混入)是一种经过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另外一个对象上面去,不过你能够拷贝任意多
个对象的任意个
方法到一个新对象上去,这是继承
所不能实现的。它的出现主要就是为了解决代码复用问题。react
不少开源库提供了Mixin
的实现,如Underscore
的_.extend
方法、JQuery
的extend
方法。git
使用_.extend
方法实现代码复用:es6
var LogMixin = {
actionLog: function() {
console.log('action...');
},
requestLog: function() {
console.log('request...');
},
};
function User() { /*..*/ }
function Goods() { /*..*/ }
_.extend(User.prototype, LogMixin);
_.extend(Goods.prototype, LogMixin);
var user = new User();
var good = new Goods();
user.actionLog();
good.requestLog();
复制代码
咱们能够尝试手动写一个简单的Mixin
方法:github
function setMixin(target, mixin) {
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++) {
target.prototype[arguments[i]] = mixin.prototype[arguments[i]];
}
}
else {
for (var methodName in mixin.prototype) {
if (!Object.hasOwnProperty(target.prototype, methodName)) {
target.prototype[methodName] = mixin.prototype[methodName];
}
}
}
}
setMixin(User,LogMixin,'actionLog');
setMixin(Goods,LogMixin,'requestLog');
复制代码
您可使用setMixin
方法将任意对象的任意方法扩展到目标对象上。算法
React
也提供了Mixin
的实现,若是彻底不一样的组件有类似的功能,咱们能够引入来实现代码复用,固然只有在使用createClass
来建立React
组件时才可使用,由于在React
组件的es6
写法中它已经被废弃掉了。npm
例以下面的例子,不少组件或页面都须要记录用户行为,性能指标等。若是咱们在每一个组件都引入写日志的逻辑,会产生大量重复代码,经过Mixin
咱们能够解决这一问题:redux
var LogMixin = {
log: function() {
console.log('log');
},
componentDidMount: function() {
console.log('in');
},
componentWillUnmount: function() {
console.log('out');
}
};
var User = React.createClass({
mixins: [LogMixin],
render: function() {
return (<div>...</div>)
}
});
var Goods = React.createClass({
mixins: [LogMixin],
render: function() {
return (<div>...</div>)
}
});
复制代码
React
官方文档在Mixins Considered Harmful一文中提到了Mixin
带来了危害:
Mixin
可能会相互依赖,相互耦合,不利于代码维护Mixin
中的方法可能会相互冲突Mixin
很是多时,组件是能够感知到的,甚至还要为其作相关处理,这样会给代码形成滚雪球式的复杂性React
如今已经再也不推荐使用Mixin
来解决代码复用问题,由于Mixin
带来的危害比他产生的价值还要巨大,而且React
全面推荐使用高阶组件来替代它。另外,高阶组件还能实现更多其余更强大的功能,在学习高阶组件以前,咱们先来看一个设计模式。
装饰者(decorator
)模式可以在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责。与继承相比,装饰者是一种更轻便灵活的作法。
高阶组件能够看做React
对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件做为参数,并返回一个新的组件。
高阶组件(
HOC
)是React
中的高级技术,用来重用组件逻辑。但高阶组件自己并非React API
。它只是一种模式,这种模式是由React
自身的组合性质必然产生的。
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props;
if (visible === false) return null;
return <WrappedComponent {...props} />; } } } 复制代码
上面的代码就是一个HOC
的简单应用,函数接收一个组件做为参数,并返回一个新组件,新组建能够接收一个visible props
,根据visible
的值来判断是否渲染Visible。
下面咱们从如下几方面来具体探索HOC
。
函数返回一个咱们本身定义的组件,而后在render
中返回要包裹的组件,这样咱们就能够代理全部传入的props
,而且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible
就是一个HOC
属性代理的实现方式。
function proxyHOC(WrappedComponent) {
return class extends Component {
render() {
return <WrappedComponent {...this.props} />; } } } 复制代码
对比原生组件加强的项:
props
static
方法refs
返回一个组件,继承原组件,在render
中调用原组件的render
。因为继承了原组件,能经过this访问到原组件的生命周期、props、state、render
等,相比属性代理它能操做更多的属性。
function inheritHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
复制代码
对比原生组件加强的项:
props
static
方法refs
state
可以使用任何其余组件和原组件进行组合渲染,达到样式、布局复用等效果。
经过属性代理实现
function stylHOC(WrappedComponent) {
return class extends Component {
render() {
return (<div> <div className="title">{this.props.title}</div> <WrappedComponent {...this.props} /> </div>); } } } 复制代码
经过反向继承实现
function styleHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
return <div> <div className="title">{this.props.title}</div> {super.render()} </div>
}
}
}
复制代码
根据特定的属性决定原组件是否渲染
经过属性代理实现
function visibleHOC(WrappedComponent) {
return class extends Component {
render() {
if (this.props.visible === false) return null;
return <WrappedComponent {...props} />; } } } 复制代码
经过反向继承实现
function visibleHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
if (this.props.visible === false) {
return null
} else {
return super.render()
}
}
}
}
复制代码
能够对传入组件的props
进行增长、修改、删除或者根据特定的props
进行特殊的操做。
经过属性代理实现
function proxyHOC(WrappedComponent) {
return class extends Component {
render() {
const newProps = {
...this.props,
user: 'ConardLi'
}
return <WrappedComponent {...newProps} />; } } } 复制代码
高阶组件中可获取原组件的ref
,经过ref
获取组件实力,以下面的代码,当程序初始化完成后调用原组件的log方法。(不知道refs怎么用,请👇Refs & DOM)
经过属性代理实现
function refHOC(WrappedComponent) {
return class extends Component {
componentDidMount() {
this.wapperRef.log()
}
render() {
return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />; } } } 复制代码
这里注意:调用高阶组件的时候并不能获取到原组件的真实ref
,须要手动进行传递,具体请看传递refs
将原组件的状态提取到HOC
中进行管理,以下面的代码,咱们将Input
的value
提取到HOC
中进行管理,使它变成受控组件,同时不影响它使用onChange
方法进行一些其余操做。基于这种方式,咱们能够实现一个简单的双向绑定
,具体请看双向绑定。
经过属性代理实现
function proxyHoc(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
onChange = (event) => {
const { onChange } = this.props;
this.setState({
value: event.target.value,
}, () => {
if(typeof onChange ==='function'){
onChange(event);
}
})
}
render() {
const newProps = {
value: this.state.value,
onChange: this.onChange,
}
return <WrappedComponent {...this.props} {...newProps} />; } } } class HOC extends Component { render() { return <input {...this.props}></input> } } export default proxyHoc(HOC); 复制代码
上面的例子经过属性代理利用HOC的state对原组件进行了必定的加强,但并不能直接控制原组件的state
,而经过反向继承,咱们能够直接操做原组件的state
。可是并不推荐直接修改或添加原组件的state
,由于这样有可能和组件内部的操做构成冲突。
经过反向继承实现
function debugHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
console.log('props', this.props);
console.log('state', this.state);
return (
<div className="debuging"> {super.render()} </div>
)
}
}
}
复制代码
上面的HOC
在render
中将props
和state
打印出来,能够用做调试阶段,固然你能够在里面写更多的调试代码。想象一下,只须要在咱们想要调试的组件上加上@debug
就能够对该组件进行调试,而不须要在每次调试的时候写不少冗余代码。(若是你还不知道怎么使用HOC,请👇如何使用HOC)
高阶组件能够在render函数中作很是多的操做,从而控制原组件的渲染输出。只要改变了原组件的渲染,咱们都将它称之为一种渲染劫持
。
实际上,上面的组合渲染和条件渲染都是渲染劫持
的一种,经过反向继承,不只能够实现以上两点,还可直接加强
由原组件render
函数产生的React元素
。
经过反向继承实现
function hijackHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render();
let newProps = {};
if (tree && tree.type === 'input') {
newProps = { value: '渲染被劫持了' };
}
const props = Object.assign({}, tree.props, newProps);
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
}
}
复制代码
注意上面的说明我用的是加强
而不是更改
。render
函数内其实是调用React.creatElement
产生的React元素
:
getOwnPropertyDescriptors
函数来打印下它的配置项:
能够发现,全部的writable
属性均被配置为了false
,即全部属性是不可变的。(对这些配置项有疑问,请👇defineProperty)
不能直接修改,咱们能够借助cloneElement
方法来在原组件的基础上加强一个新组件:
React.cloneElement()
克隆并返回一个新的React元素
,使用element
做为起点。生成的元素将会拥有原始元素props与新props的浅合并。新的子级会替换现有的子级。来自原始元素的 key 和 ref 将会保留。
React.cloneElement()
几乎至关于:
<element.type {...element.props} {...props}>{children}</element.type>
复制代码
上面的示例代码都写的是如何声明一个HOC
,HOC
其实是一个函数,因此咱们将要加强的组件做为参数调用HOC
函数,获得加强后的组件。
class myComponent extends Component {
render() {
return (<span>原组件</span>)
}
}
export default inheritHOC(myComponent);
复制代码
在实际应用中,一个组件可能被多个HOC
加强,咱们使用的是被全部的HOC
加强后的组件,借用一张装饰模式
的图来讲明,可能更容易理解:
假设如今咱们有logger
,visible
,style
等多个HOC
,如今要同时加强一个Input
组件:
logger(visible(style(Input)))
复制代码
这种代码很是的难以阅读,咱们能够手动封装一个简单的函数组合工具,将写法改写以下:
const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
compose(logger,visible,style)(Input);
复制代码
compose
函数返回一个全部函数组合后的函数,compose(f, g, h)
和 (...args) => f(g(h(...args)))
是同样的。
不少第三方库都提供了相似compose
的函数,例如lodash.flowRight
,Redux
提供的combineReducers
函数等。
咱们还能够借助ES7
为咱们提供的Decorators
来让咱们的写法变的更加优雅:
@logger
@visible
@style
class Input extends Component {
// ...
}
复制代码
Decorators
是ES7
的一个提案,尚未被标准化,但目前Babel
转码器已经支持,咱们须要提早配置babel-plugin-transform-decorators-legacy
:
"plugins": ["transform-decorators-legacy"]
复制代码
还能够结合上面的compose
函数使用:
const hoc = compose(logger, visible, style);
@hoc
class Input extends Component {
// ...
}
复制代码
下面是一些我在生产环境中实际对HOC
的实际应用场景,因为文章篇幅缘由,代码通过不少简化,若有问题欢迎在评论区指出:
实际上这属于一类最多见的应用,多个组件拥有相似的逻辑,咱们要对重复的逻辑进行复用, 官方文档中CommentList
的示例也是解决了代码复用问题,写的很详细,有兴趣能够👇使用高阶组件(HOC)解决横切关注点。
某些页面须要记录用户行为,性能指标等等,经过高阶组件作这些事情能够省去不少重复代码。
function logHoc(WrappedComponent) {
return class extends Component {
componentWillMount() {
this.start = Date.now();
}
componentDidMount() {
this.end = Date.now();
console.log(`${WrappedComponent.dispalyName} 渲染时间:${this.end - this.start} ms`);
console.log(`${user}进入${WrappedComponent.dispalyName}`);
}
componentWillUnmount() {
console.log(`${user}退出${WrappedComponent.dispalyName}`);
}
render() {
return <WrappedComponent {...this.props} /> } } } 复制代码
function auth(WrappedComponent) {
return class extends Component {
render() {
const { visible, auth, display = null, ...props } = this.props;
if (visible === false || (auth && authList.indexOf(auth) === -1)) {
return display
}
return <WrappedComponent {...props} />; } } } 复制代码
authList
是咱们在进入程序时向后端请求的全部权限列表,当组件所须要的权限不列表中,或者设置的 visible
是false
,咱们将其显示为传入的组件样式,或者null
。咱们能够将任何须要进行权限校验的组件应用HOC
:
@auth
class Input extends Component { ... }
@auth
class Button extends Component { ... }
<Button auth="user/addUser">添加用户</Button>
<Input auth="user/search" visible={false} >添加用户</Input>
复制代码
在vue
中,绑定一个变量后可实现双向数据绑定,即表单中的值改变后绑定的变量也会自动改变。而React
中没有作这样的处理,在默认状况下,表单元素都是非受控组件
。给表单元素绑定一个状态后,每每须要手动书写onChange
方法来将其改写为受控组件
,在表单元素很是多的状况下这些重复操做是很是痛苦的。
咱们能够借助高阶组件来实现一个简单的双向绑定,代码略长,能够结合下面的思惟导图进行理解。
首先咱们自定义一个Form
组件,该组件用于包裹全部须要包裹的表单组件,经过contex
向子组件暴露两个属性:
model
:当前Form
管控的全部数据,由表单name
和value
组成,如{name:'ConardLi',pwd:'123'}
。model
可由外部传入,也可自行管控。changeModel
:改变model
中某个name
的值。class Form extends Component {
static childContextTypes = {
model: PropTypes.object,
changeModel: PropTypes.func
}
constructor(props, context) {
super(props, context);
this.state = {
model: props.model || {}
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.model) {
this.setState({
model: nextProps.model
})
}
}
changeModel = (name, value) => {
this.setState({
model: { ...this.state.model, [name]: value }
})
}
getChildContext() {
return {
changeModel: this.changeModel,
model: this.props.model || this.state.model
};
}
onSubmit = () => {
console.log(this.state.model);
}
render() {
return <div> {this.props.children} <button onClick={this.onSubmit}>提交</button> </div>
}
}
复制代码
下面定义用于双向绑定的HOC
,其代理了表单的onChange
属性和value
属性:
onChange
事件时调用上层Form
的changeModel
方法来改变context
中的model
。value
改成从context
中取出的值。function proxyHoc(WrappedComponent) {
return class extends Component {
static contextTypes = {
model: PropTypes.object,
changeModel: PropTypes.func
}
onChange = (event) => {
const { changeModel } = this.context;
const { onChange } = this.props;
const { v_model } = this.props;
changeModel(v_model, event.target.value);
if(typeof onChange === 'function'){onChange(event);}
}
render() {
const { model } = this.context;
const { v_model } = this.props;
return <WrappedComponent {...this.props} value={model[v_model]} onChange={this.onChange} />; } } } @proxyHoc class Input extends Component { render() { return <input {...this.props}></input> } } 复制代码
上面的代码只是简略的一部分,除了input
,咱们还能够将HOC
应用在select
等其余表单组件,甚至还能够将上面的HOC
兼容到span、table
等展现组件,这样作能够大大简化代码,让咱们省去了不少状态管理的工做,使用以下:
export default class extends Component {
render() {
return (
<Form > <Input v_model="name"></Input> <Input v_model="pwd"></Input> </Form>
)
}
}
复制代码
基于上面的双向绑定的例子,咱们再来一个表单验证器,表单验证器能够包含验证函数以及提示信息,当验证不经过时,展现错误信息:
function validateHoc(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = { error: '' }
}
onChange = (event) => {
const { validator } = this.props;
if (validator && typeof validator.func === 'function') {
if (!validator.func(event.target.value)) {
this.setState({ error: validator.msg })
} else {
this.setState({ error: '' })
}
}
}
render() {
return <div> <WrappedComponent onChange={this.onChange} {...this.props} /> <div>{this.state.error || ''}</div> </div> } } } 复制代码
const validatorName = {
func: (val) => val && !isNaN(val),
msg: '请输入数字'
}
const validatorPwd = {
func: (val) => val && val.length > 6,
msg: '密码必须大于6位'
}
<HOCInput validator={validatorName} v_model="name"></HOCInput>
<HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>
复制代码
固然,还能够在Form
提交的时候判断全部验证器是否经过,验证器也能够设置为数组等等,因为文章篇幅缘由,代码被简化了不少,有兴趣的同窗能够本身实现。
redux中的connect
,其实就是一个HOC
,下面就是一个简化版的connect
实现:
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {}
let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} /> } } return Connect } 复制代码
代码很是清晰,connect
函数其实就作了一件事,将mapStateToProps
和mapDispatchToProps
分别解构后传给原组件,这样咱们在原组件内就能够直接用props
获取state
以及dispatch
函数了。
当咱们应用HOC
去加强另外一个组件时,咱们实际使用的组件已经不是原组件了,因此咱们拿不到原组件的任何静态属性,咱们能够在HOC
的结尾手动拷贝他们:
function proxyHOC(WrappedComponent) {
class HOCComponent extends Component {
render() {
return <WrappedComponent {...this.props} />; } } HOCComponent.staticMethod = WrappedComponent.staticMethod; // ... return HOCComponent; } 复制代码
若是原组件有很是多的静态属性,这个过程是很是痛苦的,并且你须要去了解须要加强的全部组件的静态属性是什么,咱们可使用hoist-non-react-statics
来帮助咱们解决这个问题,它能够自动帮咱们拷贝全部非React
的静态方法,使用方式以下:
import hoistNonReactStatic from 'hoist-non-react-statics';
function proxyHOC(WrappedComponent) {
class HOCComponent extends Component {
render() {
return <WrappedComponent {...this.props} />; } } hoistNonReactStatic(HOCComponent,WrappedComponent); return HOCComponent; } 复制代码
使用高阶组件后,获取到的ref
其实是最外层的容器组件,而非原组件,可是不少状况下咱们须要用到原组件的ref
。
高阶组件并不能像透传props
那样将refs
透传,咱们能够用一个回调函数来完成ref
的传递:
function hoc(WrappedComponent) {
return class extends Component {
getWrappedRef = () => this.wrappedRef;
render() {
return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {...this.props} />; } } } @hoc class Input extends Component { render() { return <input></input> } } class App extends Component { render() { return ( <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input> ); } } 复制代码
React 16.3
版本提供了一个forwardRef API
来帮助咱们进行refs
传递,这样咱们在高阶组件上获取的ref
就是原组件的ref
了,而不须要再手动传递,若是你的React
版本大于16.3
,可使用下面的方式:
function hoc(WrappedComponent) {
class HOC extends Component {
render() {
const { forwardedRef, ...props } = this.props;
return <WrappedComponent ref={forwardedRef} {...props} />;
}
}
return React.forwardRef((props, ref) => {
return <HOC forwardedRef={ref} {...props} />;
});
}
复制代码
React
Diff
算法的原则是:
每次调用高阶组件生成的都是是一个全新的组件,组件的惟一标识响应的也会改变,若是在render
方法调用了高阶组件,这会致使组件每次都会被卸载后从新挂载。
官方文档对高阶组件的说明:
高阶组件就是一个没有反作用的纯函数。
咱们再来看看纯函数的定义:
若是函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。 该函数不会产生任何可观察的反作用,例如网络请求,输入和输出设备或数据突变。
若是咱们在高阶组件对原组件进行了修改,例以下面的代码:
InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... }
复制代码
这样就破坏了咱们对高阶组件的约定,同时也改变了使用高阶组件的初衷:咱们使用高阶组件是为了加强
而非改变
原组件。
使用高阶组件,咱们能够代理全部的props
,但每每特定的HOC
只会用到其中的一个或几个props
。咱们须要把其余不相关的props
透传给原组件,以下面的代码:
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props;
if (visible === false) return null;
return <WrappedComponent {...props} />; } } } 复制代码
咱们只使用visible
属性来控制组件的显示可隐藏,把其余props
透传下去。
在使用React Developer Tools
进行调试时,若是咱们使用了HOC
,调试界面可能变得很是难以阅读,以下面的代码:
@visible
class Show extends Component {
render() {
return <h1>我是一个标签</h1>
}
}
@visible
class Title extends Component {
render() {
return <h1>我是一个标题</h1>
}
}
复制代码
为了方便调试,咱们能够手动为HOC
指定一个displayName
,官方推荐使用HOCName(WrappedComponentName)
:
static displayName = `Visible(${WrappedComponent.displayName})`
复制代码
这个约定帮助确保高阶组件最大程度的灵活性和可重用性。
回顾下上文提到的 Mixin
带来的风险:
Mixin
可能会相互依赖,相互耦合,不利于代码维护Mixin
中的方法可能会相互冲突Mixin
很是多时,组件是能够感知到的,甚至还要为其作相关处理,这样会给代码形成滚雪球式的复杂性而HOC
的出现能够解决这些问题:
HOC
须要在原组件上进行包裹或者嵌套,若是大量使用HOC
,将会产生很是多的嵌套,这让调试变得很是困难。HOC
能够劫持props
,在不遵照约定的状况下也可能形成冲突。Hooks
是React v16.7.0-alpha
中加入的新特性。它可让你在class
之外使用state
和其余React
特性。
使用Hooks
,你能够在将含有state
的逻辑从组件中抽象出来,这将可让这些逻辑容易被测试。同时,Hooks
能够帮助你在不重写组件结构的状况下复用这些逻辑。因此,它也能够做为一种实现状态逻辑复用
的方案。
阅读下面的章节使用Hook的动机你能够发现,它能够同时解决Mixin
和HOC
带来的问题。
咱们要使用class
组件实现一个计数器
功能,咱们可能会这样写:
export default class Count extends Component {
constructor(props) {
super(props);
this.state = { count: 0 }
}
render() {
return (
<div> <p>You clicked {this.state.count} times</p> <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}> Click me </button> </div>
)
}
}
复制代码
经过useState
,咱们使用函数式组件也能实现这样的功能:
export default function HookTest() {
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}> Click me </button> </div>
);
}
复制代码
useState
是一个钩子,他能够为函数式组件增长一些状态,而且提供改变这些状态的函数,同时它接收一个参数,这个参数做为状态的默认值。
Effect Hook 可让你在函数组件中执行一些具备 side effect(反作用)的操做
参数
useEffect
方法接收传入两个参数:
render
和以后的每次update
后运行,React
保证在DOM
已经更新完成以后才会运行回调。useEffect(() => {
// 只要组件render后就会执行
});
useEffect(() => {
// 只有count改变时才会执行
},[count]);
复制代码
回调返回值
useEffect
的第一个参数能够返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect
以前,会调用这个函数。这个函数经常用来对上一次调用useEffect
进行清理。
export default function HookTest() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('执行...', count);
return () => {
console.log('清理...', count);
}
}, [count]);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}> Click me </button> </div>
);
}
复制代码
执行上面的代码,并点击几回按钮,会获得下面的结果:
注意,若是加上浏览器渲染的状况,结果应该是这样的:
页面渲染...1
执行... 1
页面渲染...2
清理... 1
执行... 2
页面渲染...3
清理... 2
执行... 3
页面渲染...4
清理... 3
执行... 4
复制代码
那么为何在浏览器渲染完后,再执行清理的方法还能找到上次的state
呢?缘由很简单,咱们在useEffect
中返回的是一个函数,这造成了一个闭包,这能保证咱们上一次执行函数存储的变量不被销毁和污染。
你能够尝试下面的代码可能更好理解
var flag = 1;
var clean;
function effect(flag) {
return function () {
console.log(flag);
}
}
clean = effect(flag);
flag = 2;
clean();
clean = effect(flag);
flag = 3;
clean();
clean = effect(flag);
// 执行结果
effect... 1
clean... 1
effect... 2
clean... 2
effect... 3
复制代码
模拟componentDidMount
componentDidMount
等价于useEffect
的回调仅在页面初始化完成后执行一次,当useEffect
的第二个参数传入一个空数组时能够实现这个效果。
function useDidMount(callback) {
useEffect(callback, []);
}
复制代码
官方不推荐上面这种写法,由于这有可能致使一些错误。
模拟componentWillUnmount
function useUnMount(callback) {
useEffect(() => callback, []);
}
复制代码
不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。
使用useRef Hook
,你能够轻松的获取到dom
的ref
。
export default function Input() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<div> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </div> ); } 复制代码
注意useRef()
并不只仅能够用来看成获取ref
使用,使用useRef
产生的ref
的current
属性是可变的,这意味着你能够用它来保存一个任意值。
模拟componentDidUpdate
componentDidUpdate
就至关于除去第一次调用的useEffect
,咱们能够借助useRef
生成一个标识,来记录是否为第一次执行:
function useDidUpdate(callback, prop) {
const init = useRef(true);
useEffect(() => {
if (init.current) {
init.current = false;
} else {
return callback();
}
}, prop);
}
复制代码
React
函数式组件或自定义Hook
中使用Hook
。Hook
的提出主要就是为了解决class
组件的一系列问题,因此咱们能在class
组件中使用它。
Hook
经过数组实现的,每次useState
都会改变下标,React
须要利用调用顺序来正确更新相应的状态,若是useState
被包裹循环或条件语句中,那每就可能会引发调用顺序的错乱,从而形成意想不到的错误。
咱们能够安装一个eslint
插件来帮助咱们避免这些问题。
// 安装
npm install eslint-plugin-react-hooks --save-dev
// 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error"
}
}
复制代码
像上面介绍的HOC
和mixin
同样,咱们一样能够经过自定义的Hook
将组件中相似的状态逻辑抽取出来。
自定义Hook
很是简单,咱们只须要定义一个函数,而且把相应须要的状态和effect
封装进去,同时,Hook
之间也是能够相互引用的。使用use
开头命名自定义Hook
,这样能够方便eslint
进行检查。
下面咱们看几个具体的Hook
封装:
咱们可使用上面封装的生命周期Hook
。
const useLogger = (componentName, ...params) => {
useDidMount(() => {
console.log(`${componentName}初始化`, ...params);
});
useUnMount(() => {
console.log(`${componentName}卸载`, ...params);
})
useDidUpdate(() => {
console.log(`${componentName}更新`, ...params);
});
};
function Page1(props){
useLogger('Page1',props);
return (<div>...</div>)
}
复制代码
根据不一样的页面名称修改页面title
:
function useTitle(title) {
useEffect(
() => {
document.title = title;
return () => (document.title = "主页");
},
[title]
);
}
function Page1(props){
useTitle('Page1');
return (<div>...</div>)
}
复制代码
咱们将表单onChange
的逻辑抽取出来封装成一个Hook
,这样全部须要进行双向绑定的表单组件均可以进行复用:
function useBind(init) {
let [value, setValue] = useState(init);
let onChange = useCallback(function(event) {
setValue(event.currentTarget.value);
}, []);
return {
value,
onChange
};
}
function Page1(props){
let value = useBind('');
return <input {...value} />; } 复制代码
固然,你能够向上面的HOC
那样,结合context
和form
来封装一个更通用的双向绑定,有兴趣能够手动实现一下。
Hook
和Mixin
在用法上有必定的类似之处,可是Mixin
引入的逻辑和状态是能够相互覆盖的,而多个Hook
之间互不影响,这让咱们不须要在把一部分精力放在防止避免逻辑复用的冲突上。
在不遵照约定的状况下使用HOC
也有可能带来必定冲突,好比props
覆盖等等,使用Hook
则能够避免这些问题。
大量使用HOC
的状况下让咱们的代码变得嵌套层级很是深,使用Hook
,咱们能够实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。
在使用class
组件构建咱们的程序时,他们各自拥有本身的状态,业务逻辑的复杂使这些组件变得愈来愈庞大,各个生命周期中会调用愈来愈多的逻辑,愈来愈难以维护。使用Hook
,可让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。
相比函数,编写一个class
可能须要掌握更多的知识,须要注意的点也越多,好比this
指向、绑定事件等等。另外,计算机理解一个函数比理解一个class
更快。Hooks
让你能够在classes
以外使用更多React
的新特性。
实际上,Hook
在react 16.8.0
才正式发布Hook
稳定版本,笔者也还未在生产环境下使用,目前笔者在生产环境下使用的最多的是HOC
。
React
官方彻底没有把classes
从React
中移除的打算,class
组件和Hook
彻底能够同时存在,官方也建议避免任何“大范围重构”,毕竟这是一个很是新的版本,若是你喜欢它,能够在新的非关键性的代码中使用Hook
。
mixin
已被抛弃,HOC
正当壮年,Hook
初露锋芒,前端圈就是这样,技术迭代速度很是之快,但咱们在学习这些知识之时必定要明白为何要学,学了有没有用,要不要用。不忘初心,方得始终。
文中若有错误,欢迎在评论区指正,谢谢阅读。
想阅读更多优质文章,或者须要文章中思惟导图源文件可关注个人github博客,欢迎star✨。
推荐关注个人微信公众号【code秘密花园】,咱们一块儿交流成长。
关注公众号后回复【加群】拉你进入优质前端交流群。