【React源码笔记13】- 其他 hooks

React | 2020-08-23 01:42:55 717次 1次

继续看下 hooks 中其他的一些方法原理,useEffect、useLayoutEffect useMemouseCallback 等。至于 refcontext 打算和类组件的放在一起分析。


一、useEffect 

useEffect 创建

useEffect 的创建指的创建数据结构,因为这个过程仅仅时挂载一些数据结构,并不会执行相关逻辑。

创建的过程分为初始加载更新两个步骤,这两个步骤在创建数据结构时唯一的区别是是否需要挂载 destroy 方法(useEffect 中返回),初始加载时不需要这个(但是会在 flushPassiveEffectsImpl 创建好,等待更新时使用),更新时需要先执行这个方法,因为需要清除某些事件的句柄等一些操作。

同样的 useEffect 也会被挂载到 fiber memoizedState 中,和 useStateuseReduce 一起的,结合上篇的数据结构图

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

初始加载

useEffectuseState不同的是,这个方法仅需要调用执行返回结果即可,不参杂其他过程,只有初始加载和更新时会执行。

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人赞

分享到: