likes
comments
collection
share

深入浅出react(源码剖析hook内部执行原理)

作者站长头像
站长
· 阅读数 52

学习内容:

  • useState内部运行机制
  • useEffect内部运行机制
  • useCallback,useMomo内部运行机制
  • useRef内部运行机制
  • react用什么方式记录了hooks,状态是如何保存的。

1.hook和fiber之间的关联。

首先先大概了解一下渲染流程:

  • 1.jsx通过babel编译后调用React.createElement方法转换成vdom
  • 2.reconcile 阶段将 vdom 转换成 fiber
  • 3.commit阶段开始执行真正的dom增删改查操作

更新流程: 在初次渲染完成之后后有一个current fiber树,在更新过程前,会有一个workInProgress fiber树,即将调和渲染的 fiber 树.再一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。

简单了解一下fiber的作用:

  • 描述一个节点的基本信息
  • 以链表的方式连接其他节点
  • 存储状态更新的值
  • 优先级调度

和我们hook相关联的就是存储状态更新的值。fiber通过memoizedStateupdateQueue属性存储hook相关的状态。函数组件会将内部用到的所有的 hook 通过单向链表的形式,保存在组件对应 fiber 节点的 memoizedState 属性上。updateQueueuseEffect 产生的 effect 连接成的环状单向链表。

2.hook的结构。

const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  • memoizedState: 当前需要保存的值
  • baseState: 基础状态, 作为合并hook.baseQueue的初始值
  • baseQueue:由于之前某些高优先级任务导致更新中断,baseQueue 记录的就是尚未处理的最后一个 update
  • queue:内部存储调用 setValue 产生的 update 更新信息,是个环状单向链表
  • next:下一个hook

本次讲解hook我们主要关注memoizedState,queue,next。

hook链表创建过程

每个hook都有mount阶段和update阶段。

mount阶段:

mount阶段通过mountWorkInProgressHook创建当前 hookhook 对象。 所有hook通过它新建 hook 对象,如果前面没有hook 对象,就将该 hook 挂到当前 fiber 节点的 memoizedState上面,否则接到前一个 hook 对象的 next 上,构成单向链表。

function mountWorkInProgressHook(): Hook {
    const hook: Hook = {
      memoizedState: null,
  
      baseState: null,
      baseQueue: null,
      queue: null,
  
      next: null,
    };
  
    if (workInProgressHook === null) {
      // This is the first hook in the list
      currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    } else {
      // Append to the end of the list
      workInProgressHook = workInProgressHook.next = hook;
    }
    return workInProgressHook;
  }

update阶段:

update阶段通过updateWorkInProgressHook函数获取hook的。之前我们讲过hook保存在fibermemoizedState属性,每个hook通过链表的形式连接,update阶段遍历mount 阶段创建的链表,故不能改变 hook 的执行顺序,否则会拿错

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
  ......
}

具体hook原理

useCallback

mount阶段:

  • 通过mountWorkInProgressHook创建当前 hook 的 hook 对象
  • 获取传入的第二个参数deps,没有传入则设置为null
  • memorizedState属性上放入一个数组,第一个元素为传入的函数,第二个元素为deps
  • 返回传入的函数
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;
}

update阶段:

  • 阶段通过updateWorkInProgressHook函数获取hook
  • 取出memorizedState属性上的数组的deps进行对比,如果没变,那就返回之前的回调函数,否则返回新传入的函数。
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

mount阶段:

  • 通过mountWorkInProgressHook创建当前 hook 的 hook 对象
  • 获取传入的第二个参数deps,没有传入则设置为null
  • memorizedState属性上放入一个数组,第一个元素为传入的函数执行结果,第二个元素为deps
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;
}

update阶段:

  • 阶段通过updateWorkInProgressHook函数获取hook
  • 取出memorizedState属性上的数组的deps进行对比,如果没变,那就返回之前的数组第一个元素(之前函数执行的结果),如果变了,创建一个新的数组放在 memorizedState,第一个元素是新传入函数的执行结果,第二个元素是 deps
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) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    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;
}

useRef

mount阶段:

  • 把传入的值包装成一个含有current属性的对象, 然后放在 memorizedState 属性上。
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

updated阶段:

  • 直接取出hookmemoizedState值返回出去。
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

那么对于设置ref节点的dom值,useRef会在什么时候更新呢?

我们知道组件在commit阶段的mutation阶段执行DOM操作,所以对应ref的更新也是发生在mutation阶段。

useEffect

mount阶段:

  • 通过mountWorkInProgressHook创建当前 hook 的 hook 对象
  • 获取传入的第二个参数deps,没有传入则设置为null
  • 通过位运算给currentlyRenderingFiber.effectTag赋值
  • 最后给调用pushEffect方法给memoizedState赋值
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.effectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    undefined,
    nextDeps,
  );
}

update阶段:

  • 通过mountWorkInProgressHook获取当前 hook 的 hook 对象
  • 判断currentHook是否为null
  • 通过areHookInputsEqual比较一下deps是否相同
  • 如果相同,调用if里面的pushEffect函数,副作用不会执行
  • 如果不相同,调用下方的pushEffect函数,副作用会执行
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  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;
      }
    }
  }
  currentlyRenderingFiber.effectTag |= fiberEffectTag;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

pushEffect函数:

先看代码:

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    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;
}

pushEffect 的作用:

  • 创建一个effect对象
  • 并将组件内的 effect 对象串成环状单向链表放到fiber.updateQueue上面

两次调用pushEffect函数的差别?

// if内部的,第一个参数是hookFlags = 4
pushEffect(hookFlags, create, destroy, nextDeps);
// if外部的,第一个参数是HasEffect | hookFlags = 5
hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);

这两行代码的区别是传入的第一个参数不同,而第一个参数就是effect.tag的值,effect.tag = 4不会添加到副作用执行队列,而effect.tag = 5可以。没有添加到副作用执行队列的effect就不会执行。这样就巧妙的实现了useEffect基于deps来判断是否需要执行回调函数。

到这里, 我们搞明白了,不管useEffect里的deps有没有变化都会为回调函数创建effect并添加到effect链表和fiber.updateQueue中,但是React会根据effect.tag来决定该effect是否要添加到副作用执行队列中去执行。

effect执行时机

在commit中的mutation阶段,基于副作用创建任务并放到taskQueue中。在页面渲染完成之后,根据fiber上 updateQueue的链表effect依次执行。

schedulePassiveEffects中,会决定是否执行effect链表中的effect,判断的依据就是每个effect上的effect.tag:


function schedulePassiveEffects(finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;
    // 遍历effect链表
    do {
      var _effect = effect,
          next = _effect.next,
          tag = _effect.tag;
      // 基于effect.tag决定是否添加到副作用执行队列
      if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }

      effect = next;
    } while (effect !== firstEffect);
  }
}

useState

mount阶段:

  • 将初始值存放在memoizedState
  • dispatchSetState创建一个更新函数保存在dispatch属性上
  • 最终返回一个数组,包含初始值和一个由dispatchAction创建的函数。
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

dispatchAction更新函数:

精简后的代码

function dispatchAction(fiber, queue, action) {
  // 创建一个update,用于后续的更新,这里的action就是我们setState的入参
  var update = {
    lane: lane,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  // 这段闭环链表插入update的操作有没有很熟悉?
  var pending = queue.pending;

  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;
  var alternate = fiber.alternate;
    // 判断当前是否是渲染阶段
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      var lastRenderedReducer = queue.lastRenderedReducer;
       // 这个if语句里的一大段就是用来判断我们这次更新是否和上次一样,如果一样就不会在进行调度更新
      if (lastRenderedReducer !== null) {
        var prevDispatcher;

        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }

        try {
          var currentState = queue.lastRenderedState;
          var eagerState = lastRenderedReducer(currentState, action);

          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;

          if (objectIs(eagerState, currentState)) {
            return;
          }
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }
    // 将携带有update的fiber进行调度更新
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

dispatchAction函数的功能:

  • 创建一个update并加入到fiber.hook.queue链表中,并且链表指针指向这个update

  • 判断当前是否是渲染阶段决定要不要马上调度更新;

  • 判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新;

  • 满足上述条件则将带有updatefiber进行调度更新;

update阶段:

update 时:可以看到,其实调用的是 updateReducer,只是 reducer 是固定好的,作用就是用来直接执行 setValue(即 dispath) 函数传进来的 action,即 useState 其实是对 useReducer 的一个封装,只是 reducer 函数是预置好的。

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

updateReducer函数:


function updateReducer(reducer, initialArg, init) {
  // 创建一个新的hook,带有dispatchAction创建的update
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;

  queue.lastRenderedReducer = reducer;
  var current = currentHook;

  var baseQueue = current.baseQueue; 
  var pendingQueue = queue.pending;

  current.baseQueue = baseQueue = pendingQueue;

  if (baseQueue !== null) {
    // 从这里能看到之前讲的创建闭环链表插入update的好处了吧?直接next就能找到第一个update
    var first = baseQueue.next;
    var newState = current.baseState;

    var update = first;
    // 开始遍历update链表执行所有setState
    do {
      var updateLane = update.lane;
      // 假如我们这个update上有多个setState,在循环过程中,最终都会做合并操作
      var action = update.action;
      // 这里的reducer会判断action类型,下面讲
      newState = reducer(newState, action);

      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

上面的更新中,会循环遍历update进行一个合并操作,只取最后一个setState的值,这时候可能有人会问那直接取最后一个setState的值不是更方便吗?

这样做是不行的,因为setState入参可以是基础类型也可以是函数, 如果传入的是函数,它会依赖上一个setState的值来完成更新操作,下面的代码就是上面的循环中的reducer

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

我们终于明白多个setState是如何合并的

总结

以上就是我们本章学习的内容,大概学习了一下hook的内部执行原理,希望大家喜欢

系列文章

深入浅出react(带你手写一个简易版react)

转载自:https://juejin.cn/post/7158665693070622728
评论
请登录