You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 非常单纯的函数,就是创建一个新的 Hook 对象,然后添加到链表中functionmountWorkInProgressHook(): Hook{// 创建一个 Hook 对象consthook: Hook={memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,// 链表的 next 指针,指向下一个 Hook};if(workInProgressHook===null){// 如果这是当次渲染的第一个 Hook,那么将链表的头指向这个 Hook// This is the first hook in the listcurrentlyRenderingFiber.memoizedState=workInProgressHook=hook;}else{// 如果这不是当次渲染的第一个 Hook,那么在链表的尾部添加这个 Hook// Append to the end of the listworkInProgressHook=workInProgressHook.next=hook;}returnworkInProgressHook;}
functionupdateReducer<S,I,A>(reducer: (S,A)=>S,initialArg: I,init?: I=>S,): [S,Dispatch<A>]{consthook=updateWorkInProgressHook();// 关键函数 获取 workInProgressHook 对象,updateEffectImpl 中也会调用这个函数constqueue=hook.queue;queue.lastRenderedReducer=reducer;// 注意,useState 和 useReducer 的最大区别就在于 lastRenderedReducer 的不同constcurrent: Hook=(currentHook: any);// 获取 baseQueue 和 pendingQueue,进行链表拼接// The last rebase update that is NOT part of the base state.letbaseQueue=current.baseQueue;// The last pending update that hasn't been processed yet.constpendingQueue=queue.pending;if(pendingQueue!==null){// We have new updates that haven't been processed yet.// We'll add them to the base queue.if(baseQueue!==null){// Merge the pending queue and the base queue.constbaseFirst=baseQueue.next;constpendingFirst=pendingQueue.next;baseQueue.next=pendingFirst;pendingQueue.next=baseFirst;// 合并 baseQueue 和 pendingQueue,baseQueue 在前,pendingQueue 在后// pendingQueue.next 指向环形链表的头节点// baseQueue.next 指向 pendingQueue 的头节点}// 状态计算if(baseQueue!==null){// We have a queue to process.constfirst=baseQueue.next;// 此时也就指向了 baseQueue 的头节点letnewState=current.baseState;letnewBaseState=null;letnewBaseQueueFirst=null;letnewBaseQueueLast: Update<S,A>|null=null;letupdate=first;do{// An extra OffscreenLane bit is added to updates that were made to// a hidden tree, so that we can distinguish them from updates that were// already there when the tree was hidden.constupdateLane=removeLanes(update.lane,OffscreenLane);// 从 update.lane 中移除 offscreenlane 的那一 bitconstisHiddenUpdate=updateLane!==update.lane;// 如果 !== 成立,说明 update.lane 中包含了 offscreenlane 的那一 bit// Check if this update was made while the tree was hidden. If so, then// it's not a "base" update and we should disregard the extra base lanes// that were added to renderLanes when we entered the Offscreen tree.constshouldSkipUpdate=isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(),updateLane)
: !isSubsetOfLanes(renderLanes,updateLane);if(shouldSkipUpdate){// 如果这个 update 对象的优先级不够,则先不执行,而是加入到下一次 render 的 Hook.baseQueue 中// 因此,Hook.baseQueue 存之前的优先级不够的 update 对象// Queue.pending 存当次 render 新创建的 update 对象// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.constclone: Update<S,A>={lane: updateLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if(newBaseQueueLast===null){newBaseQueueFirst=newBaseQueueLast=clone;newBaseState=newState;}else{newBaseQueueLast=newBaseQueueLast.next=clone;}// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes=mergeLanes(currentlyRenderingFiber.lanes,updateLane,);markSkippedUpdateLanes(updateLane);}else{// This update does have sufficient priority.if(newBaseQueueLast!==null){constclone: Update<S,A>={// This update is going to be committed so we never want uncommit// it. Using NoLane works because 0 is a subset of all bitmasks, so// this will never be skipped by the check above.lane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast=newBaseQueueLast.next=clone;}// Process this update.constaction=update.action;if(shouldDoubleInvokeUserFnsInHooksDEV){reducer(newState,action);}if(update.hasEagerState){// 一个性能优化,如果存在 eagerState,直接使用 eagerState,不需要再计算一次 reducer 函数了// 在 dispatchSetState 中可能标记 update.hasEagerState 为 true// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState=((update.eagerState: any): S);}else{// 调用 reducer 函数,计算新的状态newState=reducer(newState,action);}}update=update.next;// update 指针往后推移,执行下一个 update}while(update!==null&&update!==first);// 更新属性if(newBaseQueueLast===null){newBaseState=newState;}else{newBaseQueueLast.next=(newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if(!is(newState,hook.memoizedState)){markWorkInProgressReceivedUpdate();}// 把计算之后的结果更新到workInProgressHook上hook.memoizedState=newState;// 计算完所有的高优先级任务后得到的 statehook.baseState=newBaseState;// 直到遇到第一个低优先级任务前计算得到的 statehook.baseQueue=newBaseQueueLast;queue.lastRenderedState=newState;// 因此 queue.lastRenderedState 记录最新的 state}if(baseQueue===null){// `queue.lanes` is used for entangling transitions. We can set it back to// zero once the queue is empty.queue.lanes=NoLanes;}constdispatch: Dispatch<A>=(queue.dispatch: any);return[hook.memoizedState,dispatch];// 返回的是 [最新的状态,原本的 dispatch 函数]}
functionupdateWorkInProgressHook(): Hook{// 首先,这个函数在 update 或者 re-render 的时候被触发,因此我们希望根据之前构建的// current hook 或者 work-in-progress hook,来 clone 或者作为基础创建一个新的 hook 链表// 由于新的一次更新的时候入口应该是 renderWithHooks 函数,在里面有 currentlyRenderingFiber = workInProgressFiber// 那么 currentlyRenderingFiber.alternate 应该就是目前渲染在页面中的 current fiber// current.memoizedState 也就是上一次的 Hook 链表// 1. 移动 currentHook 指针letnextCurrentHook: null|Hook;if(currentHook===null){constcurrent=currentlyRenderingFiber.alternate;// currentlyRenderingFiber 是 WIP 的 fiber,其 alternate 指向的是目前页面渲染了的 fiberif(current!==null){nextCurrentHook=current.memoizedState;// 此时 nextCurrentHook 指向上一次渲染的第一个 Hook}else{nextCurrentHook=null;}}else{nextCurrentHook=currentHook.next;}// 2. 移动 workInProgressHook 指针letnextWorkInProgressHook: null|Hook;if(workInProgressHook===null){// 注意,在 mount 阶段,workInProgressHook 总是指向 hook 链表的尾部nextWorkInProgressHook=currentlyRenderingFiber.memoizedState;}else{nextWorkInProgressHook=workInProgressHook.next;}if(nextWorkInProgressHook!==null){//! 如果 nextWorkInProgressHook 不为 null,说明这应该是一次 re-render?// 我们要直接复用之前的 Hook 链表// There's already a work-in-progress. Reuse it.workInProgressHook=nextWorkInProgressHook;nextWorkInProgressHook=workInProgressHook.next;currentHook=nextCurrentHook;}else{// 如果 nextWorkInProgressHook 为 null,说明这是一个新的更新,那么我们需要 clone 之前的 Hook 链表// Clone from the current hook.if(nextCurrentHook===null){// 不应该出现这种情况 —— 出现了的话报错constcurrentFiber=currentlyRenderingFiber.alternate;if(currentFiber===null){// This is the initial render. This branch is reached when the component// suspends, resumes, then renders an additional hook.// Should never be reached because we should switch to the mount dispatcher first.thrownewError('Update hook called on initial render. This is likely a bug in React. Please file an issue.',);}else{// This is an update. We should always have a current hook.thrownewError('Rendered more hooks than during the previous render.');}}// clone currentHook 作为新的 workInProgressHook// 随后的逻辑和 mountWorkInProgressHook 一样,单纯的将 Hook 加入到链表中currentHook=nextCurrentHook;// 直接复制了 currentHook 指向的 Hook 对象。因此前一次 Hook 中的状态不会丢失constnewHook: Hook={memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null,};if(workInProgressHook===null){// This is the first hook in the list.currentlyRenderingFiber.memoizedState=workInProgressHook=newHook;}else{// Append to the end of the list.workInProgressHook=workInProgressHook.next=newHook;}}returnworkInProgressHook;}
React Hooks 原理 —— 状态 Hook
我们知道 React 组件最为常见的写法分为 Class 组件和 Function 组件。
在 Class 组件中,每一个组件都会被实例化成为一个 instance,通过实例化,其先天性的可以在多次渲染之间保留组件的状态变化。再后续的更新操作中,Class Component 只是调用
render
方法,因此实例中的信息不会丢失。然而 Function 组件中,其不会被实例化,每次渲染或者更新都是单纯的直接调用这个 Function,所以函数组件中没有办法保存 state 等信息。那么我们怎么样保存组件的局部状态呢?—— 这就需要依赖 Hooks 来实现了。
为什么需要 React Hooks?
详细的内容可以参考官方文档 https://zh-hans.legacy.reactjs.org/docs/hooks-intro.html
总结来说,React Hooks 有着以下的优点
componentDidUpdate
函数中useEffect
里为什么不建议使用 Class Component?
this
,bind
方法的使用导致更重的心智负担综上,React Hooks + Function Component 是经过大量实践后得出的维护性更强 + 性能更好 + 更加优雅的实践方案,是官方推荐我们使用的方案!
介绍
Hook 一般分为两个大类 —— 状态 Hook 和 副作用 Hook
useState
/useReducer
useEffect
/useLayoutEffect
本文中会先介绍状态 Hook 相关的源代码。再后续文章中会介绍副作用 Hook 相关的源代码。
在源代码中,执行 Function Component 的链路是
beginWork
→updateFunctionComponent
→renderWithHooks
renderWithHooks
这个函数主要做了三件事情
ReactFiberHooks.js
中的一些全局变量进行了初始化,包括currentlyRenderingFiber
属性,让我们记录目前正在处理的是哪一个 Fiber 节点useState
->mountState
(mount) |updateState
(update)*Component(props, secondArgs)
* 其中 Component 就是我们的这个函数式组件的被编译后的函数形式,在其中我们会调用 useState 等 HookmountState
对于
useState
来说,如果是在 mount 阶段被调用,就会走到mountState
函数hook.memoizedState
,hook.baseState
,hook.queue
,hook.dispatch
[当前状态,dispatch]
注意最后返回的格式是
return [hook.memoizedState, dispatch];
是不是与我们每次调用
const [count, setCount] = useState(0);
的写法非常相似!mountWorkInProgressHook
其实就是创建一个 Hook 对象,然后添加到一个全局的链表之中。无论是你调用
useState
,useReducer
,useEffect
, … ,这些函数的第一步永远都是调用mountWorkInProgressHook
因此,在一个 Function Component Fiber 中,所有你调用过的 React Hooks 函数,都会被转换成一个个 Hook 对象,然后以链表的形式结合起来。
mountReducer
useReducer
函数在 mount 阶段会走到mountReducer
,和上文的mountState
一对比,发现几乎没有区别!唯一的区别就是,在 mountState 中,
queue.lastRenderedReducer = basicStateReducer
, basicStateReducer 是一个内置的函数而在 mountReducer 中,
queue.lastRenderedReducer = reducer
, reducer 是用户输入的第一个参数因此,正如我们常说的,
useState
是一种特殊的且更加简单的useReducer
useState
可以转换成useReducer
至此,mount 阶段已经处理完了。让我们进入到 update 阶段。
例如我们有这么一段代码
const [count, setCount] = useState(0);
那么当我们调用
setCount(1)
的时候会发生什么呢?本质上是调用了dispatchSetState
函数(useReducer 的场景下调用的是dispatchReducerAction
函数)dispatchSetState
dispatchSetState 函数最主要的是创建一个 Update 对象,然后决定是否发起新的一轮的调度更新
update.eagerState
中beginWork
→updateFunctionComponent
→renderWithHooks
。只不过在renderWithHooks
中,当我们运行 Function 的时候,useState
会被解析成updateState
;useReducer
会被解析成updateReducer
dispatchReducerAction
dispatchReducerAction 和前文的 dispatchSetState 几乎一样,只是没有增加 eagerState 的性能优化 —— 因此,在某些场景下,useState 可能可以取得更好的性能
为什么 dispatchReducerAction 不增加关于 eagerState 的性能优化呢?因为 React 无法控制用户是如何声明自定义的 reducer function 的。
我们判断 eagerState 的时候,其实会有两种情况
dispatch(100)
,是更新到一个固定的值,那么我们直接比较 state 更新前后的 value 即可dispatch(oldState => oldState + 100)
,传递的是一个 function,那么我们需要确保在 dispatchSetState 中,我们的状态更新函数实际上就是
basicStateReducer
,这是一个内置的函数,React 可以保障这个 function 本身是不变的。但是当用户自定义 reducer 的时候,事情就会变的不确定起来
在上面 App1 的写法中,是满足上述的两个要求的
然而在 App2 的写法中,由于每一次 App2 function 都会重新执行,因此每次生成的 function 都是一个新的 function,所以是不满足 eagerState 的条件的
因此,对于 useReducer 来说命中优化的不确定性是很高的!而这也导致了一些衍生的 Bug,由于暂时没有很好的处理方案,因此 React 团队先去除了 useReducer 中关于 eagerState 的性能优化
(https://juejin.cn/post/7220223630667726907)
updateState
嗯…就是调用
updateReducer
updateReducer
这是一个非常长的代码,但是其实可以划分成为三个部分
const hook = updateWorkInProgressHook();
如前文所说,每一个 React Hooks 函数都对应了一个 Hook 对象,updateWorkInProgressHook
则是帮助我们获取这个目标 Hook 对象步骤 1 我们将在后面一节讲解
步骤 2 只是单纯的链表操作
步骤 3 的逻辑我们可以看到非常的复杂… React 为了支持并发执行,并不是一口气就执行完所有的 Update 对象的,它会根据每一个 Update 对象的优先级,先执行其中的一部分,然后再后续的 render 中再执行剩下的部分。
具体而言,假设我们有 update1, update2, update3, update4, update5 五个 Update 对象,其中 1 2 5 是高优先级的任务,3 4 是低优先级任务
那么这里的逻辑是
Hook.memoizedState = newState
,Hook.baseState = newBaseState
,Hook.baseQueue = baseQueue
之所以要这样设计,是因为一方面我们希望高优先级任务尽早执行,所以在计算中间状态的时候,memoizedState 是经历了所有的高优先级任务的计算。
但是另一方面,我们希望我们按照 update 注册的顺序来执行,避免出现异常结果,所以在计算最终状态的时候,我们是沿着 newBaseState 按照 update 注册的顺序来执行的。
updateWorkInProgressHook
这个函数会沿着 Hook 链表访问我们的 Hook 对象,针对每一个 Hook 对象,会创建一个 newHook 来复制 currentHook 的属性。通过观察这个函数,解答了许多关于使用 React Hooks 的疑惑
updateWorkInProgressHook
,于是我们的指针每一次都会沿着链表移动The text was updated successfully, but these errors were encountered: