原理篇: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;
}

posted @ 2022-01-04 09:27  webLion200  阅读(783)  评论(0编辑  收藏  举报