React Hooks 是 React 16.8 以及之后版本的产物,React Hooks 就是一堆钩子函数,不同的钩子函数提供了不同的功能,React 通过这些钩子函数对函数型组件进行增强。Hooks 允许你在不编写 class 的情况下使用状态(state)和其他 React 特性。 你还可以构建自己的 Hooks, 跨组件共享可重用的有状态逻辑。
React 核心成员 Sebastian Markbåge (Hooks 的发明者)曾说:我们在 React 中做的就是践行代数效应。
代数效应是函数式编程的一个概念,它所解决的一部分问题只在纯函数式编程中才存在,是为了分离一些副作用。代数效应实际上是一个比较领先的理念(写这篇博客为止),这种感觉就像你在写回调去解决异步问题,突然有一天有一个人告诉你有一个叫 async/await 的东西!
有人看到这可能就会说了,我不关心什么破代数效应,不要跟我讲大道理,直接给老子上代码就行了≦(._.)≧
代数效应看起来像是很高深的原理,下面我们用一些虚构的伪代码来解释一下什么是代数效应。
假设我们有一个 getName 是根据 id 获取用户信息的函数,还有一个 makeFriends 函数用来处理 getName 返回的用户信息
function getName(id) {
// ... 获取用户数据
}
function makeFriends(id1, id2) {
const user1 = getName(id1)
const user2 = getName(id2)
return `${user1.name}和${user2.name}变成了好朋友`
}
现在我们将 getName 变成异步函数
async function getName(id) {
// ... 异步操作
}
async function makeFriends(id1, id2) {
const user1 = await getName(id1)
const user2 = await getName(id2)
return `${user1.name}和${user2.name}变成了好朋友`
}
可以看到,由于 getName 函数变成了异步函数,导致 makeFriends 也需要变成异步函数去获取 getName 返回的数据,getName 变成异步函数破坏了 makeFriends 函数的同步特性。但是其实以正常编程思想,我才不想关注 getName 是怎样的实现的,我只在乎 getName 返回的数据。
这其实就是 getName 变成异步函数导致的副作用。
我们尝试虚构一个类似 try…catch 的语法 —— try…handle 和两个操作符 perform、resume 去分离一下这样的副作用
function getName(id) {
const user = perform axios.get(id)
return user
}
function makeFriends(id1, id2) {
const user1 = getName(id1)
const user2 = getName(id2)
return `${user1.name}和${user2.name}变成了好朋友`
}
try {
makeFriends('9527', '9528')
} handle (user) {
if (user) {
resume with user
} else {
resume with {
name: '毛小星'
}
}
}
当 makeFriends 执行到 getName 方法的时候,会执行 perform 后面的语句,perform 跳出当时的执行栈,try…handle 会捕获 perform 执行的结果,这就是一个效应。这种语法看起来非常像 try…catch,但是一旦 catch 到了 Error,那么当前执行的这条逻辑就完成了,当前的调用栈就销毁了,那么我们能不能使用一种语法“回到”之前的执行逻辑中去呢?try…handle 让这种场景成为了可能,handle 捕获了 perform 的执行结果后,依然会捕获下一个 perform。
上面这段伪代码就是代数效应的思想,是不是很简单,其实代数效应就是将副作用从函数中分离,让函数变得更加纯粹一些,这也是函数式编程的核心思想。
React 16.8 中的 Hooks 就是在践行代数效应,像 useState、useReducer、useRef 等,我们不需要关注函数式组件中的 state 在 Hooks 中是如果保存的,React 会为我们处理。你可以把 useState 看做成是一个 perform State(),这个效应就被 React 处理了,这样我们就直接使用 useState 返回的 state,编写我们的业务逻辑即可。
下面我们看看 useState 的基本使用
在 16.8 之前的版本,函数型组件基本只负责展示数据,不负责状态的保存。useState 的出现就可以让函数型组件保存状态了,下面我们来看看 useState 是怎样的使用的
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
const userMap = new Map([
['9527', { name: '毛小星' }],
['9528', { name: '杨秘书' }],
])
const Friend = () => {
const [count, setCount] = useState(() => 0)
const [id1] = useState('9527')
const [id2] = useState('9528')
const [content, setContent] = useState('')
const getName = (_id) => {
return userMap.get(_id)
}
const makeFriend = (_id1, _id2) => {
const user1 = getName(_id1)
const user2 = getName(_id2)
const result = `${user1.name} 和 ${user2.name} 变成了好朋友`
setContent(result)
}
useEffect(() => {
makeFriend(id1, id2)
}, [id1, id2, count])
return (
<div>
<p>{count && content}</p>
<Button type="primary" onClick={() => setCount((_count) => _count + 1)}>increment</Button>
</div>
)
}
export default Friend
上面这段代码,是不是和 try…handle 非常像,有点那个味了,是不是?没错,useState 和 useEffect 的组合就是 React 践行代数效应的最好示例。
上面我们在讲代数效应的时候,我们说不用关心 useState 里面做了什么,我们只需要使用 useState 返回给我们的 state 即可。诶,但是我就是个好奇宝宝,我就想知道 useState 内部到底是怎样实现的。
下面我们自己实现一个简易版的 useState,来了解一下 Hooks 内部的基本原理。
我们先来准备一些基础代码,也就是我们要写的 useState 的架子,写代码之前,我们先来梳理一下我们需要干什么。
下面我们就通过上面的思路来把 hooks 的框架搭出来
let isMount = true // 是否渲染
let workInprogressHook = null // 当前处理 hook
// Fiber对象
const fiber = {
stateNode: App,
memoizedState: null, // 用链表去存储 hook
}
function useState (initialState) {
// todo 实现 useState
}
// 调度
function schedule() {
// 初始化 当前处理 hook
workInprogressHook = fiber.memoizedState
const app = fiber.stateNode()
isMount = false
return app
}
// 测试组件
// 为了简化流程,我们忽略 DOM 更新
function App () {
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)
return {
onClick() {
setNum(num => num + 1)
},
updateCount() {
setCount(count => count + 1)
}
}
}
// 将调度挂载到 window 对象上,方便测试点击效果
window.app = schedule()
上面我们在 useState 函数中留了一个 todo项,在实现 useState 函数之前,我们先来思考一个问题,我们应该怎样存储 useState 生成的状态呢,通常我们调用 useState 是像下面这样的
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)
在 React 源码中,React 是通过链表结构来存储这些 hook 的,我们要把所有的 state 通过链表的形式存储,并且我们要将 workInprogressHook 指向当前 hook 方便我们处理,下面我们来试着实现 useState
function useState(initialState) {
let hook // 当前 hook 节点
if (typeof initialState === 'function') {
initialState = initialState();
}
if (isMount) {
hook = {
memoizedState: initialState,
next: null,
}
// 创建 hook 链表
// 如果没有初始化的 hook 则初始化 hook 节点,并将当前处理节点(workInprogressHook)指向当前 hook
// 如果不是初始化的话,则将 当前处理节点(workInprogressHook)的下一个节点指向 hook
if (!fiber.memoizedState) {
fiber.memoizedState = hook
} else {
workInprogressHook.next = hook
}
workInprogressHook = hook
}
// todo 实现更新逻辑
}
在完善 useState 的更新逻辑,我们先来想想,既然 state 是需要用链表来存储的,那么update 函数也得需要对应一个链表来存储啊,我们来看看为什么需要链表来存储
const [count, setCount] = useState(0)
return (
<p onClick={() => {
setCount(num => num + 1)
setCount(num => num + 1)
setCount(num => num + 1)
}}>
{num}
</p>
)
可以看到更新函数 setCount 可能不是只调用一次,在 React 中,这些 update 函数被环状链表组合在了一起。这时我们就需要在 hook 上增加一个 queue 属性来存储 update 函数
hook = {
memoizedState: initialState,
next: null,
// 保存改变的状态
// 队列是因为 有可能有多个更新函数
// setCount(num => num + 1)
// setCount(num => num + 1)
// setCount(num => num + 1)
queue: {
pending: null,
}
}
在 React 源码中,更新阶段会调用 dispatchAction.bind(null, hook.queue) 这个函数来更新 state,我们先来看看是怎样实现的
function dispatchAction(queue, action) {
// 更新节点
const update = {
action,
next: null,
}
// 构建更新链表 环状链表
// queue.pending === null 还没有触发更新,创建第一个更新
if (queue.pending === null) {
// u0 -> u0 -> u0
update.next = update
} else {
// u0 -> u0
// u1 -> u0 -> u1
update.next = queue.pending.next
queue.pending.next = update
}
queue.pending = update
// 触发更新
schedule()
}
环状链表的操作可能不太容易理解,下面我们来详细讲解下。
queue.pending = u0 ---> u0
^ |
| |
---------
queue.pending = u1 ---> u0
^ |
| |
---------
这样做的好处就是,当我们需要遍历 update 时,queue.pending.next指向第一个插入的update,方便我们去操作 update 函数。逻辑还是比较清晰明了的,如果上面看不懂的话,需要去好好补一下数据结构了哦。
在 dispatchAction 中,我们将 update 构建成环状链表后, 接着我们就可以继续实现 useState 中的更新逻辑,当我们需要更新 state 时,我们就需要遍历环状链表,将新的状态更新到 update 函数中去,当遍历完,我们将链表清空,最后我们将新的 state 和 update 函数返回即可。
function useState(initialState) {
let hook // 当前 hook 节点
if (typeof initialState === 'function') {
initialState = initialState();
}
if (isMount) {
... mount 阶段
} else {
// 如果是 update 的情况,则将 hook 指向 workInprogressHook
// workInprogressHook 指向 hook 链表的下一个节点
hook = workInprogressHook
workInprogressHook = workInprogressHook.next
}
// 处理更新 遍历更新函数的环状链表
// 获取初始状态
let baseState = hook.memoizedState
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next
do {
const action = firstUpdate.action
// 处理更新状态
baseState = action(baseState)
firstUpdate = firstUpdate.next
} while (firstUpdate !== hook.queue.pending.next) // 遍历完环状链表
// 清空链表
hook.queue.pending = null
}
hook.memoizedState = baseState
return [baseState, dispatchAction.bind(null, hook.queue)]
}
上面我们实现一个简单的 useState,我们使用 isMount 来判断更新时机,但是 React 中没有这么 low,React 中使用了不同的 hash 值来标识不同的 hooks 的状态
本篇博客 React 源码为 16.12.0 版本
// 利用 hash 来存储不同状态的方法
// mount 阶段
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
};
// update 阶段
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
};
Redux 的作者 Dan Abramov 在加入 React 团队中也是将 Redux 的思想带入了 React 中,useState 和 useReducer 这两个 hook 就是他的代表作,而且从本质来说,useState 不过就是预置了 reducer 的 useReducer,下面的源码会印证这点。
在 mount 阶段,useState 会调用 mountState, 而 useReducer 则会调用 mountReducer
下面我们来看看这两个方法
// \react\packages\react-reconciler\src\ReactFiberHooks.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建hook对象
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
能看到 mountState 和 mountReducer 的区别就是 queue 中 lastRenderedReducer 字段
const queue = (hook.queue = {
// 与极简实现中的同名字段意义相同,保存update对象
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 上一次render时使用的reducer
lastRenderedReducer: reducer,
// 上一次render时的state
lastRenderedState: (initialState: any),
});
mountReducer 的 lastRenderedReducer 接收的就是传入你自定义的 reducer;而 mountState 接收的 lastRenderedReducer 是一个预置的 basicStateReducer。
下面我们来看看 basicStateReducer 的实现
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
这也直接证明了 useState 即 reducer 为 basicStateReducer 的 useReducer。
在 update 阶段 updateState 则是直接调用了 updateReducer 方法,更加证明了 useState 就是特殊的 useReducer
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
下面我们来看看 updateReducer 是怎样实现的
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取当前hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// ...计算 newState 的过程
hook.memoizedState = newState;
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
源码这个部分比较长,我只保留了一些主干代码,大致流程就是重新计算新的 state,然后将新的 state 返回。
我们在使用 setCount((count) => count + 1) 这样的更新函数更新 state 的时候,会触发 dispatchAction 函数,这个时候当前的函数组件对应的 Fiber 和 对应的更新方法(hook.queue)就通过调用 dispatchAction.bind 传入了方法内
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 构建 update 环状链表
// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
currentlyRenderingFiber.expirationTime = renderExpirationTime;
} else {
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
// 只保留核心代码
// ...优化调度渲染
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
}
// 调度
scheduleWork(fiber, expirationTime);
}
}
dispatchAction 函数我只留了一些主干代码,总结一下:将 update 加入 queue.pending,构建环状链表,在优化渲染后,开启调度。
if…else… 是 React 的一些优化手段,if 内:
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
)
这是需要 render 阶段触发的更新,所以需要给当前的更新放到一个延迟队列中,在渲染阶段,再重新启用 workInProgress 去触发更新
而下面的 else…if
else if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
)
fiber.expirationTime 保存的是 fiber 对象的 update的优先级,fiber.expirationTime === NoWork 则意味着 fiber 对象上不存在 update。
通过源码的学习,我们已经知道了 update 计算 state 是在 hook 的声明阶段,在调用阶段还通过内置的 reducer 重新计算 state,如果调用阶段的 state 和声明阶段的 state 是相等的,那么就完全不需要重新开启一次新的调度了。
到此我们就了解了 hooks 的理念,其实 React 就是在践行函数式编程,如果你觉得这篇“人类高质量文章”写的不错就点个赞吧!