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. 对于何时用全局上下文参数以及函数上下文参数有了更多的理解。
相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子3 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina3 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路4 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_4 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码4 小时前
1.
react.js·node.js·angular.js
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app