react源码学习-实现篇-hooks

极简hooks实现

通过下面demo可以知道react中hooks的执行流程。

极简hooks实现
通过demo我们知道,mount的时候,每个hooks会创建一个hooks对象,而hooks的状态就存放在hooks对象上,多个hooks对象以链表形式存放在fiber节点上,每次hooks状态更新,会生成update。update会放在hooks.queue.pending上,形成环状链表。重新执行函数组件,即重新执行如useState函数,这时候是update,从fiber上获取hooks对象,会获取hooks对象上存放的update链表,然后进行计算,返回最新的state。接下来看看react中hooks的执行过程。

Hooks是什么?

在真实的hooks中,组件mount的hooks和Update时的hook来源于不同的对象,这类对象在源码中称为dispatcher

在functionComponent执行render阶段的时候

在这里插入图片描述

执行updateFunctionComponent。

在这里插入图片描述

主要是执行函数组件,获取返回的vdom,然后根据vdom创建子fiber或者update的时候更新props加上effectTag属性。

重点看这个renderWithHooks

看第一部分:判断当前是mount还是update,赋值ReactCurrentDispatcher,也就是我们使用的hooks。然后执行函数组件。

在这里插入图片描述

在这里插入图片描述

mount和update使用的hooks并不一样。

然后执行函数组件的时候,里面如果有比如useState,就会执行这些函数。

第二部分:
在这里插入图片描述

执行完函数组件后,又重新赋值了ReactCurrentDispatcher
在这里插入图片描述

可以看到,都是抛出错误的。因为函数组件执行完后,如果还有一些副作用又调用了hooks,就会报错。比如useEffect中调用了useState。

useEffect调度的时候,是正常的dispatcher,然后函数组件执行完毕,ReactCurrentDispatcher又重新赋值了,等useEffect调度完毕执行的时候,useState已经是throwInvalidHookError,就会报错。这也是为什么hooks里面不能调用hooks,因为hooks只能在函数组件里面直接调用。
在这里插入图片描述

Hooks的数据结构

通过极简hooks的demo知道,每个hooks执行的时候,会创建一个hooks的对象。拿useState为例子,在mount的时候,useState执行mountState

在这里插入图片描述

在这里插入图片描述
每个函数组件的hooks那么多怎么保证每次更新的时候都能准确获取?

  • 当当前函数组件是第一个有hooks的组件的时候,他会将hook对象挂在到fiber.memoizedState。
  • 下一个组件执行的时候,创建的hook对象会跟在后面,

创建hook,然后通过全局变量workInProgressHook指向当前的hook。hooks的数据结构如图,

  • baseQueue: 存放着跳过的update链表,指向尾部。而且这里跳过的update链表是一个闭环链表。

  • baseState: 本次更新前的state。update基于这个state计算更新后的state

  • queue: 存放着更新的action。

  • next: 指针,通过他将hooks对象连接成链表,存放在fiber.memoizedState上。

  • memoizedState: hooks和函数fiber都有该属性,而fiber.memoizedState是保存该fiber上的hooks链表,而hook.memoizedState,是保存当前hook的state。

不同的hooks,memoizedState保存不同的值,如useState,useReducer保存state的值,

useEffect保存useEffect的回调函数和依赖项等的链表结构effect,effect链表会同时保存在fiber.updateQueue中。函数组件的update跟其他组件的不同,函数组件的Update存放在hooks上,不放在fiber.updateQueue中。

useRef保存{current:null}, useMemo,useCallback保存[clllback(),deps]和[callback, deps]

而有些hooks是没有memoizedState的,比如useContext

useState和useReducer的实现

在这里插入图片描述

在这里插入图片描述

mount

函数组件在执行之前,会赋值给ReactCurrentDispatcher.current,那么执行的时候useState就可以获取到dispacher.usestate。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,useState和useReducer的区别,一个reducer存放着basicStateReducer,一个存放着传入的reducer。useState是特殊的useReducer。

看queue的结构,

  // 创建queue,类似于updateQueue,只不过存放在hooks上。
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    // 与极简实现中的同名字段意义相同,保存环状update对象
    pending: null, //这里的update链表依然是环状链表,并且永远指向最后一个。
    interleaved: null,
    lanes: NoLanes,
     // 保存dispatchAction.bind()的值 也就是setState
    dispatch: null,
     // 上一次render时使用的reducer
    lastRenderedReducer: basicStateReducer,
    // 上一次render时的state
    lastRenderedState: (initialState: any),
  };

如图,函数组件产生的状态更新的update是存放在hooks对象的queue上的,而hooks对象以链表的形式存放在fiber.memoizedState上。

类组件和HostComponent的update是存放在updateQeueu的shard.pending上的,updateQueue直接存放在fiber上。

一个fiber有一个updateQueue,一个updateQueue上可以有很多个update对象,而一个fiber可以有很多hooks,一个hooks上的queue可以有很多针对当前hooks的update对象。

这就是useState和useReducer第一次执行的时候做的事情,创建hooks,创建queue,dispatch,初始化state返回。

update的时候

这两者mount的时候有区别,但是update的时候都是调用同一个函数了。

在这里插入图片描述

在这里插入图片描述

useState只是自己传入了一个reducer。接着看这里的逻辑,他的逻辑跟处理类组件的update更新状态差不多。

可以看到状态的更新是在函数组件第二次执行的时候才会计算state的。

在这里插入图片描述

第一步,取出上次跳过的update链表和这次的update链表连接起来,注意,这里跳过的链表也是一个环状链表,而baseQueue永远指向最后一个。存储reducer,可以看到update执行的useReducer每次都会更新reducer的值,如果useReducer的第一个参数reducer有变化,那么计算state的reducer也会对应变化。

然后取出所有update,并清空hooks上的update链表。
在这里插入图片描述

这里的逻辑很熟悉,跳过的优先级,他的newBaseState表示当前hooks使用的state,对比hooks.baseState,而newState会赋值给memoizedState,他表示的是最新的state。因为如果有update跳过的缘故,那么baseState !== memoizedState。之前已经举过例子。

在这里插入图片描述

如果有跳过的update,那么newBaseState永远指向上一个baseState,而newState最新的state则会赋值给memoizedState。

这里处理的逻辑跟render阶段beginwork处理update(processUpdate)很类似。
在这里插入图片描述

最后返回的是最新的state,但是如果有跳过的update,下次更新的时候,处理update的时候是以baseState为基础的,而不是memoizedState。

有个需要注意的点:mount和update获取当前hooks对象的方法是不一样的。

在这里插入图片描述

在这里插入图片描述

这是因为,在mount的是欧,可以确定是调用ReactDOM.render或者相关的初始化的API产生的更新,只会执行一次函数组件。

update的时候,可能是事件回调或者副作用中触发的更新,或者是render阶段产生的更新,为了避免组件的无限次循环,后者需要区别对待。

function App() {
  const [num, updateNum] = useState(0);
  
  updateNum(num + 1);

  return (
    <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
  )
}

updateNum会触发一次更新,如果不做处理,那么在执行函数组件的时候又会触发一次更新,导致死循环。

基于这个原因,React用一个标记变量didScheduleRenderPhaseUpdate判断是否是render阶段触发的更新。

updateWorkInProgressHook方法也会区分这两种情况来获取对应hook

现在我们知道了hooks执行的时候发生了什么事情。mount的时候,初始化state,创建hooks对象。update的时候,从hooks对象获取update,计算最新的state返回。

调用阶段

现在讲讲,useState返回的第二个参数,setState的调用。

在这里插入图片描述

创建update,插入queue,开始调度。

在这里插入图片描述

这里插入update,会判断是否是由render阶段产生的更新。
在这里插入图片描述

是的话,会标记变量。

还有一个需要注意的点:

在这里插入图片描述

fiber.lanes保存update的优先级,fiber.lanes ===Nolanes表示fiber上不存在update。我们知道函数组件state的计算是在触发更新=>创建update=>调度=>render阶段=>renderWithHooks=>执行函数组件=>获取update链表=>计算。因为hooks上可能存在多个不同的优先级的update,最终state的计算需要多个update共同决定。而如果此时fiber没有update,那么此次更新创建的update就是第一个update,则在此时就直接计算state的值,不需要到函数执行的时候在计算。这样做的好处:如果新的state和老的state一样,那么不需要开启一轮新的调度,即使不一样,到函数执行的时候也可以直接获取计算好的state。

总结

为什么hooks只能在函数组件里面调用。
  • react的hooks存放在了ReactCurrentDispatcher上,当组件第一次执行的时候,即函数组件到达render阶段,执行beginwork的时候,他会调用updateFunctionComponent->renderWithHooks,其中,renderWithHooks,会根据当前组件状态,决定赋值HooksDispatcherOnMount还是HooksDispatcherOnUpdate,然后再执行函数组件!这里是先赋值,再执行。

  • 然后执行完函数组件后,又将ReactCurrentDispatcher赋值为会报错的ContextOnlyDispatcher,当函数组件执行完后,再调用hooks,就会报错,此时hooks已经是throwInvaildHookError了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值