React | 2020-08-23 01:42:55 717次 1次
继续看下 hooks 中其他的一些方法原理,useEffect、useLayoutEffect 、useMemo、useCallback 等。至于 ref、context 打算和类组件的放在一起分析。
一、useEffect
useEffect 创建
useEffect 的创建指的创建数据结构,因为这个过程仅仅时挂载一些数据结构,并不会执行相关逻辑。
创建的过程分为初始加载和更新两个步骤,这两个步骤在创建数据结构时唯一的区别是是否需要挂载 destroy 方法(useEffect 中返回),初始加载时不需要这个(但是会在 flushPassiveEffectsImpl 创建好,等待更新时使用),更新时需要先执行这个方法,因为需要清除某些事件的句柄等一些操作。
同样的 useEffect 也会被挂载到 fiber 的 memoizedState 中,和 useState、useReduce 一起的,结合上篇的数据结构图。
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = mountWorkInProgressHook(); ... hook.memoizedState = pushEffect(...); }
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = updateWorkInProgressHook(); ... // 相对于初始加载 增加了这部分的逻辑 if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; // 依赖没有发生变化 if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(hookEffectTag, create, destroy, nextDeps); return; } } } ... hook.memoizedState = pushEffect(...); }
通过 pushEffect 方法将返回的数据挂载到 hook 的 memoizedState,同时这份数据也会被保存到 updateQueue 中并且还会存在一条 effectList 链表,和之前文章中提到的那个不一样(但是结构一致),这里的是挂载到 updateQueue。
function pushEffect(tag, create, destroy, deps) { const effect: Effect = { tag, create, destroy, deps, // Circular next: (null: any), }; // wip let componentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); // 第一个 if (componentUpdateQueue === null) { // 初始赋值:{ lastEffect: null } componentUpdateQueue = createFunctionComponentUpdateQueue(); // 挂载到 wip 也就是当前fiber 的 updateQueue currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); componentUpdateQueue.lastEffect = effect.next = effect; } else { // 创建一条链表放到 fiber.updateQueue 中(之前的是放到fiber上) const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
useEffect 执行
useEffect 中内部方法执行起源自 flushPassiveEffects,它会在 react render 之后执行,因为在 before mutation 阶段发起一次调度异步执行(可以追踪堆栈信息查看调用):
function commitBeforeMutationEffects() { ... scheduleCallback(NormalPriority, () => { flushPassiveEffects(); return null; }); ... }
然后做两件事情(这里最新版代码中已经发生变化,不过思路仍然是这样,但是存储的结构换成了数组,难得一见):
先执行上一次 render 的销毁回调
再执行当前 render 的回调
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { ... if (lastEffect !== null) { ... do { if ((effect.tag & tag) === tag) { // 卸载执行 const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } } function commitHookEffectListMount(tag: number, finishedWork: Fiber) { ... if (lastEffect !== null) { ... do { if ((effect.tag & tag) === tag) { // 这个 creat 是在第一部分那里创建的 const create = effect.create; // 执行后返回销毁回调 并挂载 effect.destroy = create(); ... } effect = effect.next; } while (effect !== firstEffect); } }
这个方法为什么异步执行?与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
其中对于 useLayoutEffect 方法而言是同步执行的,页面渲染后又立即触发这个事件,因为在 commitLifeCycles 中执行了如下方法,而不是等待调度(内容和 commitHookEffectListMount 一致):
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
二、useMemo
初始加载
和 useEffect、useState不同的是,这个方法仅需要调用执行返回结果即可,不参杂其他过程,只有初始加载和更新时会执行。
function mountMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = mountWorkInProgressHook(); //依赖项 const nextDeps = deps === undefined ? null : deps; // 首次执行传入的回调 const nextValue = nextCreate(); // 它的数据结构是 ==> [产生的值,依赖项] hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
更新
更新的时候,就是判断依赖项是否发生改变,不变则返回旧值(存储在 memoizedState 上 ),改变则重新计算:
function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 取出上一次计算的值 const prevState = hook.memoizedState; if (prevState !== null) { // 依赖项存在,并且进行一个数据的对比, is 方法 if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } // 依赖项变化则重新计算新值 const nextValue = nextCreate(); // 重新挂载最新的数据以及依赖项 hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
三、useCallback
初始加载
这个方法更加过分,就是简单的将传入的函数存起来,句柄依据依赖项的变化而变化。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; }
更新
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }
这个方法和 useMemo 是一致的,useMemo 中也可以返回一个函数,这样就和 useCallback 的作用一致:
useMemo(()=>return fn); || || useCallback(fn);
1人赞