React hooks 数据结构
代数效应
第一部分我们首先介绍一下react hooks践行的理念——代数效应,以及它与react hooks的联系 首先是定义:
把副作用从 函数 中剥离
下面请看一些例子
同步
如果我们想从一个函数同步地获取值,可以得到如下代码
function getTotalCommentNum(id1, id2) {
const num1 = getCommentNum(id1)
const num2 = getCommentNum(id2)
return num1 + num2
}
异步
如果一个函数的返回值是异步的,最简单的方法如下
async function getTotalCommentNum(id1, id2) {
const num1 = await getCommentNum(id1)
const num2 = await getCommentNum(id2)
return num1 + num2
}
缺点:async\await
具有传染性,调用函数也需要变成async函数
代数效应
假设,我们可以 虚构一个如下语法(协程)
function getCommentNum(id){
const num = perform id
return num
}
perform {
getTotalCommentNum(1, 2)
} handle(id) {
// switch(id) {
// case 1:
// resume with 111
// break
// case 2:
// resume with 222
// break
// default:
// resume with 0;
// }
// 异步(比如fetch)
fetch("xx").then(res => res.json()).then(({num}) => resume with num)
}
这样我们就可以顺利地把一些副作用从我们的函数中** 剥离 **出去
与react 关系
那么上面这段虚构的语法与react hooks有什么关系呢?
下面是我们常用的 用useEffect
获取数据的写法
function TotalCommentNum(id1, id2) {
const num1 = useCommentNum(id1)
const num2 = useCommentNum(id2)
return <div> { num1 + num2 } </div>
}
function useCommentNum(id){
const [num, setNum] = useState(0)
useEffect(() => {
fetch("xx").then(res => res.json()).then(({num}) => setNum(num))
}, [id])
return num
}
可以发现,我们利用useEffect
顺利地规避了async/await
的传染
所以,我们可以得到结论:react hooks 虽然不是真正的代数效应,但是其利用了某些特性践行了代数效应的思想
引申(server component)
根据上面我们获取数据的自定义hooks,我们发现,其实如果把组件放到服务端直接与接口、数据库交互,那么我们前后端代码都会变得非常组件化和统一,这样前端就得以数据、逻辑、ui真正的闭环了
hooks数据结构 实现
下面这一小节我将介绍react hooks 的基本数据结构和执行流程,并给出对应的简易代码实现
使用
首先,在忽略jsx的前提下,为了简化代码,我们的组件将会以如下方式定义
function App() {
const [num, setNum] = useState(0);
console.log('isMount?', isMount);
console.log('num:', num)
return {
onClick() {
setNum(num => num + 1)
setNum(num => num + 2)
setNum(num => num + 3)
}
}
}
它会返回一个包含setState 的对象
运行时环境
为了组件能运行,react定义了一些运行时环境,保存整个组件的状态
fiber节点
在react 中每个组件对应一个fiber node
const fiber = {
// 对应组件实例
stateNode: App,
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null
}
- stateNode: 对应的组件
- memoizedState: 对应组件的hooks(链表)
全局变量
let isMount = true
此变量用来控制hook的初始化(与真实react有区别,下文会介绍)
schdule调度函数
用来组织整个fiber的运行流程
function schedule() {
workInProgressHook = fiber.memoizedState;
const app = fiber.stateNode();
isMount = false;
return app;
}
- workInProgressHook:指向fiber的第一个hook节点,后文再详细解释
- app:对应的组件
- isMount:为了方便,我们用一个全局变量来区分是否为第一次渲染(mount),真实react如何处理后文会进行对比
调用组件
以下是我们如何对一个组件进行调用,入口即为schedule
函数
const app = schedule()
const clickMe = () =>{
app.onClick()
}
schedule函数会对我们的app组件进行渲染
顺序如下:
useState
首先useState在mount
和rerender
阶段需要分两种情况讨论
mount
初始化hook
let workInProgressHook = null
function useState(initialState) {
let hook;
if (isMount) {//首次加载组件, 新建对应状态hook
hook = {
memoizedState: initialState,// 状态
next: null, // 下一个hook
queue: { // 一个state的更新循环链表
pending: null
}
}
// 这里是更新链表的过程:新增节点
if (!fiber.memoizedState) { // fiber.hook1 -> hook2 -> hook3 ->.....
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook; // 让上一个hook指向当前的hook,更新workInProgressHook
}
/**
* fiber.hook1 -> hook2 -> hook3 ->.....
* ↑
* workInProgressHook
*/
workInProgressHook = hook; // workInProgressHook 记录链表最后一个 节点
}
这里需要解释的是,在第一次mount
阶段,我们要为每一个hook定一个数据结构,
- memoizedState: 改hook的状态
- next:下一个hook
- queue:
update
链
并借助新定义的
workInProgressHook
指针(总是指向最后一个hook节点),插入到前文提到的fiber.memoizedState
链表的末尾,最后产生的链表如下图所示
useState的返回
众所周知,useState返回一个数组,包含 state 和 dispatch函数,代码如下:
return [hook.memoizedState, dispatchAction.bind(null, hook.queue)]
在mount阶段,state是initState
render(主动触发dispatchAction)
setState(1)
dispatchAction
实质就是调用了dispatchAction这个函数
dispatchAction为一个被传入hook update 队列的函数:
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();
}
可以看到 dispatchAction
接受两个参数
- queue: hook节点的update队列
- action: state的更新函数
其中这个 queue 需要特别说明:
由于我们可能会同时调用多次setState
function conClick(){
setState(1)
setState(2)
setState(3)
}
所以我们同样需要将他们放入一个链表当中 而由于真实react 更新有优先级。比如:点击 > 请求。为了使用户交互更顺滑,环状链表更方便定位action,所以这边的update action会被放入一个环状链表中 如图所示:
- p: pending
- u: update
调用schedule
同上
再次渲染(fiber.stateNode)
此时的useState主要需要做两件事
- 确认当前调用的是哪个hook
- 根据之前状态,更新该hook的state
function useState(initialState) {
let hook;
if (isMount) {//首次加载组件, 新建对应状态hook
//
} else {
// 定位当前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 = typeof action === 'function' ?action(baseState) : action;
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next)
}
//更新当前hook的state
hook.memoizedState = baseState;
hook.queue.pending = null;
return [baseState, dispatchAction.bind(null, hook.queue)]
}
至此,一个hook的mount
和rerender
都已经闭环,可以完整的运行了
与真实react 的区别
reactFiberHooks
isMount
真实的hooks 区分mount不实用 isMount变量,而是各自定义一个不同状态的state(多态?)
useState
不同状态下useState不一样
mountState
updateState
包含跳出重入机制等等..
基于baseState 更新state
...200行
useReducer
useState === 预置useReducer的 useReducer
useEffect
mountEffect
updateEffect
useRef
mountRef
updateRef
useMemo
总结
问题
setState()
调用后,组件是怎么更新的?- 多个不同
hooks
是怎么区分,怎么按什么顺序执行的? - 不同的
setState()
被调用,是怎么对应到各自的state值的 useEffect
是在什么阶段执行的?
数据结构
const fiber = {
stateNode: App, // 组件函数本身
memoizedState: { // 指向第一个hook的链表
memoizedState: 0, // 此hook的状态
queue: { // 此hook的更新队列,是一个环状链表
pending: { // 指向最新的更新
action: someActionFunction, // 更新函数
next: null, // 在链表中指向下一个更新
},
},
next: { // 指向下一个hook的指针
memoizedState: null, // 下一个hook的状态
queue: { // 下一个hook的更新队列
pending: null, // 下一个hook的更新
},
next: null, // 如果没有更多的hooks了,那么这里是null
},
},
// Fiber节点还有其他属性,比如child、sibling、return等,用于构建Fiber树
};
拓展
- fiberHook如何实现优先级
- 如何合并多个更新
- commit函数的实现、调度原理
附录
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="clickMe()">点击 setNumber</button>
<button onclick="clickBlur()">点击 setName</button>
<script src="./main.js">
</script>
</body>
</html>
// main.js
const updateState = (hook) => {
let baseState = hook.memoizedState;
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = typeof action === 'function' ?action(baseState) : action;
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next)
return baseState
}
const createUpdateQueue = (update, queue) => {
if (queue.pending === null) {
// 只有一个的时候 update1 -> update1
update.next = update;
} else {
/**
* 两个:update2 -> update1 -> update2 , queue.pending = update2
* 三个:update3 -> update1 -> update2 -> update3
* 假设有3个update,queue.pending对应最新那个update
* ↓ ← ← ← ← ← ← ↑
* queue.pending = update3 -> update1 -> update2
* ↑
* queue.penging
*/
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
}
let isMount = true; // 是否首次加载
let workInProgressHook = null; // 当前运行的hook
// 组件fiber的状态节点,这里全局
const fiber = {
// 指向App函数
stateNode: App,
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null
}
function useState(initialState) {
let hook;
if (isMount) {//首次加载组件, 新建对应状态hook
hook = {
memoizedState: initialState,// 状态
next: null, // 下一个hook
queue: { // 一个state的更新循环链表
pending: null
}
}
// 这里是更新链表的过程:新增节点
if (!fiber.memoizedState) { // fiber.hook1 -> hook2 -> hook3 ->.....
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook; // 让上一个hook指向当前的hook,更新workInProgressHook
}
/**
* fiber.hook1 -> hook2 -> hook3 ->.....
* ↑
* workInProgressHook
*/
workInProgressHook = hook; // workInProgressHook 记录链表最后一个 节点
} else { // 组件已mount
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
console.log("hook:", hook)
console.log("workInProgressHook:",workInProgressHook)
let baseState = hook.memoizedState;
if (hook.queue.pending) {
baseState = updateState(hook)
hook.queue.pending = null;
}
//更新当前hook的state
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)]
}
/**
*
* @param {Object} queue
* @param {Function|String|Object|Number|...} action 要更新的状态
*/
let timer = null
function dispatchAction(queue, action) {
// 待更新状态的链表节点
const update = {
action,
next: null
}
createUpdateQueue(update, queue)
// console.log("queue:",queue.pending)
// if(timer)return;
// timer = setTimeout(()=>{
// timer = null
// react 使用scheduleUpdateOnFiber 来更新
// 会有 任务合并、优先级高的任务先执行、可中断等优化,有兴趣可以看看
schedule();
// },100)
}
function schedule() {
workInProgressHook = fiber.memoizedState;
const app = fiber.stateNode();
isMount = false;
return app;
}
function App() {
const [num, setNum] = useState(0);
const [num22, setNum22] = useState(0);
const [name, setName] = useState('aaa');
console.log('isMount?', isMount);
console.log('num:', num)
// console.log('num22:', num22)
console.log('name:', name)
return {
onClick() {
setNum(num => num + 1)
setNum(num => num + 2)
setNum(num => num + 3)
// updateNum22(num => num + 10)
},
// onFocus() {
// setNum22(num => num + 10)
// },
onBlur(){
setName('leofhe')
}
}
}
const app = schedule()
const clickMe = () =>{
app.onClick()
}
const clickBlur = () =>{
app.onBlur()
}