实现目标
以下是一个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到控制台代替显示numberincrease调用代替点击新增按钮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%…
文中如有错误或不严谨的地方,请给予指正,十分感谢。