react hooks设计初衷
从刚学习react到现在有半年时间,对react的hooks写法和class写法都有接触过,基于此想写一篇文章对hooks的设计初衷作简要介绍和总结。文章分为三部分:
- hooks设计初衷
- 与class比较
- ahooks库介绍
一、hooks设计初衷
官方文档里面有说明,主要有三个动机
1、在组件之间复用状态逻辑很难
2、复杂组件变得难以理解
3、难以理解的class
第二条动机可以看作第一个问题的结果,由于逻辑复用很难,导致复杂组件的逻辑很混乱,难以理解。所以可以简化为两条动机,进行一一说明
· 逻辑复用
传统class组件逻辑复用有两种方法,高阶组件、render props、mixins(由于mixins会带来命名冲突,隐式依赖等问题,不建议使用)。从一个table请求的例子来讲。 实现一个table组件,分为两部分,一部分是逻辑,另一部分是视图(render函数)。逻辑部分大致分为以下几个关注点
1、异步加载的 loading 2、page 3、pageSize 4、筛选条件切换的时候,要重置 page 到第一页。page/pageSize 变化要重新发起请求 5、组件卸载的时候,要清掉正在进行的网络请求。
如果只复用table组件的渲染函数(即table是纯函数组件,没有生命周期),则每次都要在引用table的class组件里面重新写生命周期函数,逻辑处理部分。如果把生命周期部分放到table组件里面,当想在通用逻辑之外加上自己的逻辑,就会很难实现。此时table就是一个完整的组件,可以通过render props等方法向table组件注入额外的内容,这样会增加复杂度,hooks就是一种更好的逻辑复用方式。
const { tableProps, search, params } = useAntdTable(getTableData, {
defaultPageSize: 5,
form,
});
//自己的逻辑//
<Table columns={columns} {...tableProps} />
hooks可以很好的解决这个问题,自己封装的hooks就像是一个参数,传给组件就可以,也可以复用class的生命周期写法。如果使用hooks来模拟继承,就会更加明白。
但是hooks也不能完全替代高阶组件,render props,需要结合具体情况来判断。
· 难以理解的class
class难以理解,是因为this比较复杂。传统的this问题就不多说,Dan Abramov写的函数式组件和类组件之间的差异就是最核心的问题。
现在有如下两个组件:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
以上两个组件功能相同,延迟3s后打印所关注的user。一个是类组件,一个是函数式组件。如果在3s内props有变化,类组件的user会变化,函数式组件不会变化。这里和新旧数据无关,而是组件不应该混淆实际上真正的user,在某些情况下会遇到这样的问题。关注了A之后不应该随意变为B。如果对类组件进行改写就能得到和函数式组件一样的结果。
class ProfilePage extends React.Component {
render() {
const props = this.props;
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
这样类组件的结果就和函数式组件一样。如果进一步观察,把这个render函数提取出去正是上文的函数式组件,所以hooks一直隐藏在其中。
比较这两种组件可以得出结论
1、函数式组件捕获了渲染所使用的值
2、this永远会改变
捕获渲染所使用的值就是利用了闭包。闭包就是能够读取其他函数内部变量的函数。常见的节流,防抖原理就是闭包,闭包也能作为私有属性使用。
二、与class组件的比较
这里不列举开发上面的具体差异,这些网上也有很多资料,主要列举基础的异同和开发思路比较。这里主要参考官方文档。
1、class里面的生命周期方法目前还没有全部对应到hooks上面,getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError会尽快添加对应的hook。
2、组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。这里其实就是闭包,开发里经常会遇到的问题就是过时闭包。
3、类组件面向对象的编程思想,能够获得最大程度的操作权限,对象的所有属性都可以掌控。可以方便进行侵入性操作。
4、函数式组件类似面向过程编程,处理模块为一个个函数,由于hooks的存在使得函数可以拥有副作用,具有面向对象的一些特征。此外函数式可以无缝转换为逻辑流。很自然就联想到流程控制。
5、逻辑复用,hooks和mixin。mixin可以看作一个类,混入的组件都有这个类,可以对这个mixin重写,相当于继承。hooks纯粹的函数逻辑,看作一个参数。
三、ahooks介绍
ahooks是一个开源的hooks库,内置了许多常用的hooks,这里选取几个看一下设计思路。
function useCreation(factory, deps) {
var current = react_1.useRef({
deps: deps,
obj: undefined,
initialized: false
}).current;
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj;
}
防止重复创建实例,使用useRef作为标志储存,类似单例。
var useDeepCompareEffect = function useDeepCompareEffect(effect, deps) {
var ref = react_1.useRef();
var signalRef = react_1.useRef(0);
if (!depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
react_1.useEffect(effect, [signalRef.current]);
};
深比较useEffect依赖项,3.0版本新增hooks,比较实用。使用useRef储存是否更新
var useSetState = function useSetState(initialState) {
var _a = __read(react_1.useState(initialState), 2),
state = _a[0],
setState = _a[1];
var setMergeState = react_1.useCallback(function (patch) {
setState(function (prevState) {
var newState = utils_1.isFunction(patch) ? patch(prevState) : patch;
return newState ? __assign(__assign({}, prevState), newState) : prevState;
});
}, []);
return [state, setMergeState];
};
自动合并state的hooks,setState是默认不合并的。这里也需要注意要合理使用setState,把所有状态合在一起有时不利于状态抽离,封装hooks。
function useMemoizedFn(fn) {
......
var fnRef = react_1.useRef(fn);
fnRef.current = react_1.useMemo(function () {
return fn;
}, [fn]);
var memoizedFn = react_1.useRef();
if (!memoizedFn.current) {
memoizedFn.current = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current;
}
代替useCallBack,避免useCallBack重新渲染,进行性能优化。使用ref.current避免引起更新通知
四、总结
以上是hooks的基本介绍,如何使用参考官网完全可以入门。关于hooks设计的深入理解可以查看Dan Abramov的相关博客,里面会深入分析hooks的理念。而ahooks的本质不是魔法,没有高深的内容,却设计非常巧妙,值得借鉴思想。在之后的思考中会关注hooks与class的更新渲染方式。
关于hooks原理和react源码的关系详见后面的文章 :)
参考链接:
转载自:https://juejin.cn/post/7065271822484733960