实现目标
以下是一个useState
的简单使用demo。两个按钮点击触发setNumber
函数,修改number
值。然后组件render
,重新计算number
的新值,用于展示。
export const PageDemo = () => {
const [number, setNumber] = useState(0)
return (
<div>
<div>{number}</div>
<button onClick={() => setNumber(number + 1)}>+</button>
<button onClick={() => setNumber(number - 1)}>-</button>
</div>
)
}
代数效应
在实现我们自己的useState
之前,我们需要了解一个思想 —— 代数效应
函数中的副作用
假设,我们有一个获取用户存款的函数getUserMoney
,还有一个获取多个用户存款的函数getTotalMoney
。代码如下:
// 同步
function getUserMoney(name) {
const picNum = doSomeSyncThing();
return picNum;
}
function getTotalMoney(useList) {
return useList.reduce((totalMoney, user) => {
const money = getUserMoney(user) ?? 0;
return totalMoney + money
}, 0)
}
const total = getTotalMoney(['ZS', 'LS'])
// 异步
async function getUserMoney(name) {
const picNum = await doSomeAsyncThing();
return picNum;
}
async function getTotalMoney(user1, user2) {
return useList.reduce((totalMoney, user) => {
const money = await getUserMoney(user) ?? 0;
return totalMoney + moeny
}, 0)
}
之前的代码都是同步的情况下获取用户的存款。突然有一天需求变更了,需要变成调用后台接口来获取用户的存款。那么我们的getUserMoney
函数需要变成异步的,需要给getUserMoney
和getTotalMoney
裹上async/await
。但是异步是会传染的,相应的getTotalMoney
函数也会需要使用async/await
。
这样我们的代码就被迫全面修改了,那我们有办法避免这种修改吗。怎样我们才能在需要异步获取用户存款的时候,还能继续写同步的代码呢。
这里我们就需要引入代数效应这个思想。
代数效应的伪代码示例
function getNumber(name) {
const picNum = perform name;
return picNum;
}
function getTotalNumber(user1, user2) {
return useList.reduce((totalMoney, user) => {
const money = getUserMoney(user) ?? 0;
return totalMoney + money
}, 0)
}
try {
getTotalNumber('kaSong', 'xiaoMing');
} handle (who) {
switch (who) {
case 'kaSong':
resume with 230;
case 'xiaoMing':
resume with 122;
default:
resume with 0;
}
}
我们假设有一个resume、handle、perform
语法。以下步骤简单的解释代数效应伪代码的执行流程:
- 代码执行到
getNumber
函数中,遇上了perform
会像try/catch
一样跳出当前的函数调用栈 - 找到最近的
handle
模块,执行handle
模块中的代码。 handle
中代码执行完毕之后,会跳转回到perform
跳出的位置,继续执行后续的代码。
注意:
perform handle resume
是我们虚拟出的伪代码,并不是如今js中存在的语法
实现属于自己的 mini useState
简单分析
useState
接受一个初始值,作为state
的初始值useState
返回一个数组,数组的第一项是state
的当前值,第二项是一个记录修改state
动作的函数- 记录修改
state
动作的函数执行后,需要重新render
组件 hook
函数允许多个使用,但是他们之间是彼此独立互不影响的,如下图:
实现代码
App组件
App
是我们模拟的react
组件,我使用三个函数代替生产的html
元素的点击和显示效果。
showState
输出state
到控制台代替显示number
increase
调用代替点击新增按钮decrease
调用代替点击减少按钮
// 简单模拟jsx模块
const App = () => {
const [number, setNumber] = useMiniState(0)
console.log('isMount ->', isMount)
console.log('number ->', number)
return {
showState: () => {
console.log('number ->', number)
},
increase: () => {
setNumber(state => state + 1)
},
decrease: () => {
setNumber(number - 1)
}
}
}
虚拟节点
在打包时候,react
会去解析jsx
语法,通过createElement
函数生成一个个的虚拟节点。这里我们不深入去实现虚拟dom
的实现过程,暂时使用一个全局变量代替createElement
生成的虚拟dom
。
let fiber = {
node: App,
memorizedState: null,
}
调度函数
我们需要实现一个简单的调度函数,重新执行App
函数计算useMiniState
的返回值,重新生成showState、increase、decrease
三个函数。
- 我们需要把当前的工作
hook
指针指向当前App
节点里面的第一个hook
函数 - 重新执行
App
函数,即App
组件的render
- 如果是第一次挂载节点,需要把挂载节点标识置为
false
- 返回
App
函数的结果
let isMount = true
let workInProgressHook = null
const schedule = () => {
workInProgressHook = fiber.memorizedState
const node = fiber.node()
if (isMount) {
isMount = false
}
return node
}
useMiniState
接下来我们需要去实现useMiniState
函数,我们之前分析过useState
实现的四个点,这里一步一步来实现。首先是hook
用来存放数据的对象。
hook函数存放数据的对象
首先我们需要建立一个对象来装载当前hook
的一些参数。
useMiniState
函数的当前state
需要被记录,所以我们给这个对象添加一个属性memorizedStateValue
。- 但是当我们在一个组件中多次使用
useMiniState
函数创建多个状态的时候,我们该如何获取下一个useMiniState
的state
值呢?答案当然是链表,于是我们把多个hook
函数的数据存在一条链表中。给hook
对象添加一个next
属性,指向下一个hook
。下图是我们现在暂时得到的hook
对象:
记录hook动作的函数
我们需要写一个记录修改state
动作的函数,给这个函数取名dispatchAction
。
- 这个函数需要记录下对
state
修改的每次动作,为后续的计算state
新值提供基础。很自然我们有了dispatchAction
函数的第一个参数,修改state
的动作,即action
。 - 那么我们这些
action
需要存放在什么地方呢?没错就是我们刚刚新建的hook
对象,我们给hook
对象再添加一个属性queue
,现在hook
对象如下图所示。这里有人肯定会问了,为什么queue
是一个对象,里面的value
才是一个数组。这是因为我们需要把queue
传递到dispatchAction
函数中,再dispatchAction
中修改queue
。所以我们需要一个引用传递。
- 现在我们的
dispatchAction
函数有了接受参数了,queue
和action
。我们只需要把action
添加在queue
队尾。然后重新触发调度函数就行,于是我们得到了如下图所示的dispatchAction
函数
计算state的新值
目前我们只能在hook
对象中得到旧值,即memorizedStateValue
。我们还需要通过hook
对象中记录的queue
来计算state
的新值。
queue
中记录的action
有两种
action
是一个基础值action
是一个函数,接收旧的state
,来计算新的state
所以我们需要对action
进行一个判断,如果是函数执行,如果是基础值就直接记录下来。等到queue
都遍历好了,我们state
的新值也就得到了。接下来我们只需要返回新的state
和dispatchAction
函数就可以了
以下是useMiniState
的完整代码:
const dispatchAction = (queue, action) => {
queue.value.push(action)
window.app = schedule()
}
const useMiniState = (initState) => {
let hook
if (isMount) {
hook = {
memorizedStateValue: initState,
next: null,
queue: {
value: []
}
}
if (!fiber.memorizedState) {
fiber.memorizedState = hook
} else {
workInProgressHook.next = hook
}
workInProgressHook = hook
} else {
hook = workInProgressHook
workInProgressHook = workInProgressHook.next
}
let oldStateValue = hook.memorizedStateValue
if (hook.queue.value?.length > 0) {
for (const action of hook.queue.value) {
if(typeof action === 'function'){
oldStateValue = action(oldStateValue)
} else {
oldStateValue = action
}
}
hook.queue.value = []
hook.memorizedStateValue = oldStateValue
}
return [hook.memorizedStateValue, dispatchAction.bind(null, hook.queue)]
}
感谢
最后要感谢一下《React技术揭秘》的作者卡哥,这本书对我的react源码学习起到了很大帮助。 《React技术揭秘》:react.iamkasong.com/#%E5%AF%BC%…
文中如有错误或不严谨的地方,请给予指正,十分感谢。