React(v16.8) Hooks 简析

动机

  • 在组件之间复用状态逻辑很难, providers,consumers,高阶组件,render props等能够将横切关注点 (如校验,日志,异常等) 与核心业务逻辑分离开,可是使用过程当中也会带来扩展性限制,ref传值问题,“嵌套地狱”等问题;Hook 提供一种简单直接的代码复用方式,可使开发者在无需修改组件结构的状况下复用状态逻辑。html

  • 复杂组件生命周期经常包含一些不相关的逻辑,相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿;不少开发者将 React 与状态管理库结合使用,这每每会引入了不少抽象概念,开发过程当中还须要在不一样的文件之间来回切换;Hook 提供一种更合理的代码组织方式,能够将组件中相互关联的代码汇集在一块儿,而不是被生命周期方法强制拆开,使其更加可预测。前端

  • class 组件存在着一些问题(如:class 不利于代码压缩,而且会使热重载出现不稳定的状况);Hook支持函数组件,使开发者在非class 的状况下可使用更多的 React 特性。react

使用

Hooks 只能在函数组件 (FunctionComponent) 中使用,赋予无实例无生命周期的函数组件以 class 组件的表达力而且更合理地拆分/组织代码,解决复用问题。git

实现原理

Fiber 提供了 hooks 实现的基础:hooks 是基于 Fiber 对象上能存储 memoizedState,它以双向链表的形式存储在 Fiber 的 memoizedState 字段中。github

什么是 Fiber?

Fiber 把应用树区分红每个节点的更新,每个 ReactElement 对应一个 Fiber 对象,Fiber 呈链表结构,串联整个应用树结构 (child, siblings, return),如图:数组

Fiber Tree

Fiber 会记录节点的各类状态 (state, props)(包括functional Component),而且在 update 的时候,会从原来的 Fiber(current)clone 出一个新的 Fiber(alternate)。两个 Fiber diff 出的变化(side effect)记录在 alternate上,在更新结束后 alternate 会取代以前的 current 的成为新的 current 节点。浏览器

Fiber 对 react 渲染机制的改变主要的影响:微信

  • 异步更新:由于 Fiber 把应用树区来分红每个节点的更新,它们的更新互相独立,不会有相互的影响,因此能够异步打断如今的更新,而后去等待一个别的任务执行完成以后回过头来继续进行更新。数据结构

  • 提供了 hooks 实现的基础:hooks 是基于 Fiber 对象上能存储 memoizedState, 基于 memoizedState 上能够存储这些东西,一步一步向下构建了 hooks API 的体系。dom

主要 Hooks

  • 经常使用的:useState, useEffect, useContext, useReducer;

  • 此外不经常使用的:useLayoutEffect, useCallback, useMemo, useRef, useImperativeHandle。

以 useState 为例了解 hook 的渲染更新过程 先了解 Hook 的数据结构

export type Hook = {
  memoizedState: any, //上一次渲染的时候的state

  baseState: any, // 当前正在处理的state
  baseUpdate: Update<any, any> | null, // 当前的更新
  queue: UpdateQueue<any, any> | null, // 产生的update放在这个队列里

  next: Hook | null, // 下一个
};

复制代码

运行下面的组件代码

export default function App() {
  const [name, setName] = useState('dora')

  const nameChange = e => {
    setName(e.target.value)
  }

  return (
    <React.Fragment>
      <input type='text' value={name} onChange={nameChange} />
      <p>{name}</p>
    </React.Fragment>
  )
}

复制代码

初始化 state 时调用 mountState,初始化 initialState,而且记录在 workInProgressHook.memoizedState 和 workInProgressHook.baseState上,而后建立 queue 对象, queue 的 dispatch 属性是用来记录更新 state 的方法的,dispatch 就是 dispatchAction绑定了对应的 Fiber 和 queue。而后返回初始的格式 [name, setName] = useState('dora');执行源码以下:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
复制代码

再来看更新过程

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

复制代码

因而可知 useState 只是个语法糖,本质就是 useReducer;那么再来看 useReducer:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  if (numberOfReRenders > 0) {
    //在当前更新周期中又产生了新的更新
    //就继续执行这些更新直到当前渲染周期中没有更新为止
    ...
  }
  const last = queue.last;
  const baseUpdate = hook.baseUpdate;
  const baseState = hook.baseState;

  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }
if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        if (!didSkip) {
          didSkip = true;
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        if (updateExpirationTime > remainingExpirationTime) {
          remainingExpirationTime = updateExpirationTime;
        }
      } else {
        markRenderEventTimeAndConfig(
          updateExpirationTime,
          update.suspenseConfig,
        );
        if (update.eagerReducer === reducer) {
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);
    if (!didSkip) {
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

复制代码

就是根据 reducer 和 update.action 来建立新的 state,并赋值给 Hook.memoizedState以及 Hook.baseState;在当前更新周期中又产生了新的更新, 就继续执行这些更新直到当前渲染周期中没有更新为止。而后对每一个更新判断其优先级(根据expirationTime值的大小),若是不是当前总体更新优先级内得更新会跳过,第一个跳过得 Update 会变成新的 baseUpdate,他记录了在以后全部得 Update,即使是优先级比他高得,由于在他被执行得时候,须要保证后续的更新要在他更新以后的基础上再次执行。

最后执行 dispatchAction方法,发起一次 scheduleWork 的调度,完成更新,此处省略代码。

useEffect vs useLayoutEffect

useEffect 和 useLayoutEffect 带给 FunctionalComponent 产生反作用能力的 Hooks,他们的行为很是相似 componentDidMount 和 componentDidUpdate 的合集,而且经过 return 一个函数指定如何“清除”反作用。

先看 useEffect 和 useLayoutEffect 更新的过程:

updateEffect

updateLayoutEffect

二者都调用了 updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) 方法,传入的第二个参数又做为 pushEffect 的入参生成一个新的 effect。

updateEffectImpl

这个 effect 的 tag 就是入参 hookEffectTag,pushEffect 方法返回一个新的 effect,而且建立了一个 updateQueue,这个 queue 会在 commit 阶段被执行。

能够看到,这个阶段,useEffect 和 useLayoutEffect 的主要区别在生成的 effect 的 tag参数不一样,经过计算,二者的tag值分别为二进制:0b11000000 和 0b00100100;

再来看 commit 阶段调用的 commitHookEffectList 方法。

commitHookEffectList

经过对比传入的 effectTag(unmountTag & mountTag) 和 Hook 对象上的 effectTag,判断是否须要执行对应的 destory 和 create 方法,那么又在哪些地方调用了commitHookEffectList 方法呢?能够看下其中两处:commitLifeCycles 和 commitWork。

commitLifeCycles

commitWork

在 commitLifeCycles 中传入的 unmountTag 和 mountTag 值分别为:0b00010000 和 0b00100000; 在 commitWork 中传入的 unmountTag 和 mountTag 值分别为:0b00000100 和 0b00001000; 分别计算 effect.tag & unmountTag 和 effect.tag & mountTag:

conclusion

能够看到 useLayoutEffect 的 destory 会在 commitWork 的时候被执行;而他的 create会在 commitLifeCycles 的时候被执行;useEffect 在这个流程中都不会被执行。

事实上:

  • useLayoutEffect 会在当前 commit 执行的过程当中就会被执行 destroy 和 create, 而对于 useEffect,会异步地等到此次全部的 dom 节点更新完成,浏览器渲染完成后,才会去执行这部分代码。

  • 它对于 useLayoutEffect 来讲,它是不会去阻塞浏览器的渲染,由于咱们可能在 useLayoutEffect 里面去执行一些 dom 相关的操做,甚至 setState 来执行一些更新,这种更新都会同步执行,至关于 react 的运行时它要占用更长的 js 的运行时间,致使浏览器没有时间去渲染,最终可能会致使页面会有些卡顿。

  • 服务端渲染状况下,不管 useLayoutEffect 仍是 useEffect 都没法在 Javascript 代码加载完成以前执行。能够经过使用 showChild &&进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展现组件。

  • useLayoutEffect 的执行过程跟 componentDidMount 和 componentDidUpdate 很是类似,因此 React 官方也说了,若是你必定要选择一个相似于生命周期方法的 Hook,那么 useLayoutEffect 是不会错的那个,可是咱们推荐你使用 useEffect,在你清楚他们的区别的前提下,后者是更好的选择。

更多 Hook 使用,请查看如下文档

官方文档 zh-hans.reactjs.org/docs/hooks-…

官方源码 github.com/facebook/re…

做者:朵拉

扫码关注:「铜板街科技」微信公众号,精彩内容按期推送。公众号消息界面回复「推荐」「前端」「Java」「客户端」获取更多精准内容。

相关文章
相关标签/搜索