useState和useReducer的原理及区别

1,844 阅读6分钟

背景

在端午假期时写的,把之前学习的知识做了整理,想分享给大家看看,知识点有没有理解不到位的地方,还有没有需要改进的地方,希望借助大家的慧眼,提升自己,同时,也希望能帮助同学们,对react的理解更加深入;

useReducer

1.mountReducer

页面加载时,走的是mountReducer;mountReducer会创建hook对象,得到初始状态,创建queue,生成dispatch供用户调用;

  1. 创建hook对象,形成或加入hook单向链表;(mountWorkInProgressHook)
  2. initialArg和init生成initialState初始状态,存到hook对象的memoizedState和baseState里;
  3. 创建queue对象给hook.queue,queue用于存储当前状态,当前reducer,更新队列pending,赛道lanes,dispatch等;(调用dispatch时会用到queue)
  4. 绑定dispatch函数,dipatch的前两个参数绑定为,dispatch函数所在的函数组件fiber,和用于存储更新状态的queue;(之所以要绑定fiber和queue,是在用户同时多次调用同一个dispatch,所更新的是同一个组件下的同一个状态)
  5. 返回初始状态和绑定过的dispatch函数(return [hook.memoizedState, dispatch]);
function mountReducer(reducer, initialArg, init) {
  var hook = mountWorkInProgressHook();
  //创建hook对象,形成hook链表或加入已经创建好的hook链表
  var initialState;

  if (init !== undefined) {//根据initialArg, init得到初始状态
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }

  hook.memoizedState = hook.baseState = initialState;
  //将初始状态存储到hook对象的memoizedState和baseState
  var queue = { 
    pending: null,//存储更新环状链表
    interleaved: null,
    lanes: NoLanes,//赛道
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState //上一次渲染的状态
  };
  hook.queue = queue; //创建queue对象,也存储到hook对象中
  var dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber$1, queue);
  //用函数组件fiber(currentlyRenderingFiber$1),queue对象生成dispatch;
  //用户调用同一个dispatch,拿到的fiber和queue是一样的;
  //用户同时调用多次同一个dispatch,会将更新对象,添加queue.pending环状列表;
  return [hook.memoizedState, dispatch];
  //生成dispath函数,和初始状态一起返回给用户调用;
}

2.dispatch

用户调用dispatch,会触发更新;创建更新对象,添加到hook对象的queue.pending环状列表中,开始调度更新;

  1. 拿到更新的赛道lane,创建更新对象update,将update对象加入到queue.pending环状链表里;
  2. 开始调度更新(scheduleUpdateOnFiber(fiber, lane, eventTime));
  3. 拿到root节点,从root节点开始更新工作,将performSyncWorkOnRoot同步更新工作添加到syncQueue中,有添加过的就不用添加,只添加一次,做批量更新;
  4. scheduleMicrotask添加一个微任务(默认用queueMicrotask,除了IE,其余新版本的浏览器均有这个api),用于处理syncQueue;(多次调用dispatch,也只执行一次,dispatch是同步任务)
  5. 同步任务执行完后,就会执行scheduleMicrotask添加的微任务,开始renderRootSync,workLoopSync工作循环,到renderWithHooks时执行函数组件,执行updateReducer,拿到新状态,返回给用户,生成新的jsx,与老fiber进行比对,生成新的fiber,将新状态渲染到页面中
function dispatchReducerAction(fiber, queue, action) {
  //dispatchReducerAction就是用户调用的dispatch函数,
  //fiber为当前hook所在的函数组件fiber;
  //queue是在mountReducer时绑定的
  //action是用户传的;
  
  var lane = requestUpdateLane(fiber);//得到更新的lane
  var update = {//创建update更新对象
    lane: lane, //更新赛道
    action: action, //用户传入的action
    hasEagerState: false,
    eagerState: null,
    next: null
  };

  if (isRenderPhaseUpdate(fiber)) {
    //根据fiber判断是否是加载渲染时的更新
    //很明显,用户手动调用dispatch是更新,不是加载阶段,所以不会走这里
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    enqueueUpdate$1(fiber, queue, update);
    //将update,加入到queue.pending环状链表中
    //多次调用dispatch,创建的update都会加入到这个queue.pending环状链表中;
    
    var eventTime = requestEventTime();//事件时间,更新时用
    
    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    //开始更新调度
  }
}

3.updateReducer

  1. 根据上次更新或加载后,存储的组件fiber的hook对象,创建新的hook对象;
  2. 拿到hook的更新对象环状链表queue.pending,循环环状链表,算出新的状态;
  3. 判断新老状态,如果不一样就标记更新;
  4. 把新的状态存储到hook对象;
  5. 把新的状态和dispatch返回给用户;
function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  //根据上次更新或加载后,存储的组件fiber的hook对象,创建新的hook对象
  var queue = hook.queue;


  queue.lastRenderedReducer = reducer;//更新reducer
  var current = currentHook; // The last rebase update that is NOT part of the base state.

  var pendingQueue = queue.pending;//拿到环状链表

  // We have a queue to process.
  var first = pendingQueue.next;// 拿到第一个更新对象
  var newState = current.baseState;
  var update = first;

  do { //循环queue.pending环状链表,拿到新的状态
    if (update.hasEagerState) {
      // If this update is a state update (not a reducer) and was processed eagerly,
      // we can use the eagerly computed state
      newState = update.eagerState;
    } else {
      var action = update.action;
      newState = reducer(newState, action); 
      //调用用户给的reducer和action,得到新的状态
    }

    update = update.next;
  } while (update !== null && update !== first);
  // 如果 update === first,意味着环状链表循环结束,
  // 所有的update对象均已使用,可以跳出循环;


  if (!objectIs(newState, hook.memoizedState)) {
    //比对新老状态,如果不相等,标记更新;
    markWorkInProgressReceivedUpdate();
  }

    hook.memoizedState = newState;//更新hook对象的状态;
    queue.lastRenderedState = newState;

  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];//返回给用户
}

useState

1.mountState

  1. 创建hook对象,形成或加入hook单向链表;(mountWorkInProgressHook)
  2. initialState初始状态,存到hook对象的memoizedState和baseState里;
  3. 创建queue给hook.queue,用于存储当前状态,当前reducer,环状链表pending,赛道lanes,dispatch等;
  4. 绑定dispatch函数,更新时调用dispatch,dipatch的前两个参数绑定为,dispatch函数所在的函数组件fiber,和fiber的更新队列queue;
  5. 返回初始状态和绑定过的dispatch函数(return [hook.memoizedState, dispatch]);
function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
    //如果initialState是函数,就执行它,返回的值就是初始状态
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,// 更新环状链表
    interleaved: null,
    lanes: NoLanes,// 更新赛道
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState //上一次渲染的状态
  };
  hook.queue = queue;
  var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
  //生成dispatch函数;
  
  return [hook.memoizedState, dispatch];
}

2.dispatch(setState)

用户调用dispatch,会触发更新

  1. 拿到更新的赛道lane,创建更新对象update,将update对象加入到queue.pending更新队列里;
  2. 拿到老状态,算出新的状态,新老状态进行比对,如果一样,就return,不会调度更新;
  3. 开始调度更新(scheduleUpdateOnFiber(fiber, lane, eventTime));(这块和useReducer的dispatch是一样的)
function dispatchSetState(fiber, queue, action) {
 //dispatchSetState就是用户调用的setState函数,
  //dispatch的参数和useReducer的dispatch是一样的
  
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null
  };
  enqueueUpdate$1(fiber, queue, update);
  //将update对象添加到queue.pending环状链表
  
  var currentState = queue.lastRenderedState; //拿到旧的状态
  var eagerState = lastRenderedReducer(currentState, action); 
  //得到新的状态

  update.eagerState = eagerState;
  //更新新的状态

  if (objectIs(eagerState, currentState)) {
    //判断新老状态是否相等
    return;
  }

  var eventTime = requestEventTime();
  var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
  //开始调度更新

}

3.updateState

调用的就是updateReducer,基于updateReducer,传入basicStateReducer,也就是useReducer的reducer,用于计算用户的新状态;

function updateState(initialState) {
  return updateReducer(basicStateReducer);
}

function basicStateReducer(state, action) {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
  //如果用户调用的setState传入的参数是一个函数,就执行这个函数,返回新的state;
  //不是函数就返回action,action为用户传入的新state;
}

4.多次调用同一个dispatch

也就是多次调用setState,由于绑定的是同一个dispatch,每次调用dispatch都会创建一个update对象,所以将update对象添加到queue.pending时,添加的是同一个环状链表;在执行updateReducer时,会循环这个环状链表,更新状态,得到最新状态

const [number, setNumber] = useReducer(reducer, 0);
const [age,setAge] = useState(0)

return (
  <>
  <div>{number}</div>
  <div>{age}</div>
  <button onClick={() => {
  debugger
  setNumber({type: 'add'})
  setAge(()=>age+1) //update1
  setAge(3) //update2
}}>add</button>
</>
)

总结

1.useState与useRender的相同点和不同点?

相同点

  1. 都是返回一个长度为2的数组,状态与dispatch方法;
  2. 在mount阶段,都创建了hook对象和queue对象,以hook所在的组件fiber和queue为前两个参数,生成了dispatch函数;
  3. 调用dispatch时,都要创建update对象,将update对象加入到queue.pending环状链表中;
  4. 在update阶段,updateState是基于updateReducer的,只是给了一个默认的reducer;

不同点

  1. 用法不同,useState只需传入一个状态即可;而useReducer需要reducer和状态;
  2. 调用dispatch时,useState会判断新老状态是否相等,再决定调度更新;useReducer直接调度更新;
  3. update阶段,useState的reducer是内部固定的;useReducer的reducer是用户传的;

2.对useReducer和useState和设计的个人理解

  1. 可以将相关联的状态放在一个state里,不相关的另写一个useState,不用像this.setState一样,所有状态放在一起;这块官方有说明zh-hans.reactjs.org/docs/hooks-…
  2. dispatch时,将更新对象添加到环状链表,将更新处理添加到一个微任务,这样可以多次调用dispatch,而只执行一次更新;这个处理不像老版的this.setState那样,利用一个批量更新的开关控制批量更新,将更新放到事件函数执行完以后执行,但更新执行的优先级还是要高于宏任务和微任务的,如果将this.setState放到一个宏任务或者微任务中执行,批量更新先执行完,将批量更新的开关置为false,而this.setState还没有被执行,批量更新已被破坏;而将多个dispatch放到一个宏任务或者微任务下,执行dispatch,等于重新添加了一个处理更新的微任务,在同一个宏任务下所有的同步dispatch执行完,触发更新,批量更新没有被破坏,完美解决批量更新不被异步任务破坏的问题
  3. 每个useState返回一个dispatch,可以利用组件通信的方式,在所有的函数组件调用修改同一个状态,灵活性很高;
  4. 在mountState或mountReducer时,利用闭包可以保存和读取函数内部变量的特性,返回的dispatch函数,读取和修改的都是各自独立的,对应各自的hook对象和queue对象,状态等

3.学习方法

和我在写解读useEffect和useLayoutEffect原理总结的一样,只关注当下所学的知识,抓住知识的核心点,这些学会就行,其余的咱不看,没有那么多的时间和精力