React源码解析(五):hook原理

前言

基于前面几篇的解析,我们已经了解了 react渲染的主链路了,当然还有 commit阶段没有详细的解析,后面会补充。这一篇我们主要来看看,hook是实现更新组件状态的,当然除了状态hook,还有副作用hook,这个我们放到后面再继续介绍。

背景

在解析原理之前,我们先来思考一下,hook的引入是为了解决什么问题的,这一点很重要。

  1. 在原有的状态组件实现上,只能基于复杂的class组件来实现。
  2. 复用状态逻辑只能基于高阶组件和props传递,导致组件嵌套过深。

hooks使用

我们来看看 hook是如何使用的。

js 复制代码
import React, { useState } from 'react'
export default function hookState() {
    const [hookState, setHookState] = useState(0)
    return (
        <>
            <div>
                count: {hookState}
            </div>
            <button onClick={() => {
                setHookState(preState => {
                    return preState + 1
                })
            }}>
                点我
            </button>
        </>
    )
}

这样简单的方式就可以实现在函数组件中,保持状态了。

hook实现猜想

基于我们之前对react渲染主链路的探究,hook的实现无非要做两件事:

  1. 绑定到当前的函数组件对应的fiber,这样才能嵌入rendercommit主链路。
  2. 开启渲染调度。

我们再来看看上面的示例,好像也没有看到明显的绑定呀。setHookState开启渲染调度,我们可以大概联想到,因为在class组件中this.setState调用,也是合并当前组件的状态,然后调用scheduleUpdateOnFiber,开启调度。

js 复制代码
const [hookState, setHookState] = useState(0)

仅凭这行代码我们也没有看出来,它如何实现绑定的。接下来我们还是从源码调试入手,看看它是如何实现的。

hook实现

第一部分:fiber与hook关联

我们可以把上面的示例修改一下:

js 复制代码
import React, { useState } from 'react'

export default function hookState() {
    const [hookState, setHookState] = useState(() => {
        debugger
        return 0
    })
    return (
        <>
            <div>
                count: {hookState}
            </div>
            <button onClick={() => {
                setHookState(preState => {
                    return preState + 1
                })
            }}>
                点我
            </button>
        </>
    )
}

这样重启项目的时候我们就能得到这样的调用栈了:

beginWork是我们的老熟人了,我们关注它之后的哪些调用函数。

mountIndeterminateComponent

js 复制代码
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
    resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
    var props = workInProgress.pendingProps;
    var context;
    var value;
    var hasId;

    // 这一步会收集 hooks(比如 useState、useEffect 等)
    value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);

    workInProgress.flags |= PerformedWork;


    // 省略
     reconcileChildren(null, workInProgress, value, renderLanes);
     return workInProgress.child;
    }
  }

mountIndeterminateComponent做的事情比较多,因为这里我们只关注 hook这部分的逻辑。在我们前面了解到的beginWork阶段,主要将 react ele翻译为与之对应的fiber结构。我们重点关注 renderWithHooks的实现。

renderWithHooks

js 复制代码
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
    renderLanes = nextRenderLanes;
    currentlyRenderingFiber$1 = workInProgress; 
    //Tip:重要 - 当前正在构造的 fiber, 等同于 workInProgress

    // 清除当前fiber的遗留状态
    workInProgress.memoizedState = null;
    workInProgress.updateQueue = null;
    workInProgress.lanes = NoLanes;
    
    // 执行function函数, 其中进行分析Hooks的使用
    var children = Component(props, secondArg);
    // Check if there was a render phase update

    ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
    
    // 省略...

    // 重置全局变量,并返回
    // 执行function之后, 还原被修改的全局变量, 不影响下一次调用
    var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
    renderLanes = NoLanes;
    currentlyRenderingFiber$1 = null;
    currentHook = null;
    workInProgressHook = null;
    // currentHook 与 workInProgressHook: 
    // 分别指向 current.memoizedState 和 workInProgress.memoizedState

    didScheduleRenderPhaseUpdate = false; 
    // This is reset by checkDidRenderIdHook
    // localIdCounter = 0;

    return children;
  }

renderWithHooks大概做了三件事情:

  1. 维护全局变量,比如 currentlyRenderingFiber$1,这个也是后续 hook收集的中间桥梁。
  2. var children = Component(props, secondArg);,执行function函数, 其中进行分析Hooks的使用。
  3. 重置全局变量,比如 currentHookworkInProgressHook等等。

接下来我们看看在执行这行var children = Component(props, secondArg)代码时,发了什么。

useState

Component对应的就是我们的函数组件,当它调用时,就会执行useState()

js 复制代码
// react
function useState(initialState) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useState(initialState);
}
// react-dom
 useState: function (initialState) {
    currentHookNameInDev = 'useState';
    mountHookTypesDev();
    var prevDispatcher = ReactCurrentDispatcher$1.current;
    ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

    try {
      return mountState(initialState);
    } finally {
      ReactCurrentDispatcher$1.current = prevDispatcher;
    }
}

useStatereact暴露的api,但是内部却要关联到react-dom的运行时,resolveDispatcher()就是拿二者到联系纽带。

mountState

那么重点来了,mountStatefiberhook主要的关联实现。

js 复制代码
// useState 的返回
  function mountState(initialState) {
    // 先处理hook,获取当前的hook,在 mount 阶段
    var hook = mountWorkInProgressHook();

    if (typeof initialState === 'function') {
      // $FlowFixMe: Flow doesn't like mixed types
      initialState = initialState();
    }

    hook.memoizedState = hook.baseState = initialState;
    // hook queue,每个 useState 对应一个 hook 对象。
    // 产生的 update 保存在 useState 对应的 hook.queue中
    var queue = {
      pending: null,
      lanes: NoLanes,
      dispatch: null,
      lastRenderedReducer: basicStateReducer,
      lastRenderedState: initialState
    };
    hook.queue = queue;
    var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
  }

诶呀,但是这里好像没有二者关联的部分,函数的后半部分,主要构造queue队列,然后将初始的state和更新的dispatch以数组的结构返回。函数的前端半部分,我们看看。

mountWorkInProgressHook

js 复制代码
// 构造hook
  function mountWorkInProgressHook() {
    // hook定义 
    var hook = {
      memoizedState: null, // 当前状态: 保持在内存中的局部状态.
      baseState: null,     // 基状态: hook.baseQueue中所有update对象合并之后的状态.
      baseQueue: null,     // 基队列: 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象
      queue: null,         // 更新队列: 存储update对象的环形链表, 包括所有优先级的update对象.
      next: null           // next指针: next指针, 指向链表中的下一个hook.
    };

    // 将 hook 插入 fiber.memoizedState 链表末尾
    if (workInProgressHook === null) {
      // This is the first hook in the list
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
    } else {
      // Append to the end of the list
      // Tip: 
      workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
  }

currentlyRenderingFiber$1.memoizedState对应的就是fiber.memoizedState,这样我们就把hookfiber关联上了。这里还有workInProgressHook,主要用于hook state持久化。

第二部分:hook触发调度

那么当我们调用 setHookState是如何触发更新的呢,在前部分mountState中,我们关注到这行代码var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue)

dispatchSetState

js 复制代码
function dispatchSetState(fiber, queue, action) {

    var lane = requestUpdateLane(fiber);
    // hook update 对象
    var update = {
      lane: lane,
      action: action,
      hasEagerState: false,
      eagerState: null,
      next: null
    };

    // 什么条件下 ? render阶段触发的更新?
    // 为什么会render阶段触发更新?=> bad code
    if (isRenderPhaseUpdate(fiber)) {
      enqueueRenderPhaseUpdate(queue, update);
    } else {
      var alternate = fiber.alternate;
      // TODO: 
      var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

      if (root !== null) {
        var eventTime = requestEventTime();
        // 调度任务更新
        scheduleUpdateOnFiber(root, fiber, lane, eventTime);
        entangleTransitionUpdate(root, queue, lane);
      }
    }

    markUpdateInDevTools(fiber, lane);
  }

dispatchSetState做了这么几件事情: 1.创建一个 update,包含 laneaction(即 setState 的参数)、hasEagerState。 2.判断是否在 render 阶段触发更新,如果是的话,加入到hook.queue中;如果不是的话,加入到ConcurrentQueue中,然后调用scheduleUpdateOnFiber,启动调度更新。

总结

  1. state hook主要围绕如何关联当前fiber,以及后续触发更新,将状态融入到函数组件当中。
  2. 对于何时用全局上下文参数以及函数上下文参数有了更多的理解。
相关推荐
慧一居士24 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead26 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina7 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码8 小时前
1.
react.js·node.js·angular.js