React Hook系列 useState 源码解析笔记

652 阅读7分钟

背景

工作快两年了,这个时候开始思考如何继续提升自己,keep going。既然如此那就一起来学习一下React的源码吧! 那就首先从工作中最常用的 useState 开始吧~

带着问题来学习

  1. 如何存储状态?
  2. 不同的hook是如何区分的?
  3. 如何更新状态,并返回最新状态?

let's go

首先要知道hooks出来之前,函数组件无法存储状态,一般是被动接收参数,然后渲染。hooks出来之后,便可以将状态存储起来,那么是如何存储的呢?

看看react源码中的定义: 方便理解这里只需要记住三个值 memoizedState(当前状态)、queue(更新队列)、next(下一个hook对象)

    export type Hook = {
      memoizedState: any,
      baseState: any,
      baseQueue: Update<any, any> | null,
      queue: any,
      next: Hook | null,
    };

第一个问题的答案:每一个hook执行之后都会生成一个hook对象。并且这些对象是用链表结构存储的,这也就是为什么hook不能写在if条件中(避免hooks链表顺序错误)

代码实现

首先我们先搭好函数的框架,大致分为以下几步:

  1. 获取(生成)hook 对象,并添加到链表尾部
  2. 更新 hook 对象中的状态
  3. 返回最新的状态和一个修改函数的函数,即 [state, setState]
function useState(initState) {
    /**
     * TODO
     * 1、获取hook对象
     */

    /**
     * TODO
     * 2、更新hook对象
     */


    /**
     * 3、返回
     */
    return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}

步骤拆分

1. 获取hook对象并添加到链表尾部
  • 首先需要区分是否是第一次加载,这里简单用一个变量值来表示是否是第一次加载
    如果首次加载,初始化hook对象
    不是首次加载,则在链表中找到这个hook节点
  • 然后将这个 hook 对象添加到链表尾部

到这里就会有疑问,首次添加结点时如何找到链表尾部? 非首次添加如何找到对应的hook结点?这里就要去理解链表结构与 workInProgressHook 指针的关系。
1. hooks 链表存储在 fiberNode 中
2. workInProgressHook 是一个指针,指向当前的链表中正在执行的 hook 对象
3. 当 workInProgressHook 为 null 时,代表当前在加载第一个 hook
4. hook在链表中严格按顺序加载,所以要找到当前 hook, 就要找上一个hook(workInProgressHook)

具体实现见代码与注释:

    let workInProgressHook = null;
    let currentFiberNode = {
        memorizedState: null
    };
    
    function useState(initState) {
    /**
     * 1、获取hook对象
     * 分支1:首次加载-> 初始hook对象
     * 分支2:触发更新-> 更新每个hook对象
     */
    // 如果是首次加载 为了方便理解这里用一个简单的值来代表首次加载,源码中不是这样的
    if (isFirstTimeMount) {
        let hook = {
            memorizedState: initState,
            next: null,
            queue: {
                pending: null,
            },
        }

        // workInProgressHook 是一个指针,指向当前的链表中最后一个 hook 对象
        // 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
        if (!workInProgressHook) {
            // currentFiberNode 代表当前的fiberNode节点 将链表的首个节点挂在 fiberNode 的 memorizedState 上
            currentFiberNode.memorizedState = hook;
            workInProgressHook = hook;
        } else {
            // 直接将新的 hook 对象添加到链表尾部, 并更新指针位置
            workInProgressHook.next = hook;
            workInProgressHook = workInProgressHook.next;
        }
    } else {
        // 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
        if (!workInProgressHook) {
            // currentFiberNode.memorizedState 链表首个节点
            hook = currentFiberNode.memorizedState; 
            workInProgressHook = hook;
        } else {
            // 获取hook,更新指针位置
            hook = workInProgressHook.next;
            workInProgressHook = hook.next;
        }
    }

    /**
     * TODO
     * 2、更新hook对象
     */


    /**
     * 3、返回
     */
    return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}
2. 更新hook对象
  • 首先需要思考什么时候会更新 hook 对象?然后发现当组件内调用 setState 函数(dispatchAction)时,就会进行更新,并且重新渲染组件。所以首先需要了解的是这个 dispatchAction 函数
  • 当 dispatchAction 执行完毕后,组件会重新渲染,就会再次调用 useState hook,此时就会去更新该 hook 的更新队列 queue。 queue是一个循环链表结构,queue.pending 总是指向最新的更新对象(update)。

这里就来看 dispatchAction 中做了什么:

  1. 生成一个 update 对象(每次调用都会生成一个update对象)
  2. 将 update 对象添加到循环链表中,并将指针 queue.pending 指向自己
  3. 组件渲染更新(调用 fiber 中的调度器,这里简化为执行 fiberSchedule 函数)
const dispatchAction = function(queue, action){
    // 每次更新都要生成一个 update 对象 同样该对象也是也链表形式存在,且是一个循环链表
    let update = {
        action,
        next: null,
    };

    // 判断是否是首次更新,首次更新,指向自己
    if (queue.pending === null) {
       update.next = update;
    } else {
        /**
         * 链表结构:
         *  update1 -> update2 -> update3 -> update1
         *  现在需要将update4插入循环链表中
         *  queue.pending指向最新的update 此例中指向update3,即可直接将update4指向update1(queue.pending.next)
         *  update3(queue.pending).next = update4
         */
        // update的下一个节点指向update1
        update.next = queue.pending.next ;
        queue.pending.next = update;
    }
    // 更新queue.pengding指向
    queue.pending = update;

    // 完成更新后,渲染组件,这里就进入了fiber的调度过程,这里简化为fiberSchedule()
    fiberSchedule()
}

当组件重新渲染更新时,又会执行 hook 函数。此时的流程则是:
通过 workInProgressHook 找到对应的 hook
找到 hook 后通过 queue (update循环链表)获取到最新的 update 对象
更新 hook.memoizedState 状态。
最后将 hook 的最新状态返回。

function useState(initState) {
    /**
     * 1、获取hook对象
     * 分支1:首次加载-> 初始hook对象
     * 分支2:触发更新-> 更新每个hook对象
     */
    // 如果是首次加载 为了方便理解这里用一个简单的值来代表首次加载,源码中不是这样的
    if (isFirstTimeMount) {
        let hook = {
            memorizedState: initState,
            next: null,
            queue: {
                pending: null,
            },
        }

        // workInProgressHook 是一个指针,指向当前的链表中最后一个 hook 对象
        // 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
        if (!workInProgressHook) {
            // currentFiberNode 代表当前的fiberNode节点 将链表的首个节点挂在 fiberNode 的 memorizedState 上
            currentFiberNode.memorizedState = hook;
            workInProgressHook = hook;
        } else {
            // 直接将新的 hook 对象添加到链表尾部, 并更新指针位置
            workInProgressHook.next = hook;
            workInProgressHook = workInProgressHook.next;
        }
    } else {
        // 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
        if (!workInProgressHook) {
            // currentFiberNode.memorizedState 链表首个节点
            hook = currentFiberNode.memorizedState; 
            workInProgressHook = hook;
        } else {
            // 获取hook,更新指针位置
            hook = workInProgressHook.next;
            workInProgressHook = hook.next;
        }
    }

    /**
     * 2、更新hook对象
     * 使用useState返回值是 [state, setState], 函数内部则是[hook.memorizedState, dispatchAction.bind(null, hook.queue)]
     * 外部调用setState(value) 则是调用 dispatchAction.bind(null, hook.queue)(value)
     * dispatchAction执行的时候,如何把value赋值给update对象?
     * react中使用了while循环来更新update对象
     */
    if(hook.queue.pending !== null){
        first = update = hook.queue.pending.next;
        do{
            action = update.action;
            hook.memoizedState = typeof action === 'function' ? action(hook.memoizedState) : action;
            update = update.next
        }while(update !== null && update !== first)
    }

    return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}

const dispatchAction = function(queue, action){
    // 每次更新都要生成一个 update 对象 同样该对象也是也链表形式存在,且是一个循环链表
    let update = {
        action,
        next: null,
    };

    // 判断是否是首次更新,首次更新,指向自己
    if (queue.pending === null) {
       update.next = update;
    } else {
        /**
         * 链表结构:
         *  update1 -> update2 -> update3 -> update1
         *  现在需要将update4插入循环链表中
         *  queue.pending指向最新的update 此例中指向update3,即可直接将update4指向update1(queue.pending.next)
         *  update3(queue.pending).next = update4
         */
        // update的下一个节点指向update1
        update.next = queue.pending.next ;
        queue.pending.next = update;
    }
    // 更新queue.pengding指向
    queue.pending = update;

    // 完成更新后,渲染组件,这里就进入了fiber的调度过程,这里简化为fiberSchedule()
    fiberSchedule()
}

至此开头的三个问题就都有了答案:
1、如何存储状态? 解答:所有的数据都存储在 hook 对象中,包括每一次更新产生的状态(update对象)。
2、如何区分不同的 hooks? 解答:hooks 通过链表存储在 fiberNode 中,不同 hook 生成的对象不同,并且严格按照顺序执行,hook 对象与组件中的执行 hooks 顺序一一对应。
3、如何更新状态,并返回最新状态? 解答:通过调用 useState 返回的 dispatchAction 函数生成 update 对象,此时会重新渲染组件,更新过程中会再次调用 hooks 函数,就会去获取当前 hook 对象的最新状态并返回。

最后还有一处疑问:既然 queue.pending 指向最新的 update 对象,为什么还要用 while 循环遍历链表的方式来获取最新状态?直接拿 queue.pending 不就可以了嘛?
有解释说是因为 fiber 更新中 如果有优先级高的任务先执行的话,可能上一次的更新任务还没有完成,所以使用遍历的形式来更新。