Talking about hooks

99 阅读8分钟
从React16.8开始,Hooks API正式被React支持,而就在最近,Vue作者尤雨溪翻译并发布了一篇自己的文章《Vue Function-based API RFC》,并在全文开头强调这是Vue 3.0最重要的RFC,并在文中提到
Function-based API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案。
可以简单的理解为,React 和 Vue 为了解决相同的问题,基于不同的技术实现了相似的API。所以本文也将结合两种框架各自的特点,简单讲讲个人对Hooks的理解。
在未来版本的规划里,React并不如Vue激进,React的文档里专门提到
并没有从 React 中移除 class的计划。
而Vue却采取了不同的升级策略,做好了抛弃大部分历史语法的准备
  • 兼容版本:同时支持新 API 和 2.x 的所有选项;
  • 标准版本:只支持新 API 和部分 2.x 选项。
为什么我们不再需要Class Component?为了回答这个问题,我们先看看之前和现在的React组件划分产生了哪些变化。
1. 既然本来就有函数组件,开始为什么引入class组件?早期的React组件可以依据“有没有状态(state)”分为
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
// 无状态组件
const Welcome = (props) => <h1>Hello, {props.name}</h1>;
// 有状态组件
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: 'KuaiGou'};
}
render() {
return <h1>Hello, {this.state.name}</h1>;
}
}
虽然class也可以不添加状态,但想要使一个函数组件具有状态,不得不将其转换成class组件。
直观来看,好像造成这种差异是因为在class里,我们能通过this保存和访问“状态(state)”,而函数组件在其作用域内难以维持“状态(state)”,因为再次函数运行会重置其作用域内部变量,这种差异导致了我们“不得不”使用class至今。
看来如何解决函数组件保存state的成了移除class这种“难以理解”的关键。
2. 那Hook是如何保留组件状态的?这就是我看见Hook API产生的第一个疑问。其实在React里,这并不是问题,熟悉React Fiber的同学应该知道,事实上state是保存到Fiber上的属性memoizedState上的,而并不算是class的this.state上。那状态问题就迎刃而解了,如果函数组件同样访问Fiber上的memoizedState属性,就可以解决这个问题。
基于Fiber架构,解决这个问题非常容易,将memoizedState看作一个普通的变量,那么Hook的原理就容易理解和实现了。
在文章[译] 理解 React Hooks中提到
记住,在 Hooks 的实现中也没有什么“魔术”。就像 Jamie 指出的那样,它像极了这个:
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
let hooks = null;
export function useHook() {
hooks.push(hookData);
}
function reactsInternalRenderAComponentMethod(component) {
hooks = [];
component();
let hooksForThisComponent = hooks;
hooks = null
}
如Fiber一样,React实际上使用链表代替了数组这种数据结构,依次执行Hook,有兴趣的同学可以去看下React源码。
可是,class目前也能良好的支撑业务迭代,到底有什么动力去重新学习Hooks?
3. 为什么我们需要Hooks?针对这个问题,React文档提到了下面三点:
  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class
其实我觉得第三点就是来凑数的,毕竟React推出至今一直用着class,再难用各位也都会了,会者不难难者不会嘛(反正对于刚入前端坑那时候的我来说,没有啥是容易的)。
那就回答下一个问题,目前基于class实现的生命周期函数,是否真的会造成逻辑难以复用?
答案是NO
无论高阶组件或是render props,都提供了很好的方式来达到聚合业务逻辑的目的,业务逻辑并不会被生命周期“分割”。
那到底是哪里引入了复杂度?熟悉套娃的同学...呸
熟悉Ajax、Promise的等异步API的同学可能还记得“回调地狱”。类似的,高阶函数、render props等也极容易造成“嵌套地狱”,结合装饰器、函数式的compose等嵌套起来才是真的爽...一直嵌套一直爽...
但是,无论是什么地狱肯定是不好的,那一起来看最后一个问题。
复杂组件变得难以理解
之所以回避前两个问题,是因为我个人认为,无论是class还是HOC,它们都很好的解决了它们需要解决的问题,虽然生命周期函数将很多业务逻辑拆分的七零八碎,但是HOC却依旧能把它们集合在一起,仅考虑保留生命周期而言,就像Function-based一样(这是后话)。
所以我们换一个思路不难发现,真正的问题是在于它们在抽象业务逻辑的时候貌似引入了不必要的概念,才使得逻辑复用困难和难以理解。
这些概念导致了过多的嵌套,加深了组件层级,层级之间互相牵扯,就像我现在兜里的耳机线一样。
Hook独特之处在于化繁为简。
真正繁琐的是层级与层级之间的关系,我将借用React文档关于自定义Hook的例子说明这个问题
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import React, { useState, useEffect } from 'react';
// 通过friendID订阅好友状态
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
通过useFriendStatus这个自定义的hook,我可以非常轻松的在下面两个组件中实现逻辑的复用
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// FriendStatus获取好友状态
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
// FriendListItem获取好友状态
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
};
可是对于熟悉高阶组件的同学来说(不熟悉的同学请看高阶组件)依旧可以轻松的提取一个名叫useFriendStatus的高阶组件
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function useFriendStatus(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
render() {
return <WrappedComponent isOnline={isOnline} />;
}
}
}

然后,分别套上FriendStatus和FriendListItem依旧可以非常完美的多处复用此逻辑。
需要注意的是,高阶组件等概念还是有很多特点和优点的,hooks并不是万能的,hooks只是hooks而已
需要提到的是,官方文档在这里对比了新旧方案代码量的长度和复杂度,但其实在特定的业务情况下,确实不可避免会出现这种问题,个人认为这是次要矛盾(既然HOC已经封装了复杂度,还纠结里面长不长、复不复杂干啥)。
反而,这容易让刚接触Hooks的同学忽略Hook最大的亮点,先看下面这段熟悉的代码(不熟悉的同学看Promise 对象和async 函数)。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
// async
const P1 = new Promise(Something)
const P2 = new Promise(Something)
export default async function () {
const res1 = await P1;
//do Something
const res2 = await P2;
// ...
return res2
}
和刚才的Hook对比
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
// 以及 hook
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
...
return isOnline ? 'Online' : 'Offline';
}
像不像...就问你像不像...(模仿团队某成员说话)
写(粘)了这么多代码,简单来说,Hook解决的就是“嵌套地狱”的问题,正如async解决“回调地狱”一样。它们都做到了将原来不同“维度”的代码封装到了同一维度,以达到更直观、透明的将“计算结果”传递下去的目的。
而class不得不借助高阶组件等等概念,解决代码复用等问题,但是由于引入额外的概念(函数)反而使得代码更加复杂,现在的class难以解决这个问题,所以他就被抛弃了。
问题来了,谁可以不引入别的概念,完成逻辑封装?
就是函数本身啊!!还要class干嘛?!
综上,被抛弃和class生命周期函数导致的代码复杂度提升无关,Hook简化生命周期函数只是不过是举手之劳,并不是什么重要的特性。

原帖:https://juejin.cn/post/6844903871588859917