从实现自己的useState入手理解React常见hooks源码

(一)实现一个简易的useState

整体数据结构

在这里插入图片描述

需求

function App() {
    
    
  const [age, updateAge] = useState(0);
  const [num,updateNum] = useState(5);
  console.log(`${
      
      isMount ? 'mount' : 'update'} age: `,age,'num',num)
  return {
    
    
    click() {
    
    
      updateAge(num => num + 1);
      updateAge(num => num + 8);
      updateNum(num => num + 5);
    }
  }
}

我们观察以上代码,实现代码主要考虑:
1.调用ReactDOM.render会产生mount的更新,useState的返回的age,num是initialValue(即0和5)
2.点击click事件会产生update的更新,每次调用updateAge或者updateNum,去更新组件App,useState返回的num/age为更新后的结果

更新是什么

更新是如下数据结构:

const update = {
    
    
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

update数据结构

这些update是如何组合在一起呢?
答案是:他们会形成环状单向链表。
调用updateNum,updateAge实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:

function dispatchAction(queue, action) {
    
    
  const update = {
    
    
    // 更新执行的函数
    action,
    // 与同一个Hook的其他更新形成链表
    next: null
  }
  if (queue.pending === null) {
    
    
    update.next = update;
  } else {
    
    
    // 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
    // 1.新的update的next指向第一个update
    update.next = queue.pending.next;
    // 2.最后一个update的next指向新的update
    queue.pending.next = update;
  }
  // queue.pending始终指向最后一个插入的update
  queue.pending = update;
  schedule();
}

在这里插入图片描述

状态如何保存

更新产生的update对象会保存在对应的hook的queue中。对于函数式组件来说,hook存储在哪里呢?
FunctionComponent对应的fiber中。

// App组件对应的fiber对象
const fiber = {
    
    
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};

Hook数据结构

hook = {
    
    
  // 保存update的queue,即上文介绍的queue
  queue: {
    
    
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

在这里插入图片描述
整体思路:
1.mount的时候,在useState函数中创建hook,把这个hook利用workInProgressHook指针(指向上一个hook)和上一个hook串起来,形成hooks单向无环链表。返回 [initialState, dispatchAction.bind(null, hook.queue)]。

2.调用updateAge/updateNum的时候,即是在调用dispatchAction.bind(null, hook.queue),
将此次的update的对象和之前的update对象们组合成环状单向链表。并且模拟React开始调度更新。
调度更新中会执行useState函数,根据其对应的hook对象及其hooks环状单向链条,计算得出更新的结果baseState,返回 [baseState, dispatchAction.bind(null, hook.queue)];

整体代码

let workInProgressHook;
// 通过workInProgressHook变量指向当前正在工作的hook。
let isMount = true;
// 首次render时是mount


const fiber = {
    
    
  memoizedState: null,
  stateNode: App
};

function schedule() {
    
    
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  const app = fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
  return app;
}

 // 1.创建update
 // 2.环状单向链表操作
 // 3.模拟React开始调度更新
function dispatchAction(queue, action) {
    
    
  const update = {
    
    
    // 更新执行的函数
    action,
    // 与同一个Hook的其他更新形成链表
    next: null
  }
  if (queue.pending === null) {
    
    
    update.next = update;
  } else {
    
    
    // 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
    // 1.新的update的next指向第一个update
    update.next = queue.pending.next;
    // 2.最后一个update的next指向新的update
    queue.pending.next = update;
  }
  // queue.pending始终指向最后一个插入的update
  queue.pending = update;
  schedule();
}

function useState(initialState) {
    
    
  let hook;
  if (isMount) {
    
    
    hook = {
    
    
      queue: {
    
    
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
    
    
      fiber.memoizedState = hook;
    } else {
    
    
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    
    
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    
    
    let firstUpdate = hook.queue.pending.next;
   do {
    
    
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while(firstUpdate !== hook.queue.pending.next)
  }
  hook.memoizedState = initialState;
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

function App() {
    
    
  const [age, updateAge] = useState(0);
  const [num,updateNum] = useState(5);
  console.log(`${
      
      isMount ? 'mount' : 'update'} age: `,age,'num',num)
  return {
    
    
    click() {
    
    
      updateAge(num => num + 1);
      updateAge(num => num + 8);
      updateNum(num => num + 5);
    }
  }
}

const myApp = schedule();
myApp.click()

(二)原理解析

源码中,我们大部分的Hooks都有两个定义:

// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
    
    
  useEffect: mountEffect,
  useReducer: mountReducer,
  useState: mountState,
 // 其他Hooks
};

// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  useEffect: updateEffect,
  useReducer: updateReducer,
  useState: updateState,
  // 其他Hooks
};

从这里可以看出,我们的Hooks在Mount阶段和Update阶段的逻辑是不一样的。在Mount阶段和Update阶段他们是两个不同的定义。

1.useState

1.1 mountState

先来看mountState的实现。

// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
    
    
  // 1.获取当前的Hook节点,同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    
    
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // 2.声明一个链表来存放更新
  const queue = (hook.queue = {
    
    
    last: null,
    dispatch: null,
    lastRenderedReducer,
    lastRenderedState,
  });
  // 3.返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  // 4.返回当前状态和修改状态的方法 
  return [hook.memoizedState, dispatch];
}

Hook的类型定义

// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
    
    
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,  // 指向下一个Hook
};

这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里(workInProgressHook作为指针串联起来):

// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
    
    
  const hook: Hook = {
    
    
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    
    
    // 当前workInProgressHook链表为空的话,
    // 将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    
    
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上。比如我们的这个例子:

const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
  useEffect(() => {
    
    })

那么在mount阶段,就会生产如下图这样的单链表:
在这里插入图片描述
useState和useReducer都是使用了一个queue链表来存放每一次的更新。以便后面的update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的update对象,添加到queue链表上,而且这个是一个循环链表。可以看一下 dispatchAction 方法的实现:

// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action,) {
    
    
    const update = {
    
    
      action,
      next: null,
    };
    // 将update对象添加到循环链表中
    const last = queue.last;
    if (last === null) {
    
    
      // 链表为空,将当前更新作为第一个,并保持循环
      update.next = update;
    } else {
    
    
      const first = last.next;
      if (first !== null) {
    
    
        // 在最新的update对象后面插入新的update对象
        update.next = first;
      }
      last.next = update;
    }
    // 将表头保持在最新的update对象上
    queue.last = update;
   // 进行调度工作
    scheduleWork();
}

也就是我们每次执行dispatchAction方法,比如setAge或setName。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。比如假设我们执行了下面几个语句:

setAge(19);
setAge(20);
setAge(21);

那么我们的Hooks链表就会变成这样:
在这里插入图片描述

在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。
而组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。

在这里插入图片描述

1.2 updateState

然后我们来看看update阶段,也就是看一下我们的useState或useReducer是如何利用现有的信息,去给我们返回最新的最正确的值的。先来看一下useState在update阶段的代码也就是updateState:

// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
    
    
  return updateReducer(basicStateReducer, initialState);
}

可以看到,updateState底层调用的其实是updateReducer,因为我们调用useState的时候,并不会传入reducer,所以这里会默认传递一个basicStateReducer进去。我们先看看这个basicStateReducer:

// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
    
    
  return typeof action === 'function' ? action(state) : action;
} 

在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回。来继续看一下updateReducer的逻辑:

// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
    
    
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表头
  const last = queue.last;

  // 获取最早的那个update对象
  first = last !== null ? last.next : null;

  if (first !== null) {
    
    
    let newState;
    let update = first;
    do {
    
    
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}

在update阶段,也就是我们组件第二次第三次。。执行到useState或useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useState的reducer是baseStateReducer,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。
在这里插入图片描述

2.useReducer

const initialState = {
    
    age: 0, name: 'Dan'};

function reducer(state, action) {
    
    
  switch (action.type) {
    
    
    case 'increment':
      return {
    
    ...state, age: state.age + action.age};
    case 'decrement':
      return {
    
    ...state, age: state.age - action.age};
    default:
      throw new Error();
  }
}
function PersionInfo() {
    
    
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Age: {
    
    state.age}, Name: {
    
    state.name}
      <button onClick={
    
    () => dispatch({
    
    type: 'decrement', age: 1})}>-</button>
      <button onClick={
    
    () => dispatch({
    
    type: 'increment', age: 1})}>+</button>
    </>
  );
}

2.1 补充mountReducer

mountReducer,你会发现和mountState几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState其实就是阉割版的useReducer

// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init) {
    
    
  // 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  let initialState;
  // 初始化
  if (init !== undefined) {
    
    
    initialState = init(initialArg);
  } else {
    
    
    initialState = initialArg ;
  }
  hook.memoizedState = hook.baseState = initialState;
  // 存放更新对象的链表
  const queue = (hook.queue = {
    
    
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
 // 返回状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}

3.总结

3.1 React 如何管理区分Hooks?

React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中

3.2 useState和useReducer如何在每次渲染时,返回最新的值?

每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回

3.3 useState为什么不能放到条件语句里面?

这是因为React通过单链表来管理Hooks。
update 阶段,每次调用 useState,会根据其对应的hook对象及其update环状单向链条,计算得出更新的结果。
那么如何得到对应的hook对象呢,我们是用了一个指针workInProgressHook去顺着hooks链表依次获取的,每次调用 useState,链表就会执行 next 向后移动一步,workInProgressHook就会指向下一个useState对应的hook对象,在下一个useState中就可以直接使用进行计算了。
如果将 useState 写在条件判断中,假设条件判断不成立,没有执行里面的 useState 方法,会导致接下来所有的 useState 的hook对象的取值出现偏移,从而导致异常发生。
在这里插入图片描述

4.useEffect

function PersionInfo () {
    
    
  const [age, setAge] = useState(18);
  useEffect(() =>{
    
    
      console.log(age)
  }, [age])

 const [name, setName] = useState('Dan');
 useEffect(() =>{
    
    
      console.log(name)
  }, [name])
  return (
    <>
      ...
    </>
  );
}

PersionInfo组件第一次渲染的时候会在控制台输出age和name,在后面组件的每次update中,如果useEffect中的deps依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在unmount的时候就会执行清除函数(如果有)

React中是怎么实现的呢?其实很简单,在FiberNode中通过一个updateQueue来存放所有的effect,然后在每次渲染之后依次执行所有需要执行的effect。useEffect 也分为mountEffect和updateEffect

在这里插入图片描述

4.1 mountEffect

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑

function mountEffect( create,deps,) {
    
    
  return mountEffectImpl(
    create,
    deps,
  );
}

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    
    
  // 获取当前Hook,并把当前Hook添加到Hook链表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将当前effect保存到对应的Hook节点的memoizedState属性上,
  // 以及添加到fiberNode的updateQueue上
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
    
    
  const effect: Effect = {
    
    
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // componentUpdateQueue 会被挂载到fiberNode的updateQueue上
  if (componentUpdateQueue === null) {
    
    
    // 如果当前Queue为空,将当前effect作为第一个节点
    componentUpdateQueue = createFunctionComponentUpdateQueue();
   // 保持循环
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    
    
    // 否则,添加到当前的Queue链表中
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
    
    
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
    
    
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // componentUpdateQueue.lastEffect永远指向最后一个插入的effect
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect; 
}

可以看到在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
    
    
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}

也就是在mount阶段我们所有的effect都以链表的形式被挂载到了fiberNode上。然后在组件渲染完毕之后,React就会执行updateQueue中的所有方法。

在这里插入图片描述

4.2 updateEffect

update阶段和mount阶段类似,只不过这次会考虑effect 的依赖deps,如果此次更新effect的依赖没有变化的话,就会被打上NoHookEffect标签,最后会在commit阶段跳过该effect的执行。

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑

function updateEffect(create,deps){
    
    
  return updateEffectImpl(
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
    
    
  // 获取当前Hook节点,并把它添加到Hook链表
  const hook = updateWorkInProgressHook();
  // 依赖 
  const nextDeps = deps === undefined ? null : deps;
  // 清除函数
  let destroy = undefined;
  
  // 将当前effect保存到对应的Hook节点的memoizedState属性上,
  // 以及添加到fiberNode的updateQueue上
  if (currentHook !== null) {
    
    
    // 拿到前一次渲染该Hook节点的effect
    // hook对象上memoizedState存储了effect,effect.deps是前一次渲染时候的依赖
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
    
    
      const prevDeps = prevEffect.deps;
      // 对比deps依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
    
    
        // 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
        // effect的执行
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
 
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

commit阶段跳过NoHookEffect的effect的执行。

function commitHookEffectList(unmountTag,mountTag,finishedWork) {
    
    
  const updateQueue = finishedWork.updateQueue;
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    
    
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
    
    
      if ((effect.tag & unmountTag) !== NoHookEffect) {
    
    
        // Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
    
    
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
    
    
        // Mount 阶段执行所有tag !== NoHookEffect的effect.create,
        // 我们的清除函数(如果有)会被返回给destroy属性,等到unmount的时候执行
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

在这里插入图片描述

5.总结useEffect做了什么?

1.FiberNdoe节点中会有一个updateQueue链表来存放所有的本次渲染需要执行的effect。
2.mountEffect阶段和updateEffect阶段会把effect 挂载到updateQueue上。
3.updateEffect阶段,deps没有改变的effect会被打上NoHookEffect tag,commit阶段会跳过该Effect。

6.完整图示在这里插入图片描述

参考资料

1.react技术揭秘
2.react hooks源码解析

猜你喜欢

转载自blog.csdn.net/m0_57307213/article/details/126990343