• 原理篇:hooks


    Q: React 引入hooks的原因

    A: 让函数组件可以做类组件的事,可以有自己的state,可以处理一些副作用,获取ref。

    hooks 与 fiber (workInProgress)

    hooks主要以三种形态存在于react中:

    • HooksDispatcherOnMount:函数组件初始化,建立fiber与hooks之间的关系
    • HooksDispatcherOnUpdate: 函数组件的更新,需要 hooks 去获取或者更新维护状态。
    • ContextOnlyDispatcher: 防止在函数外部调用,直接报错

    所有函数组件的触发是在 renderWithHooks 方法中:

    let currentlyRenderingFiber
    function renderWithHooks(current,workInProgress,Component,props){
        // 初始化时把处理中的fiber赋值给currentlyRenderingFiber,每个hooks内部读取的就是currentlyRenderingFiber的内容。
        currentlyRenderingFiber = workInProgress; 
        // memoizedState用来存放hooks列表
        workInProgress.memoizedState = null; 
        workInProgress.updateQueue = null;    /* 清空状态(用于存放effect list) */
        // 如果是初始化阶段使用HooksDispatcherOnMount,更新阶段使用HooksDispatcherOnUpdate
        // React 通过赋予 current 不同的 hooks,以此来监控 hooks 是否在函数组件内部调用
        ReactCurrentDispatcher.current =  current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate
        // 此时,函数组件在Component函数内部被真正执行,对应的hooks也会被依次执行
        let children = Component(props, secondArg); 
        
        ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 将hooks变成第一种,防止hooks在函数组件外部调用,调用直接报错。 */
    }
    

    注意:函数组件触发时把处理中的fiber赋值给currentlyRenderingFiber,后面的源码会调用。

    在组件初始化的时候,每一次hooks的执行,都会调用mountWorkInProgressHook。

    function mountWorkInProgressHook() {
      const hook = {  
        memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
        baseState: null, 
        baseQueue: null,
        queue: null, 
        next: null,
      };
      if (workInProgressHook === null) {
        // 在renderWithHooks函数中已经将workInProgress赋值给currentlyRenderingFiber
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
      } else {  
        // 每一个 hooks 通过 next 链表建立起关系
        workInProgressHook = workInProgressHook.next = hook;
      }
      return workInProgressHook;
    }
    

    如下例子:

    export default function Index(){
        const [ number,setNumber ] = React.useState(0) // 第一个hooks
        const [ num, setNum ] = React.useState(1)      // 第二个hooks
        const dom = React.useRef(null)                 // 第三个hooks
        React.useEffect(()=>{                          // 第四个hooks
            console.log(dom.current)
        },[])
        return <div ref={dom} >
            <div onClick={()=> setNumber(number + 1 ) } > { number } </div>
            <div onClick={()=> setNum(num + 1) } > { num }</div>
        </div>
    }
    

    hooks链表如下图:
    image

    Q:hooks为什么要放在函数顶部,不能写在条件判断语句中?

    A:在更新阶段,会先复用一份hooks,形成新的hooks链表。如果放在if语句中,会造成链表对比不一致的情况。

    useState

    在react中,useState会mountState函数中初始化

    function mountState(
      initialState
    ){
      const hook = mountWorkInProgressHook();
      if (typeof initialState === 'function') {
        // 如果 useState 第一个参数为函数,执行函数得到state
        initialState = initialState();
      }
      hook.memoizedState = hook.baseState = initialState;
      // 保存更新信息
      const queue = (hook.queue = {
        ...
      });
    
      // 负责更新的函数
      const dispatch = (queue.dispatch = (dispatchAction.bind( 
        null,
        currentlyRenderingFiber,
        queue,
      )))
      return [hook.memoizedState, dispatch];
    }
    
    

    useState通过dispatchAction 来触发state更新。

    dispatchAction 定义如下:

    function dispatchAction<S, A>(
      fiber: Fiber,
      queue: UpdateQueue<S, A>,
      action: A,
    )
    
    const [ number , setNumber ] = useState(0)
    

    dispatchAction 就是setNumber,dispatchAction前两个参数已被固定写死,我们传入的是第三个参数action。

    下面分析dispatchAction内部实现:

    function dispatchAction(fiber, queue, action){
        /* 第一步:创建一个 update */
        const update = { ... }
        const pending = queue.pending;
        // 第一次更新
        if (pending === null) {  
            update.next = update;
        } else {  /* 再次更新 */
           update.next = pending.next;
           pending.next = update;
        }
        if( fiber === currentlyRenderingFiber ){
            /* 说明当前fiber正在发生调和渲染更新,那么不需要更新 */
        }else{
           if(fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)){
                const lastRenderedReducer = queue.lastRenderedReducer;
                // 上一次的state
                const currentState = queue.lastRenderedState;
                // 本次需要更新的state
                const eagerState = lastRenderedReducer(currentState, action); 
                // 如果两次更新state相同,则不更新
                if (is(eagerState, currentState)) {
                   return 
                }
           }
           scheduleUpdateOnFiber(fiber, expirationTime);    /* 发起调度更新 */
        }
    }
    

    多次调用同一个setState/useState为何会合并处理?

    useState 触发更新的本质是updateReducer,源码如下:

    function updateReducer(){
        // 第一步把待更新的pending队列取出来。合并到 baseQueue
        const first = baseQueue.next;
        let update = first;
        // 当同一个useState在执行时,会继续给newState赋值,而不是向下执行
       do {
            newState = reducer(newState, action);
        } while (update !== null && update !== first);
        
         hook.memoizedState = newState;
         return [hook.memoizedState, dispatch];
    }
    

    useEffect

    当我们调用useEffect的时候,在组件第一次渲染的时候会调用mountEffect方法

    function mountEffect(create,deps){
        const hook = mountWorkInProgressHook();
        const nextDeps = deps === undefined ? null : deps;
        // pushEffect创建一个 effect, 如果存在多个effect就会形成副作用链表 
        hook.memoizedState = pushEffect( 
          HookHasEffect | hookEffectTag, 
          create, // useEffect 第一次参数,就是副作用函数
          undefined, 
          nextDeps, // useEffect 第二次参数,deps    
        )
    }
    

    对于函数组件,可能存在多个 useEffect / useLayoutEffect ,hooks 把这些 effect,独立形成链表结构,在 commit 阶段统一处理和执行。

    更新流程就是判断两次deps是否相等:

    function updateEffect(create,deps){
        const hook = updateWorkInProgressHook();
        // 如果deps没有变化,则更新effect list就可以了
        if (areHookInputsEqual(nextDeps, prevDeps)) { 
            pushEffect(hookEffectTag, create, destroy, nextDeps);
            return;
        } 
        // 如果deps依赖项发生改变,赋予 effectTag。在commit阶段会根据 effectTag 判断执行effect
        currentlyRenderingFiber.effectTag |= fiberEffectTag
        hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create,destroy,nextDeps)
    }
    
    

    useRef

    useRef 就是创建并维护一个 ref 原始对象。

    function mountRef(initialValue) {
      const hook = mountWorkInProgressHook();
      const ref = {current: initialValue};
      hook.memoizedState = ref; // 创建ref对象。
      return ref;
    }
    

    更新:

    function updateRef(initialValue){
      const hook = updateWorkInProgressHook()
      return hook.memoizedState // 取出复用ref对象。
    }
    

    useMemo

    function mountMemo(nextCreate,deps){
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    
    
    function updateMemo(nextCreate,nextDeps){
        const hook = updateWorkInProgressHook();
        const prevState = hook.memoizedState; 
        const prevDeps = prevState[1]; // 之前保存的 deps 值
        if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
            return prevState[0];
        }
        const nextValue = nextCreate(); // 如果deps,发生改变,重新执行
        hook.memoizedState = [nextValue, nextDeps];
        return nextValue;
    }
    
    
  • 相关阅读:
    38
    37
    学记
    36.java_exception_test
    c++中enum的用法——枚举类型
    35
    34
    33
    32
    31
  • 原文地址:https://www.cnblogs.com/renzhiwei2017/p/15761310.html
Copyright © 2020-2023  润新知