这是蚂蚁金服内部技术分享的文字稿,本次分享主要介绍了为什么要用 Hooks?以及如何使用 Umi Hooks 提效?Umi Hooks http://github.com/umijs/hooks
开场
大家好,我叫尽龙,来自体验技术部。社区名称叫 brickspert,砖家的意思。
自从 React 推出 React Hooks 后,就开始尝试使用 Hooks,并逐渐喜欢上了它。目前,几乎 100% 的组件都是使用 Hooks 开发。经过大半年的实践,在 Hooks 使用方面沉淀了一些经验,很高兴今天有机会能分享给大家。
在分享开始之前,我想了解下:“有多少同学目前已经在项目中大量使用 Hooks 了?”
嗯嗯,谢谢。看举手的同学,大概一半一半吧。没关系,听完今天的分享,我相信你一定有兴趣尝试下 Hooks 的。
React Hooks 是 react v16.8 的一个新特性,很佩服这么重磅的功能,在一个小版本中发布,说明 React 团队有足够的信心向上兼容。
Why Hooks?
为什么要放弃 Class,转用 Hooks 呢?在内部外部有很多争论,包括知乎也有类似提问。我们也不免俗套的要对比下 Class 和 Hooks 了。当然为了保证今天的分享效果,我肯定会偏向 Hooks 的(哈哈哈哈)。
Class 学习成本高
Class 学习成本很高。首当其中的就是生命周期,多,太多了。不仅多,还会变!React v15 和 v16 就不一样。下面是我在网上随便找的一张图。
这个是 React v15 的生命周期,你都掌握了吗?你知道 v16 有什么变化吗?
之前无论你去哪里面试,基本都会有几个必问问题:
- 讲讲 React 生命周期?React v15 和 React v16 生命周期有啥变化?
- 如何优化 Class 组件?shouldComponentUpdate 是做什么的?如何用?
- 一般在哪个生命周期发送网络请求?为什么?
- ......
生命周期最重要,但是有很高的学习成本,需要大量实践才能积累足够的经验。当然,这几个问题回答不好,百分之八十以上的几率会挂掉。
当然不止是生命周期,this 也是一个很大的问题。你有没有在组件写很多 bind
?或者所有的函数都用箭头函数定义?
this.someFunction = this.someFunction.bind(this);
// 或
someFunction = ()=>{}
为什么要这样写呢?如果不写会有什么问题?哎呦,又多了一个面试题,你会吗?
Hooks 学习成本低
对比 Class,Hooks 的学习成本可就太低了!掌握了 useState 和 useEffect,80% 的事情就搞定了。
Class 业务逻辑分散
Class 业务逻辑分散,实现一个功能,我要写在不同的生命周期里面,不聚合~
比如,如果你有个定时器,你一定要在 componentWillUnMount
去卸载。
再比如,我们要写一个请求用户信息的组件,当userId
变化时,要重新发起请求。我们就要在两个生命中期中写请求的逻辑。
相信上面的逻辑,大家也是经常会写的吧。
奥奥,sorry,上面的 componentWillReceiveProps
已经被废弃了,我们应该用 componentDidUpdate
来代替。
“咦,这是为啥呢?好好的为什么要废弃,不让这么用了?”
又来一个面试题!你知道答案吗?
Hooks 业务逻辑聚合
而 Hooks 的业务逻辑就非常聚合了。上面的两个例子,改成 Hooks 你会写吗?
简直不要太简单!香啊!我可以提前下班了。
Class 逻辑复用困难
说到逻辑复用,很多同学会说 Class 的 Render Props 和 HOC(高阶组件)可以做逻辑复用!那我们看看 Class 的逻辑复用有多么的惨不忍睹。
首先我们看看 Render Props。
首先我们想复用监听 window size 变化的逻辑,开开心心的写了下面的代码。
然后,我又想复用监听鼠标位置的逻辑,我只能这么写了。
到这里你应该看到了问题所在。这简直就是地狱!我不忍心复用其它逻辑了。
我们放过 Render Props,来看看 HOC 吧。
如果你要问什么是 HOC,那我不得不推荐我的另外一篇文章《 助你完全理解React高阶组件(Higher-Order Components)》。哪怕你不知道 HOC 是啥,你也一定用过。比如 redux 的 connect。
上面的代码,我用了三个 HOC,分别是 redux 的 connect,react-intl 的 injectIntl,以及 AntD 的 Form.create()。
这是一个非常常见的用法。如果你光看代码,大概已经懵圈了。“我是谁?我在哪?我要干什么?”
这会我仿佛听见 HOC 在说:“我不仅让你看不懂我,我还很容易出各种问题。”
是的,HOC 很容易出问题。大家都往组件的 props 上面挂属性,万一有个重名的,那就只能说一句“不好意思,GG思密达”!
Hooks 逻辑复用简单
Hooks 来了,它表示,我要一个打五个!Render Props 和 HOC 联合起来也被我秒杀!
Hooks 表示,来十个,来一百个我也能打。
Hooks 最强的能力就是逻辑复用了,这是我最最最爱的能力了。
Hooks 会产生很多闭包问题
是的,我也不偏袒 Hooks,由于 React Hooks 的机制,如果用法不正确,会导致各种奇怪的闭包问题。
如果你要问 React Hooks 的机制是什么的话,我又要给你推荐一篇我之前写的文章了:《 React Hooks 原理》。
那面对这个问题,怎么解呢?说实话,我也没有很好的解决办法。
但是,这可能也有好处。如果碰到想不明白的问题,那 99% 是由于闭包导致的,我们有很确定的方向去排查问题。
记住这句话,你可以少走很多弯路。
Show Case
当然,说再多,吹再好,也没多大用。我上面讲的 Class 和 Hooks 的优缺点,网上的也有很多人讲,大家也肯定都看过。
用程序员的交流方式,就是“Talk is cheap,Show me the code.”。
亮剑吧!
接下来,我会用一个例子,让你折服,拜倒在 Hooks 的石榴裙下。如果你不服,咱们单独撕~
网络请求组件实现
接下来,我们来实现一个最最最常见的组件。该组件接收 userId,然后发起网络请求,获得用户信息。
说白了,就是最简单的发起网络请求的组件。我们先用 Class 来实现看看。
这段代码,是最简单的网络请求。
- 定义一个 username 状态。
- componentDidMount 的时候发起网络请求。
- 网络请求结束,更新 username。
美滋滋。但是少了点东西。网络请求,我们肯定要维护一个 loading 状态,保证用户体验比较好。
那我们加上吧。
这张图,我们增加了 loading 状态,在网络请求发起前,置为 true,在网络请求结束后,置为 false。
美滋滋。但是还是少点东西。userId 变化后,我要重新发起网络请求吧。
我们再加点代码吧。
我们增加了对 userId 变化的监听,如果 userId 变化后,重新发起请求。
这次稳了吧?
不不不,还不够。如果 userId 连续变化了 5 次,发送了 5 个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。
我加!加还不行吗!
其实到这里,有些同学已经懵了。“你说的时序控制,听着很有道理,但我平时都没处理过这个问题,我看下你怎么实现的。”
确实,时序控制不算一个简单的问题,很多新手都不会解决这个问题。
稳了!到这里你觉得稳了吧。
还是年轻啊,小伙子。
如果用上面的代码来玩,你可能会偶尔碰到上面的警告。这个警告是怎么造成的呢?我说一下你就明白了。下面四个步骤执行,必会报警告:
- 组件加载
- 发起网络请求
- 组件卸载
- 网络请求请求成功,触发 setState
看出问题了吗?组件已经卸载了,还去 setState,造成了内存溢出。
怎么解决呢?
在组件卸载的时候,放弃最后一次请求。
到这里为止,我们就完成了一个完美的网络请求。这次真结束了!
看下写了多少行代码。
除去空格,我们写了 38 行代码。实话说,38 行代码我能忍,但是这些逻辑我忍不了!回想下我们处理了多少逻辑:
- 网络请求
- loading
- userId 变化重新发起请求
- 竞态处理
- 组件卸载放弃网络请求
关键这些逻辑是没办法复用的!每个项目可能有数十上百个组件会发网络请求,我就要写几十,几百遍这样的逻辑。想想我都难受。
说实话,我在写项目的时候经常会偷懒。要不就不写 loading,要不就不管竞态,要不就不管最后的内存溢出警告。你有没有和我一样呢?嘿嘿。
言归正传,接下来就邀请 Hooks 登场了。
三下五除二,我们用 Hooks 实现了刚才所有的逻辑。
17 行!代码量减少了 50% 以上。好像还行!
但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!我们把刚才的逻辑全部封装起来!
useAsync 封装了刚才我们说的所有功能,一行代码完成了网络请求。
最后整个组件会长这样。
哇!我自己都佩服自己!简直了!美呆了,帅毙了,感觉自己无敌了!提前完成工作,下班回家!
通过这个例子,我想证明一个论点:“使用 Hooks 封装逻辑的能力,可以极大提高开发效率”。
Umi Hooks
这时候你肯定要问,useAsync 在哪里?给我瞧瞧?
useAsync
useAsync 在这里,快来瞧,快来看啦!
useAsync 是 Umi Hooks 库的核心 Hooks 之一,Umi Hooks 提供了大量提炼自业务的 Hooks。一行代码真的可以实现很多功能!
Umi Hooks 在这里! 在这里!你懂的~~
当然,useAsync 不止包含上面说的功能,还支持“手动触发”执行,还支持“轮询”!
只要简单的配置一个 pollingInterval
,就能轮询发送请求了。快去试试啦!
接下来我们会介绍几个更牛的 Hooks 给大家认识!
useAntdTable
AntD 的 Table 组件,想必大家在项目中经常用到吧!除了刚才异步请求的所有逻辑外,你还得处理其它的逻辑。
比如维护 page、pageSize、sorter、filter 的状态,还得考虑搜索条件变化后,重置 page 到第一页。这些逻辑光想想就头疼了,别说写了。
现在一行代码就可以实现了!useAntdTable,封装了所有的逻辑,只要一行代码!如图上所示,你只要 ...tableProps
,就可以了。这也许就是幸福的味道吧~
useLoadMore
加载更多的场景,比如下面动图的场景,想必大家在工作中都写过。
这样一个加载更多的场景,我们要维护多少状态?写多少行逻辑?本来我打算写个 Class 实现的例子贴出来的,但是我放弃了,因为太难了~~
随便想想要处理的逻辑:
- 第一次加载时候的 loading
- 加载更多时候的 loading
- 维护 page 和 pageSize
- 网络请求
- 是不是加载全了
- 搜索条件变化后,重置到第一页。
- .....
脑壳疼,真的脑壳疼。我会写,但是写起来真的好累。
还没完,一般产品同学还会要求,上拉加载更多......
这时候我们还得监听滚动位置,如果快到底了,触发加载更多。脑壳更疼了!
Umi Hooks 听到了你的求救,派出 useLoadMore 来拯救你了。一行代码就可以实现所有的功能!一个小时变一分钟,又可以早点下班了。
useDynamicList
还有更好用的,比如 useDynamicList,下面的动态列表,一行代码搞定。
useBoolean
不仅是上面讲到的各种复杂逻辑可以封装。简单的逻辑封装起来也是极其好用的,比如 Boolean 值的管理。
我们一般控制 Modal,Popover 等显示隐藏的时候,都要维护一个 visible 状态。大概会是这样。
这样的逻辑,你写过多少遍?没有几千也有几百吧!
以后你就可以用 useBoolean 咯!
More
不仅是上面讲到的这些,我们还有很多很多的 Hooks。
比如 useSearch,就封装了通常异步搜索场景的逻辑,比如 debounce。
比如 useVirtualList,就封装了虚拟列表的逻辑。
比如 useMouse,封装了监听鼠标位置的逻辑。
比如 useKeyPress,封装了监听键盘按键的逻辑。
30+ Hooks 供您选择,并且我们仍然处于婴儿期,快速发展中。我们的愿景就是:封装别人的逻辑,让别人无逻辑可写。
未来规划
更多的 Hooks 开发
如上面所述,我们现在还处于婴儿期,需要不断汲取能量,更多的 Hooks 正在路上!要实现“让别人无逻辑可写”的目标,还需继续奋斗。
更强大的 useRequest
大家应该都听过 useSWR 吧?是 zeit 公司开发的一个专门做网络请求的 Hooks,提供了很多新颖的思路,给了我们非常大的启发,github star 就像坐火箭一样。但在实际项目使用中,还是会有很多地方不符合蚂蚁内部的体系。但是它给我们非常大的启发,基于 swr 的思路,我们可以实现更强大的 useRequest!图上的能力,我们都要!
useRequest 目前已经处于内测期了,下个版本将会与大家见面!我们的目标是:所有的网络请求,只用 useRequest 就够了!
Hooks 生态
目前社区上 Hooks 相关的基础教程、进阶教程、原理深入、常见问题等文档都比较分散,我们准备向 Hooks 生态发展,提供各式各样的文章。以后学习 Hooks,使用 Hooks,找 Umi Hooks 就对了。
当然,生态方面目前正在规划中,预计年后启动。
总结
Umi Hooks,你值得拥有。
我们目前处于发展阶段,欢迎大家一起共建。
你可以提 idea,我们负责实现。
你可以提 issue,我们负责改 bug。
你可以提 PR,将你封装的 Hooks 分享给大家,让更多人收益。
❤️期待您的参与。