lz-lee / React-Source-Code

React源码阅读笔记
11 stars 0 forks source link

Hooks #15

Open lz-lee opened 4 years ago

lz-lee commented 4 years ago

Hoooks

定义

// ReactHooks.js

import ReactCurrentOwner from './ReactCurrentOwner';

function resolveDispatcher() {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  invariant(
    dispatcher !== null,
    'Hooks can only be called inside the body of a function component.',
  );
  return dispatcher;
}

// ...
function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function useReducer<S, A>(
  reducer: (S, A) => S,
  initialState: S,
  initialAction: A | void | null,
) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialState, initialAction);
}

// ...
// ReactFiberScheduler.js

function renderRoot(root: FiberRoot, isYieldy: boolean): void {

  flushPassiveEffects();

  isWorking = true;
  if (enableHooks) {
    ReactCurrentOwner.currentDispatcher = Dispatcher;
  } else {
    ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
  }
  // ....

  // ...
  // We're done performing work. Time to clean up.
  isWorking = false;
  ReactCurrentOwner.currentDispatcher = null;
  // ...
}

更新阶段调用 hooks 过程

prepareToUseHooks

function prepareToUseHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  nextRenderExpirationTime: ExpirationTime,
): void {
  if (!enableHooks) {
    return;
  }
  // 当前更新的优先级
  renderExpirationTime = nextRenderExpirationTime;
  // 当前更新的 fiber 
  currentlyRenderingFiber = workInProgress;
  // 非首次渲染,memoizedState 记录的是 functionComponent当中第一个 hooks api 对应的 hook 对象
  firstCurrentHook = current !== null ? current.memoizedState : null;

}

finishHooks

function finishHooks(
  Component: any,
  props: any,
  children: any,
  refOrContext: any,
): any {
  if (!enableHooks) {
    return children;
  }

  // This must be called after every function component to prevent hooks from
  // being used in classes.

  // 如果是重复渲染的情况,清空全局变量 currentHook workInProgressHook,并计数 numberOfReRenders 渲染次数,在 dispatchAction 会 判断次数如果大于 RE_RENDER_LIMIT 抛出错误
  // 再执行一次 Component(props, refOrContext); 将 重复渲染在一次 更新中执行掉
  while (didScheduleRenderPhaseUpdate) {
    // Updates were scheduled during the render phase. They are stored in
    // the `renderPhaseUpdates` map. Call the component again, reusing the
    // work-in-progress hooks and applying the additional updates on top. Keep
    // restarting until no more updates are scheduled.
    didScheduleRenderPhaseUpdate = false;
    numberOfReRenders += 1;

    // Start over from the beginning of the list
    currentHook = null;
    workInProgressHook = null;
    componentUpdateQueue = null;

    children = Component(props, refOrContext);
  }
  renderPhaseUpdates = null;
  numberOfReRenders = 0;

  // 更新中的 fiber 对象
  const renderedWork: Fiber = (currentlyRenderingFiber: any);
  // firstWorkInProgressHook 是 functionComponent 每一个 hooks 对应的 hook 对象。
  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
 // 对于useEffect 或者 useLayoutEffect 生成的 effect 链 
  renderedWork.updateQueue = (componentUpdateQueue: any);

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  firstCurrentHook = null;
  currentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;

  // Always set during createWorkInProgress
  // isReRender = false;

  // These were reset above
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;

  return children;
}

useState

function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  );
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

useReducer

createWorkInProgressHook

function createWorkInProgressHook(): Hook {
  if (workInProgressHook === null) {
    // This is the first hook in the list
    if (firstWorkInProgressHook === null) {
      // 都为 null,是非重复渲染的
      isReRender = false;
      currentHook = firstCurrentHook;
      if (currentHook === null) {
        // This is a newly mounted hook
        // 首次渲染,创建 hook 对象
        workInProgressHook = createHook();
      } else {
        // Clone the current hook.
        workInProgressHook = cloneHook(currentHook);
      }
      firstWorkInProgressHook = workInProgressHook;
    } else {
      // There's already a work-in-progress. Reuse it.
      // firstWorkInProgressHook 不为 null,说明 finishHooks 方法中的didScheduleRenderPhaseUpdate 为true 导致  while 循环在执行,即说明在重复渲染
      // 重复渲染的 
      isReRender = true;
      currentHook = firstCurrentHook;
      workInProgressHook = firstWorkInProgressHook;
    }
  } else {
    // 非首次渲染,并未执行过其他的 hooks api,也是非重复渲染的 
    if (workInProgressHook.next === null) {
      isReRender = false;
      let hook;
      if (currentHook === null) {
        // This is a newly mounted hook
        hook = createHook();
      } else {
        currentHook = currentHook.next;
        if (currentHook === null) {
          // This is a newly mounted hook
          hook = createHook();
        } else {
          // Clone the current hook.
          hook = cloneHook(currentHook);
        }
      }
      // 如果 next 为null 那么将 hook1.next = hook2 
      // Append to the end of the list
      workInProgressHook = workInProgressHook.next = hook;
    } else {
      // There's already a work-in-progress. Reuse it.
      // next 不为 null,复用原来的
      //  重复渲染
      isReRender = true;
      workInProgressHook = workInProgressHook.next;
      currentHook = currentHook !== null ? currentHook.next : null;
    }
  }
  return workInProgressHook;
}

// createHook

function createHook(): Hook {
  return {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };
}

dispatchAction

useEffect 与 useLayoutEffect

// ReactHookEffectTags
const NoEffect = /*             */ 0b00000000;   // => NoHookEffect
const UnmountSnapshot = /*      */ 0b00000010;
const UnmountMutation = /*      */ 0b00000100;
const UnmountMutation = /*      */ 0b00000100;
const MountMutation = /*        */ 0b00001000;
const UnmountLayout = /*        */ 0b00010000;
const MountLayout = /*          */ 0b00100000;
const MountPassive = /*         */ 0b01000000;
const UnmountPassive = /*       */ 0b10000000;

// ReactSideEffectTags 
const Update = /*                */ 0b000000000100;   // => updateEffect
const Passive = /*               */ 0b001000000000;   //  => passiveEffect

function useLayoutEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {

  useEffectImpl(
    UpdateEffect, 
    UnmountMutation | MountLayout,  // => 0b00100100
    create, 
    inputs
  );
}

export function useEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {
  useEffectImpl(
    UpdateEffect | PassiveEffect, // => 0b001000000100
    UnmountPassive | MountPassive, // => 0b11000000
    create,
    inputs,
  );
}

useEffectImpl

areHookInputsEqual

pushEffect

function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { return { lastEffect: null, }; }


## commitHookEffectList
- 在 `commitRoot` 的三次循环中均有被调用,三次循环后 还有一次调用是针对 `useEffect` 的
- 在一次更新渲染中会先执行上一次 `effect` 的 `destroy`,即官方推荐不能在 `hooks Api` 之前写 `if` 条件判断是否使用 `hooks`,这样会下一次更新时构建的 `effect` 链的顺序是和上次不一样的,导致在 `commitRoot` 时顺序错乱。
```js
function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
) {
  if (!enableHooks) {
    return;
  }
  // 读取 updateQueue
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  // 遍历在 useEffectImpl 中 pushEffect 生成的 effect 链 (hook对象的)
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 如果 effect.tag  有 unmountTag 则执行 destroy()
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = null;
        if (destroy !== null) {
          destroy();
        }
      }
      // 如果 effect.tag 有 moountTag 则执行 create()
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;

        let destroy = create();
        if (typeof destroy !== 'function') {
          // 非函数返回的提示
          if (__DEV__) {
            if (destroy !== null && destroy !== undefined) {
              warningWithoutStack(
                false,
                'useEffect function must return a cleanup function or ' +
                  'nothing.%s%s',
                typeof destroy.then === 'function'
                  ? '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
                    'Instead, you may write an async function separately ' +
                    'and then call it from inside the effect:\n\n' +
                    'async function fetchComment(commentId) {\n' +
                    '  // You can await here\n' +
                    '}\n\n' +
                    'useEffect(() => {\n' +
                    '  fetchComment(commentId);\n' +
                    '}, [commentId]);\n\n' +
                    'In the future, React will provide a more idiomatic solution for data fetching ' +
                    "that doesn't involve writing effects manually."
                  : '',
                getStackByFiberInDevAndProd(finishedWork),
              );
            }
          }
          destroy = null;
        }
        effect.destroy = destroy;
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

第一次循环 commitBeforeMutationLifecycles

function commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork); return; } //... } //... }


### 第二次循环 commitAllHostEffects
- `commitAllHostEffects` 对 `dom` 节点的 `PlacementAndUpdate` 和 `Update` 会先执行 `commitWork`
- `unmountTag -> UnmountMutation` 对应 `useLayoutEffect` 传入 `UnmountMutation | MountLayout`,因此会执行 `useLayoutEffect` 的 `destroy`
- `mountTag -> MountMutation`。没有标记 `MountMutation` 因此都不会执行 `create` 

```js
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  // ...
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // Note: We currently never use MountMutation, but useLayout uses
      // UnmountMutation.
      commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
      return;
    }
    // ...
  }
}

第三次循环 commitAllLifeCycles


function commitAllLifeCycles(
  finishedRoot: FiberRoot,
  committedExpirationTime: ExpirationTime,
) {
  if (__DEV__) {
    ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
    ReactStrictModeWarnings.flushLegacyContextWarning();

    if (warnAboutDeprecatedLifecycles) {
      ReactStrictModeWarnings.flushPendingDeprecationWarnings();
    }
  }
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    if (effectTag & (Update | Callback)) {
      recordEffect();
      const current = nextEffect.alternate;
      commitLifeCycles(
        finishedRoot,
        current,
        nextEffect,
        committedExpirationTime,
      );
    }

    if (effectTag & Ref) {
      recordEffect();
      commitAttachRef(nextEffect);
    }

    // useEffect 中标记 fiber 的 effectTag 为 updateEffect | passiveEffect 
    // 因此对于 useEffect 的节点会满足条件,从而赋值 rootWithPendingPassiveEffects
    if (enableHooks && effectTag & Passive) {
      rootWithPendingPassiveEffects = finishedRoot;
    }

    nextEffect = nextEffect.nextEffect;
  }
}

function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedExpirationTime: ExpirationTime,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
      break;
    }
    // ...
  }
}

第四次执行 commitHookEffectList

// commitRoot 

//... 

if (
    enableHooks &&
    firstEffect !== null &&
    rootWithPendingPassiveEffects !== null
  ) {
    // This commit included a passive effect. These do not need to fire until
    // after the next paint. Schedule an callback to fire them in an async
    // event. To ensure serial execution, the callback will be flushed early if
    // we enter rootWithPendingPassiveEffects commit phase before then.
    let callback = commitPassiveEffects.bind(null, root, firstEffect);
    if (enableSchedulerTracing) {
      // TODO: Avoid this extra callback by mutating the tracing ref directly,
      // like we do at the beginning of commitRoot. I've opted not to do that
      // here because that code is still in flux.
      callback = Schedule_tracing_wrap(callback);
    }
    passiveEffectCallbackHandle = Schedule_scheduleCallback(callback);
    passiveEffectCallback = callback;
  }

//...

commitPassiveEffects

function commitPassiveEffects(root: FiberRoot, firstEffect: Fiber): void {
  rootWithPendingPassiveEffects = null;
  passiveEffectCallbackHandle = null;
  passiveEffectCallback = null;

  // Set this to true to prevent re-entrancy
  const previousIsRendering = isRendering;
  isRendering = true;

  let effect = firstEffect;
  do {
    if (effect.effectTag & Passive) {
      let didError = false;
      let error;
      if (__DEV__) {
        invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
        if (hasCaughtError()) {
          didError = true;
          error = clearCaughtError();
        }
      } else {
        try {
          commitPassiveHookEffects(effect);
        } catch (e) {
          didError = true;
          error = e;
        }
      }
      if (didError) {
        captureCommitPhaseError(effect, error);
      }
    }
    effect = effect.nextEffect;
  } while (effect !== null);

  isRendering = previousIsRendering;

  // Check if work was scheduled by one of the effects
  const rootExpirationTime = root.expirationTime;
  if (rootExpirationTime !== NoWork) {
    requestWork(root, rootExpirationTime);
  }
}

commitPassiveHookEffects

useContext

function useContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  // Ensure we're in a function component (class components support only the
  // .unstable_read() form)
  resolveCurrentlyRenderingFiber();
  return readContext(context, observedBits);
}

useRef

useImperativeHandle

function useImperativeMethods<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  inputs: Array<mixed> | void | null,
): void {
  // TODO: If inputs are provided, should we skip comparing the ref itself?
  const nextInputs =
    inputs !== null && inputs !== undefined
      ? inputs.concat([ref])
      : [ref, create];

  // TODO: I've implemented this on top of useEffect because it's almost the
  // same thing, and it would require an equal amount of code. It doesn't seem
  // like a common enough use case to justify the additional size.
  useLayoutEffect(() => {
    // 如果传递下来的 ref 是个函数
    if (typeof ref === 'function') {
      const refCallback = ref;
      const inst = create();
      // 将 create 方法执行结果当作参数
      refCallback(inst);
      return () => refCallback(null);
    } else if (ref !== null && ref !== undefined) {
      const refObject = ref;
      const inst = create();
      // 直接将 create 方法的结果挂载到传递下来的 ref.current 上
      refObject.current = inst;
      return () => {
        refObject.current = null;
      };
    }
  }, nextInputs);
}

useCallback

function useCallback<T>(
  callback: T,
  inputs: Array<mixed> | void | null,
): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();

  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [callback];

  const prevState = workInProgressHook.memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1];
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }
  workInProgressHook.memoizedState = [callback, nextInputs];
  return callback;
}

useMemo