当谈论 React hook,我们究竟说的是什么?
这个标题很大,但是落点很小,只是我,一个开发者在学习和使用 hooks 中的一点感受和总结。
React hook 的由来
在组件之间复用状态逻辑很难 复杂组件变得难以理解 难以理解的 class
难以琢磨的 this 关联的逻辑被拆分 熟练记忆众多的生命周期,在合适的生命周期里做适当的事情 代码量相对更多,尤其是写简单组件时
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this); // 要手动绑定this
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus( // 订阅和取消订阅逻辑的分散
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() { // 要熟练记忆并使用各种生命周期,在适当的生命周期里做适当的事情
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
React hook 的实现
function App () {
const [num, setNum] = useState(0);
const [age, setAge] = useState(18);
const clickNum = () => {
setNum(num => num + 1);
// setNum(num => num + 1); // 是可能调用多次的
}
const clickAage = () => {
setNum(age => age + 3);
// setNum(num => num + 1); // 是可能调用多次的
}
return <div>
<button onClick={clickNum}>num: {num}</button>
<button onClick={clickAage}>age:{age}</button>
</div>
}
function App () {
const [num, setNum] = useState(0);
const [age, setAge] = useState(10);
console.log(isMount ? '初次渲染' : '更新');
console.log('num:', num);
console.log('age:', age);
const clickNum = () => {
setNum(num => num + 1);
// setNum(num => num + 1); // 是可能调用多次的
}
const clickAge = () => {
setAge(age => age + 3);
// setNum(num => num + 1); // 是可能调用多次的
}
return {
clickNum,
clickAge
}
}
之所以能保持住state,是在一个函数组件之外的地方,保存了一个「对象」,这个对象里记录了之前的状态。
// 组件是分初次渲染和后续更新的,那么就需要一个东西来判断这两个不同阶段,简单起见,我们是使用这个变量好了。
let isMount = true; // 最开始肯定是true
// 我们在组件中,经常是使用多个useState的,那么需要一个变量,来记录我们当前实在处理那个hook。
let workInProgressHook = null; // 指向当前正在处理的那个hook
// 针对App这个组件,我们需要一种数据结构来记录App内所使用的hook都有哪些,以及记录App函数本身。这种结构我们就命名为fiber
const fiber = {
stateNode: App, // 对函组件来说,stateNode就是函数本身
memorizedState: null // 链表结构。用来记录App里所使用的hook的。
}
// 使用 setNum是会更新组件的, 那么我们也需要一种可以更新组件的方法。这个方法就叫做 schedule
function schedule () {
// 每次执行更新组件时,都需要从头开始执行各个useState,而fiber.memorizedState记录着链表的起点。即workInProgressHook重置为hook链表的起点
workInProgressHook = fiber.memorizedState;
// 执行 App()
const app = fiber.stateNode();
// 执行完 App函数了,意味着初次渲染已经结束了,这时候标志位该改变了。
isMount = false;
return app;
}
useState 究竟怎么保持住之前的状态的? 如果多次调用 setNum 这类更新状态的函数,该怎么处理这些函数呢? 如果这个 useState 执行完了,怎么知道下一个 hook 该去哪里找呢?
// 计算新状态,返回改变状态的方法
function useState(initialState) {
// 声明一个hook对象,hook对象里将有三个属性,分别用来记录一些东西,这些东西跟我们上述的三个疑问相关
// 1. memorizedState, 记录着state的初始状态 (疑问1相关)
// 2. queue, queue.pending 也是个链表,像上面所说,setNum是可能被调用多次的,这里的链表,就是记录这些setNum。 (疑问2相关)
// 3. next, 链表结构,表示在App函数中所使用的下一个useState (疑问3相关)
let hook;
if (isMount) {
// 首次渲染,也就是第一次进入到本useState内部,每一个useState对应一个自己的hook对象,所以这时候本useState还没有自己的的hook数据结构,创建一个
hook = {
memorizedState: initialState,
queue: {
pending: null // 此时还是null的,当我们以后调用setNum时,这里才会被改变
},
next: null
}
// 虽然现在是在首次渲染阶段,但是,却不一定是进入的第一个useState,需要判断
if (!fiber.memorizedState) {
// 这时候才是首次渲染的第一个useState. 将当前hook赋值给fiber.memorizedState
fiber.memorizedState = hook;
} else {
// 首次渲染进入的第2、3、4...N 个useState
// 前面我们提到过,workInProgressHook的用处是,记录当前正在处理的hook (即useState),当进入第N(N>1)个useState时,workInProgressHook已经存在了,并且指向了上一个hook
// 这时候我们需要把本hook,添加到这个链表的结尾
workInProgressHook.next = hook;
}
// workInProgressHook指向当前的hook
workInProgressHook = hook;
} else {
// 非首次渲染的更新阶段
// 只要不是首次渲染,workInProgressHook所在的这条记录hook顺序的链表肯定已经建立好了。而且 fiber.memorizedState 记录着这条链表的起点。
// 组件更新,也就是至少经历了一次schedule方法,在schedule方法里,有两个步骤:
// 1. workInProgressHook = fiber.memorizedState,将workInProgressHook置为hook链表的起点。初次渲染阶段建立好了hook链表,所以更新时,workInProgressHook肯定是存在的
// 2. 执行App函数,意味着App函数里所有的hook也会被重新执行一遍
hook = workInProgressHook; // 更新阶段此时的hook,是初次渲染时已经建立好的hook,取出来即可。 所以,这就是为什么不能在条件语句中使用React hook。
// 将workInProgressHook往后移动一位,下次进来时的workInProgressHook就是下一个当前的hook
workInProgressHook = workInProgressHook.next;
}
// 上述都是在建立、操作hook链表,useState还要处理state。
let state = hook.memorizedState; // 可能是传参的初始值,也可能是记录的上一个状态值。新的状态,都是在上一个状态的基础上处理的。
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next; // hook.queue.pending是个环装链表,记录着多次调用setNum的顺序,并且指向着链表的最后一个,那么hook.queue.pending.next就指向了第一个
do {
const action = firstUpdate.action;
state = action(state); // 所以,多次调用setNum,state是这么被计算出来的
firstUpdate.next = firstUpdate.next
} while (firstUpdate !== hook.queue.pending.next) // 一直处理action,直到回到环状链表第一位,说明已经完全处理了
hook.queue.pending = null;
}
hook.memorizedState = state; // 这就是useState能保持住过去的state的原因
return [state, dispatchAction.bind(null, hook.queue)]
}
建立 hook 的链表。将所有使用过的 hook 有序连接在一起,并通过移动指针,使链表里记录的 hook 和当前真正被处理的 hook 能够一一对应。 处理 state。在上一个 state 的基础上,通过 hook.queue.pending 链表来不断调用 action 函数,直到计算出最新的 state。
function dispatchAction(queue, action) {
// 每次dispatchAction触发的更新,都是用一个update对象来表述
const update = {
action,
next: null // 记录多次调用该dispatchAction的顺序的链表
}
if (queue.pending === null) {
// 说明此时,是这个hook的第一次调用dispatchAction
// 建立一个环状链表
update.next = update;
} else {
// 非第一调用dispatchAction
// 将当前的update的下一个update指向queue.pending.next
update.next = queue.pending.next;
// 将当前update添加到queue.pending链表的最后一位
queue.pending.next = update;
}
queue.pending = update; // 把每次dispatchAction 都把update赋值给queue.pending, queue.pending会在下一次dispatchAction中被使用,用来代表上一个update,从而建立起链表
// 每次dispatchAction都触发更新
schedule();
}
上面这段代码里,7 -18 行不太好理解,我来简单解释一下。
假设我们调用了 3 次setNum函数,产生了 3 个 update, A、B、C。
当产生第一个 update A 时:
A:此时 queue.pending === null,
执行 update.next = update, 即 A.next = A;
然后 queue.pending = A;
建立 A -> A 的环状链表
建立 B -> A -> B 的环状链表
建立起 C -> A -> B -> C 环状链表
let isMount = true;
let workInProgressHook = null;
const fiber = {
stateNode: App,
memorizedState: null
}
function schedule () {
workInProgressHook = fiber.memorizedState;
const app = fiber.stateNode();
isMount = false;
return app;
}
function useState(initialState) {
let hook;
if (isMount) {
hook = {
memorizedState: initialState,
queue: {
pending: null
},
next: null
}
if (!fiber.memorizedState) {
fiber.memorizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let state = hook.memorizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next
do {
const action = firstUpdate.action;
state = action(state);
firstUpdate.next = firstUpdate.next
} while (firstUpdate !== hook.queue.pending.next)
hook.queue.pending = null;
}
hook.memorizedState = state;
return [state, dispatchAction.bind(null, hook.queue)]
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
}
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
schedule();
}
function App () {
const [num, setNum] = useState(0);
const [age, setAge] = useState(10);
console.log(isMount ? '初次渲染' : '更新');
console.log('num:', num);
console.log('age:', age);
const clickNum = () => {
setNum(num => num + 1);
// setNum(num => num + 1); // 是可能调用多次的
}
const clickAge = () => {
setAge(age => age + 3);
// setNum(num => num + 1); // 是可能调用多次的
}
return {
clickNum,
clickAge
}
}
window.App = schedule();
由于我们是每次更新都调用了 schedule,所以 hook.queue.pending只要存在就会被执行,然后将 hook.queue.pending = null, 所以在我们的简略版 useState 里,queue.pending 所建立的环状链表没有被使用到。而在真实的 React 中,batchedUpdates会将多次 dispatchAction执行完后,再触发一次更新。这时候就需要环状链表了。
useState 究竟怎么保持住之前的状态?
如果我多次调用 setNum 这类 dispatch 函数,该怎么处理这些函数呢?
如果这个 useState 执行完了,下一个 hook 该去哪里找呢?
React hook 的理念
class Box extends React.components {
componentDidMount () {
// fetch data
}
componentWillReceiveProps (props, nextProps) {
if (nextProps.id !== props.id) {
// this.setState
}
}
}
function Box () {
useEffect(() => {
// fetch data
}, [])
useEffect(() => {
// setState
}, [id])
}
function App() {
const [count, setCount] = useState(0)
const handleWindowResize = () => {
// 把count输出
console.log(`count is ${count}`)
}
useEffect(() => {
// 让resize事件触发handleResize
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+</button>
<h1>{count}</h1>
</div>
);
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.handleWindowResize = this.handleWindowResize.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleWindowResize() {
console.log(`count is ${this.state.count}`);
}
handleClick() {
this.setState({
count: this.state.count + 1
});
}
componentDidMount() {
window.addEventListener("resize", this.handleWindowResize);
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleWindowResize)
}
render() {
const { count } = this.state;
return (
<div className="App">
<button onClick={this.handleClick}>+</button>
<h1>{count}</h1>
</div>
);
}
}
在组件之间复用状态逻辑很难 复杂组件变得难以理解 难以理解的 class
React hook 的意义
import React from "react";
function Count({ count, add, minus }) {
return (
<div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<div>You clicked {count} times</div>
<button
onClick={add}
title={"add"}
style={{ minHeight: 20, minWidth: 100 }}
>
+1
</button>
<button
onClick={minus}
title={"minus"}
style={{ minHeight: 20, minWidth: 100 }}
>
-1
</button>
</div>
);
}
const countNumber = (initNumber) => (WrappedComponent) =>
class CountNumber extends React.Component {
state = { count: initNumber };
add = () => this.setState({ count: this.state.count + 1 });
minus = () => this.setState({ count: this.state.count - 1 });
render() {
return (
<WrappedComponent
{...this.props}
count={this.state.count}
add={this.add.bind(this)}
minus={this.minus.bind(this)}
/>
);
}
};
export default countNumber(0)(Count);
因为我们想让子组件重新渲染的方式有限,要么高阶组件 setState,要么 forceUpdate,而这类方法都是 React 组件内的,无法独立于 React 组件使用,所以add\minus 这种业务逻辑和展示的 UI 逻辑,不得不粘合在一起。 使用 HOC 时,我们往往是多个 HOC 嵌套使用的。而 HOC 遵循透传与自身无关的 props 的约定,导致最终到达我们的组件时,有太多与组件并不太相关的 props,调试也相当复杂。我们没有一种很好的方法来解决多层 HOC 嵌套所带来的麻烦。
// 业务逻辑拆分到这里了
import { useState } from "react";
function useCounter() {
const [count, setCount] = useState(0);
const add = () => setCount((count) => count + 1);
const minus = () => setCount((count) => count - 1);
return {
count,
add,
minus
};
}
export default useCounter;
// 纯UI展示组件
import React from "react";
import useCounter from "./counterHook";
function Count() {
const { count, add, minus } = useCounter();
return (
<div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<div>You clicked {count} times</div>
<button
onClick={add}
title={"add"}
style={{ minHeight: 20, minWidth: 100 }}
>
+1
</button>
<button
onClick={minus}
title={"minus"}
style={{ minHeight: 20, minWidth: 100 }}
>
-1
</button>
</div>
);
}
export default Count;
function Count() {
const { count, add, minus } = useCounter();
const { loading } = useLoading();
return loading ? (
<div>loading...please wait...</div>
) : (
<div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
...
</div>
);
}
export default Count;
可以设置计数器的初始值、每次加减值、最大值最小值、精度 可以通过返回的方法,直接获得超出最大最小值时按钮变灰无法点击等等效果。 可以通过返回的方法,直接获取中间输入框只能输入数字,不能输入文字等等功能。
function HookUsage() {
const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } =
useNumberInput({
step: 0.01,
defaultValue: 1.53,
min: 1,
max: 6,
precision: 2,
})
const inc = getIncrementButtonProps()
const dec = getDecrementButtonProps()
const input = getInputProps()
return (
<HStack maxW='320px'>
<Button {...inc}>+</Button>
<Input {...input} />
<Button {...dec}>-</Button>
</HStack>
)
}
Table,
Thead,
Tbody,
Tfoot,
Tr,
Th,
Td,
TableCaption,
TableContainer,
React hook 的局限
被强制的顺序
复杂的useEffct
function App () {
let varibaleCannotReRender; // 普通变量,改变它并不会触发组件重新渲染
useEffect(() => {
// some code
}, [varibaleCannotReRender])
// 比如在一次点击事件中改变了varibaleCannotReRender
varibaleCannotReRender = '123'
}
function App() {
const [num, setNum] = useState(0);
let b = 1;
useEffect(() => {
console.log('effefct', b);
}, [b]);
const click = () => {
b = Math.random();
set((num) => num + 1);
};
return <div onClick={click}>App {get}</div>;
}
函数的纯粹性
// 把这种
function YourComponent () {
const [num, setNum] = useState(0);
return <span>{num}</span>
}
// 理解成这种形式,使用了useState,React就自动给你生成AutoContainer包裹你的函数。这样你的组件仍可以看成是纯函数。
function AutoContainer () {
const [num, setNum] = useState(0);
return <YourComponent num={num} />
}
function YourComponent (props) {
return <span>{props.num}</span>
}
写在最后
参考文档:
快 来 找 又 小 拍
推 荐 阅 读 设为星标
更新不错过
设为星标
更新不错过
[广告]赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
关注KnowSafe微信公众号
随时掌握互联网精彩
随时掌握互联网精彩
赞助链接