zoukankan      html  css  js  c++  java
  • React(v16.8) Hooks 简析

    动机

    1. 在组件之间复用状态逻辑很难,providers,consumers,高阶组件,render props等可以将横切关注点(如校验,日志,异常等)与核心业务逻辑分离开,但是使用过程中也会带来扩展性限制,ref传值问题,“嵌套地狱”等问题;Hook提供一种简单直接的代码复用方式,可以使开发者在无需修改组件结构的情况下复用状态逻辑
    2. 复杂组件生命周期常常包含一些不相关的逻辑,相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起;很多开发者将React与状态管理库结合使用,这往往会引入了很多抽象概念,开发过程中还需要在不同的文件之间来回切换;Hook提供一种更合理的代码组织方式,可以将组件中相互关联的代码聚集在一起,而不是被生命周期方法强制拆开,使其更加可预测
    3. class组件存在着一些问题(如:class 不利于代码压缩,并且会使热重载出现不稳定的情况);Hook支持函数组件,使开发者在非class 的情况下可以使用更多的React特性

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

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

    什么是Fiber?
    Fiber把应用树区来分成每一个节点的更新,每一个ReactElement对应一个Fiber对象,Fiber呈链表结构,串联整个应用树结构(child, siblings, return),如图:

    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的体系

    主要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更新的过程

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

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

    可以看到,这个阶段,useEffect和useLayoutEffect的主要区别在生成的effect的tag参数不同,通过计算,两者的tag值分别为二进制:0b11000000 和 0b00100100;


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

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

    在commitLifeCycles中传入的unmountTag和mountTag值分别为:0b00010000 和 0b00100000;

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

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

    事实上:

    • useLayoutEffect会在当前commit执行的过程中就会被执行destroy和create, 而对于useEffect,会异步地等到这次所有的dom节点更新完成,浏览器渲染完成后,才会去执行这部分代码,
    • 它对于useLayoutEffect来说,它是不会去阻塞浏览器的渲染,因为我们可能在useLayoutEffect里面去执行一些dom相关的操作,甚至setState来执行一些更新,这种更新都会同步执行,相当于react的运行时它要占用更长的js的运行时间,导致浏览器没有时间去渲染,最终可能会导致页面会有些卡顿
    • 服务端渲染情况下,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。可以通过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。
    • useLayoutEffect的执行过程跟componentDidMount和componentDidUpdate非常相似,所以React官方也说了,如果你一定要选择一个类似于生命周期方法的Hook,那么useLayoutEffect是不会错的那个,但是我们推荐你使用useEffect,在你清楚他们的区别的前提下,后者是更好的选择。


    更多Hook使用请查看官方文档官方源码

  • 相关阅读:
    Boost Log : Log record formatting
    Boost Log : Attributes
    PLSA的EM推导
    特征处理:一点经验
    海量推荐系统:mapreduce的方法
    操作系统之存储器管理
    maredit测试
    算法:链表
    c++特别要点:多态性与虚函数
    sizeof的用法与字节对齐
  • 原文地址:https://www.cnblogs.com/vicky24k/p/11151862.html
Copyright © 2011-2022 走看看