React | 2020-08-16 23:11:45 672次 2次
经过之前的文章,了解了大体的 react 运行流程,继续对 hooks 进行分析,它可以分为如下三种类型:
State hooks : useState、useReduce
Effect hooks : useEffect、useLayoutEffect
其他 hooks: useCallback、useMemo、useContext、useRef等
一、流程
在第一篇文章中介绍了 React.js 中部分的方法,同样的这里也暴露了 hooks 相关的 api,同样这里面不会做什么复杂逻辑,在 ReactHooks.js 中可以看到所有方法都是来自 ReactCurrentDispatcher 这个全局变量:
... export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; return dispatcher; } ...
这个变量会在 react-dom 包中的 ReactFiberHooks.js 文件中被赋值,主要是逻辑依然是依靠 react-dom 处理。
根据堆栈信息可以看到一个函数组件经过调度,最终在 beginWork 中执行了 renderWithHooks,也就是 hooks 执行的起点:
初始加载: ... --> beginWork --> mountIndeterminateComponent --> renderWithHooks --> reconcileChildren -->.. 更新: ... --> beginWork --> updateFunctionComponent --> renderWithHooks --> reconcileChildren --> ...
二、renderWithHooks
同样的在 renderWithHooks 中会对 ReactCurrentDispatcher.current 赋值,这不过这个赋值是根据各种条件有不同的值。
export function renderWithHooks(...) { // 这里注意 将wip赋值给了 currentlyRenderingFiber 变量,后面会全局使用 currentlyRenderingFiber = workInProgress; ... // 初始加载 | 更新 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; ... let children = Component(props, secondArg); // 检查直接在组件中调用 useState中的更新方法 避免无限loop if (workInProgress.expirationTime === renderExpirationTime) { let numberOfReRenders: number = 0; do { workInProgress.expirationTime = NoWork; // 证明无限调用 报错提示 invariant( numberOfReRenders < RE_RENDER_LIMIT, 'Too many re-renders. React limits the number of renders to prevent ' + 'an infinite loop.', ); // 这个值仅仅是内部计算最大迭代次数 和判断是否循环调用没有关系 numberOfReRenders += 1; ... } while (workInProgress.expirationTime === renderExpirationTime); } // 比如这种嵌套 useEffect(() => { useState(0);}) 抛出异常 ReactCurrentDispatcher.current = ContextOnlyDispatcher; ... return children; }
这里面涉及到的过程为:
初始加载:HooksDispatcherOnMount
更新 :HooksDispatcherOnUpdate
避免无限调用:
export default function Hooks() { const [num, updateNum] = useState(0); updateNum(1) return ( ... ) }
主要是通过 workInProgress.expirationTime 和 renderExpirationTime 来判断避免无限循环,这个值初始为 0,但是因为组件中初始加载时候立即执行了 updateNum,所以会触发 dispatchAction 方法中的一个判断,currentlyRenderingFiber 就是 wip,它有值就证明当前正在处于渲染的过程(最新的源码中直接通过 didScheduleRenderPhaseUpdate 来判断了,不再设置过期时间):
if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber) ) { // 标记 didScheduleRenderPhaseUpdate = true; // 将过期时间设置renderExpirationTime 上面比较如果相同则意味循环引用了 currentlyRenderingFiber.expirationTime = renderExpirationTime; }
在初始加载和更新中间还有一个触发更新的状态,当 hooks 方法调用的时候。
三、hooks 数据结构
hooks 的数据结构比较特殊,但总体思路还是链表存储,比如多个 useState 这种 hook 的保存,会存储在当前 fiber(当前组件)的 memoizedState 属性上,类组件数据的则是保存在当前实例。
其中每个 hook 上又有一套如下结构,进行数据的保存和更新状态的记录,一连串的 hooks 中包含各个 hook。
const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, };
凭借扎实的美术功底,通过强大的【画板】来描述这个神奇的 hooks 数据结构再适合不过了(查看大图):
// 对应示例 import React, { useState, useEffect } from 'react' export default function Hooks() { const [num, updateNum] = useState(0); const [num1, updateNum1] = useState(100); const [num2, updateNum2] = useState(1000); let setAdd = () => { updateNum(num+1) updateNum(num+2) updateNum(num+3) } useEffect(() => { console.log('load') }, [num1]) return ( <div onClick = {setAdd}> { num }, { num1 } </div> ) }
四、初始加载
无论初始加载或者是更新时,都会执行这些 hooks 方法,但是执行的处理是不一致的。
通过手动调用某个方法(useState | useReduce 的 dispatch)可以触发更新。
说明:初始加载、更新、调用这三个部分仅仅针对 useState | useReduce 展开描述,其他方法后面再看。
const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, useDebugValue: mountDebugValue, useResponder: createDeprecatedResponderListener, useDeferredValue: mountDeferredValue, useTransition: mountTransition, };
useState 方法初始加载时调用 mountState:
function mountState(initialState){ // useState 的链表创建,对应上图的 hook 那一列 const hook = mountWorkInProgressHook(); // 挂载 memoizedState 值 hook.memoizedState = hook.baseState = initialState; // basicStateReducer 中执行 action 这里暂时看不到目的 const queue = (hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }); const dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any)); // 返回的 api 执行调用更新就是触发 dispatch 方法 return [hook.memoizedState, dispatch]; }
首先通过 mountWorkInProgressHook 方法构建了上图中 hook 列中的数据结构,最终挂载到 currentlyRenderingFiber 变量(对应图最左侧的 fiber 节点),最终返回一个 dispatch 方法。
useReducer 方法初始加载时调用 mountReducer:
function mountReducer(reducer, initialArg){ ... const queue = (hook.queue = { ... lastRenderedReducer: reducer, ... }); ... }
可以看到这两个方法唯一的区别就是 queue 中 lastRenderedReducer 不一致,因为 useReducer 是直接接收一个 reducer 方法的,而 useState 则只是单纯的传入一个值,但是它上面挂载的是 basicStateReducer ,这个暂时忽略,一定是调用那里会进行处理:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { // $FlowFixMe: Flow doesn't like mixed types return typeof action === 'function' ? action(state) : action; }
五、调用
这个过程会触发更新,通过 hook 返回的第二个参数(dispatch),对应的是 dispatchAction:
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) { ... // 对应的 update结构 const update: Update<S, A> = { expirationTime, suspenseConfig, action, eagerReducer: null, eagerState: null, next: (null: any), }; //对应上图的最右侧 action 环 const pending = queue.pending; if (pending === null) { // 初始创建 自身构成一个环 update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; const alternate = fiber.alternate; if ( fiber === currentlyRenderingFiber ) { // currentlyRenderingFiber 如果和 fiber 全等 意味着render 阶段触发了更新update // renderWithHooks 中 var children = Component(props, secondArg); // 这里等于开始执行具体的函数组件,所以会阻塞 // 因为 renderWithHooks 中最后会将currentlyRenderingFiber置为null,这边如果产生更新一直占用,所以这么比较 // 这些变量会用于无限循环的调用 didScheduleRenderPhaseUpdate = true; update.expirationTime = renderExpirationTime; // 这个最上面提到过 通过这个时间判断 currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { if ( fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) ) { // 更新时,状态发现没有发生变化,is 判断,直接跳过渲染 // mount传入的 reducer const lastRenderedReducer = queue.lastRenderedReducer; if (lastRenderedReducer !== null) { let prevDispatcher; try { const currentState: S = (queue.lastRenderedState: any); const eagerState = lastRenderedReducer(currentState, action); // 更新时会用到 判断 update.eagerReducer === reducer // 更新时会用到 queue.lastRenderedReducer = reducer; update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { // 状态没有变化 终止 return; } } ... } } // 开始调度 scheduleWork(fiber, expirationTime); } }
其中对于过期时间的判断,来决定是否进行数据预计算以及判断状态是否发生改变,假如执行了三次更新,前两次的值没有发生改变(就是传入的值还是原值的情况),那么这两次都会直接终止,第三次的更新是一个新的值,则会再进入这个逻辑并进行数据的预处理,但是因为状态发生改变,此时才会执行调度,调度之后将过期时间就会发生改变,初始值为 0 === NoWork:
... let setAdd = () => { updateNum(num) ----> 终止 updateNum(num) ----> 终止 updateNum(num+3) ----> 调度,并且数据预处理好了 } ...
再换一种情况:
... let setAdd = () => { updateNum(num+1) ----> 调度,并且数据预处理好了 updateNum(num+2) ----> 调度,没有数据预处理 updateNum(num+3) ----> 调度,没有数据预处理 } ...
以上两种情况的示例主要是为了下一步【更新】做准备,因为那里面会通过 .eagerReducer 判断决定是否重新计算值或使用已经计算好的数据。
这个过程,其实做的事情很明了,分为四个步骤:
1) 创建更新链表,对应上图最右侧的 action 环链表
2) 判断是否在渲染阶段触发了更新操作,如果死循环则 wip(currentlyRenderingFiber) 一直无法重置,所以判断条件是 fiber === wip,同时赋值一些变量在 renderWithHooks 中会取检测
3) 优化操作,没有更新,状态没有发生变化直接终止,提供计算好的数据可以给到下一步(更细阶段直接使用)
4) 开始调度,回到之前的流程中
六、更新
更新阶段仍然会调用 useState | useReducer 等 hooks,这个过程中根据更新计算出新的状态。
const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useReducer: updateReducer, useRef: updateRef, useState: updateState, useDebugValue: updateDebugValue, useResponder: createDeprecatedResponderListener, useDeferredValue: updateDeferredValue, useTransition: updateTransition, };
useState 方法初始加载时调用 updateState:
function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { // 直接调用了 updateReducer, // 并且传入basicStateReducer 就是相当于帮我们创建了一个reducer return updateReducer(basicStateReducer, (initialState: any)); }
useReducer 方法初始加载时调用 updateReducer:
function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { // 这里和初始加载时候是不一样的 // 这个方法会从历史的 memoizedState 中(即图中最右侧的 fiber 节点中的属性)拿数据 // 并创建指向的过程和 mount 时一致 const hook = updateWorkInProgressHook(); // hook const queue = hook.queue; ... queue.lastRenderedReducer = reducer; // 每一次更新都会 currentHook.next 找到下一个 hook const current: Hook = (currentHook: any); let baseQueue = current.baseQueue; // 这个代表 某个hook中触发了更新链表 对应图中最右侧那个部分 let pendingQueue = queue.pending; if (pendingQueue !== null) { if (baseQueue !== null) { // 假设 baseQueue: b-1(最后一个,baseQueue引用他) -> b0 -> b1 -> b-1 // 假设 pendingQueue: p-1(最后一个,pendingQueue引用他) -> p0 -> p1 -> p-1 // 以下2行操作的意思是 // b-1 -> p0 // 则操作完成后形成 pendingQueue: p-1 -> b0 -> b1 -> b-1 -> p0 -> p1 -> p-1 let baseFirst = baseQueue.next; let pendingFirst = pendingQueue.next; baseQueue.next = pendingFirst; pendingQueue.next = baseFirst; } current.baseQueue = baseQueue = pendingQueue; queue.pending = null; } if (baseQueue !== null) { ... do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { ...这里和 processUpdateQueue 一样的 // 该update优先级不够,跳过,新的state是基于baseUpdate计算得出 // 如果这是第一个跳过的update,则之前的update/state就是新的 base update/state } else { ... // Process this update. if (update.eagerReducer === reducer) { // 初始加载-->执行-->更新 执行的时候如果已经计算好了值 这里直接使用即可 newState = ((update.eagerState: any): S); } else { // 不能使用就直接计算了,比如发生了一次调度,后续的调用都需要重新计算 const action = update.action; newState = reducer(newState, action); } } // 计算完所有的更新 update = update.next; } while (update !== null && update !== first); ... } // 返回最新的值,dispatch方法不变 const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }
首先,更新时读取的 hook 是从旧数据中复制的一部分属性,其次是通过 currentHook 全局变量记录的当前 hook,就是取得 currentHook.next。
下面就是优先级的一些判断,更新队列做相应的处理,接着来根据上一小节提到的内容决定是数据复用还是重新计算,最后返回是最新数据和 dispatch 方法。
初始加载和更新都是针对 hook 来说,调用执行是针对 hook 返回的第二个参数也就是 dispatch 而言,调用之后触发调度,从而再次进入 renderWithHooks 引起组件的更新,最终进入的流程和之前一样。
下一篇继续看下 hooks 中其他的一些方法原理,useEffect、useMemo、useCallback。至于 ref、context 打算和类组件的放在一起分析。
2人赞