react hooks源码分析:useState-阿里云开发者社区

开发者社区> 阿里云GTS能力中心-浩鲸智能> 正文
登录阅读全文

react hooks源码分析:useState

简介: 阅读react hooks源码了解工作原理,开阔思维,提升架构能力。

前言


自从react 官方发布react hooks来,项目开发组件时几乎都是使用函数式组件来开发。在使用hooks过程中可以起到简化代码并逻辑清晰,对比于类组件能更易于理解代码。网上也有很多关于这两种组件的优劣势对比,读者可自行去翻阅。本主主要是想通过阅读源码来了解这背后的原理。(因为我不想成为API工程师,哈哈哈)


在这篇文章中,我想通过源码的角度来分析下react hooks中的useState。假定读者已经对react hooks有一定的使用并初步了解。(如果不了解建议去官网学习,react官网


函数式组件和类组件


使用react开发基本上都是组件式开发,react组件分函数式组件和类组件。那函数式组件跟类组件的区别是什么。下面看两段代码


函数式组件

import React from 'react';


const App = (props) => {

 return <div>hello {props.user}<div>;

}


类组件

import React from 'react';


class App extends React.Component {

  constructor(props) {

    super(props);

  this.state = {

     name: 'Damon',

    };

  }


  componentDidMount() {}

  

  render() {

    return (

     <>

       <div>hello {this.props.user}</div>

    <div>hello {this.state.name}</div>

      </>

    )

  }

}

这两段代码除了没有自身的状态和生命周期外基本上算是等价的,那react发布hooks想解决的问题就是让函数式组件能拥有自身的状态和生命周期。

在hooks还没发布时,函数式组件一直都是作为UI组件负责渲染视图,没有自身的状态和 "生命周期函数" (当使用hooks开发时建议忘却生命周期函数的概念),类组件有自身的状态和生命周期函数可以处理复杂业务逻辑。

这两种组件有个本质的区别:函数式组件会捕获每次渲染时所用的值,类组件会创建一个实例来保存最新的值。

想深入了解的话可以阅读此文函数式组件与类组件有何不同?作者是Dan Abramov(react核心开发人员之一)。


hooks 初始阶段


分析源码首先从引入hooks开始,以useState为例子。

import { useState } from 'react';

react/src/ReactHooks.js

可以来看下useState这个方法做了什么 (简化了代码,去掉了类型和开发环境的提示)

export function useState(initialState){

  const dispatcher = resolveDispatcher();

  return dispatcher.useState(initialState);

}

调用useState等于是执行了 dispatcher.useState(initialState)

dispatcher是resolveDispatcher方法返回的,来看下这个方法做了什么。


resolveDispatcher

function resolveDispatcher() {

  const dispatcher = ReactCurrentDispatcher.current;

  return dispatcher;

}

ReactCurrentDispatcher

react/src/ReactCurrentDispatcher.js

const ReactCurrentDispatcher = {

  current: null,

};


export default ReactCurrentDispatcher;

其实看到这里第一阶段就已经结束了。ReactCurrentDispatcher.current初始化为null,先暂时记住这个接下来我们得从函数执行阶段来看。


useState 初始化


看react渲染逻辑兜兜转转到 react-reconciler/src/ReactFiberBeginWork.js beginWork这个函数。benginWork函数通过传入的当前的Fiber来创建子Fiber节点。会根据Fiber.tag来判断生成不同的字节点。

function beginWork(

 current, // 每次渲染的时候会产生一个current fiber树,commit阶段会替换成真实的dom

  workInProgress, // 更新过程中,每个Fiber都会有一个跟其对应的Fiber,在更新结束后current会和workInProgress交换位置

  renderLanes,

){

  //... 先不关心其他逻辑

 switch (workInProgress.tag) {

    // 当function component第一次创建Fiber的时候,组件类型是 IndeterminateComponent (具体可以看createFiberFormTypeAndProps)

    case IndeterminateComponent: {

      return mountIndeterminateComponent(

        current,

        workInProgress,

        workInProgress.type,

        renderLanes,

      );

    }

 }

}


function mountIndeterminateComponent(

 _current,

  workInProgress,

  Component,

  renderLanes,

) {

 //...

 

  value = renderWithHooks(

    null, // 第一次渲染是null

    workInProgress, // workInProgress fiber

    Component, // 组件本身

    props, // props

    context, // 上下文

    renderLanes, 

  );

}


export function renderWithHooks(

  current,

  workInProgress,

  Component,

  props,

  secondArg,

  nextRenderLanes,

) {

  renderLanes = nextRenderLanes;

  currentlyRenderingFiber = workInProgress;


  workInProgress.memoizedState = null;

  workInProgress.updateQueue = null;

  workInProgress.lanes = NoLanes;

  

  // current === null 第一次渲染 current !== null 更新阶段

  ReactCurrentDispatcher.current =

    current === null || current.memoizedState === null

     ? HooksDispatcherOnMount

    : HooksDispatcherOnUpdate;

 

  // 函数组件被执行

  let children = Component(props, secondArg);


  // 这里的逻辑先放一放

  if (didScheduleRenderPhaseUpdateDuringThisPass) {}


  // 当你不在函数组件内部执行hooks时候会抛出异常, ContextOnlyDispatcher对象方法都是抛出异常的方法

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;


  const didRenderTooFewHooks =

    currentHook !== null && currentHook.next !== null;


  renderLanes = NoLanes;

  currentlyRenderingFiber = (null: any);


  currentHook = null;

  workInProgressHook = null;


  didScheduleRenderPhaseUpdate = false;


  invariant(

    !didRenderTooFewHooks,

    'Rendered fewer hooks than expected. This may be caused by an accidental ' +

      'early return statement.',

  );


  if (enableLazyContextPropagation) {

    if (current !== null) {

      if (!checkIfWorkInProgressReceivedUpdate()) {

        const currentDependencies = current.dependencies;

        if (

          currentDependencies !== null &&

          checkIfContextChanged(currentDependencies)

        ) {

          markWorkInProgressReceivedUpdate();

        }

      }

    }

  }


  return children;

}

renderWithHooks 这个方法是函数执行的主要方法,首先是把memoizedState和updateQueue等于null,然后通过判断current是否为null来赋值不同的hooks对象,current为null说明是第一次渲染不为null说明是更新,这里第一次渲染跟更新是执行不同的hooks对象方法的。还记得第一阶段看到ReactCurrentDispatcher.current吗?就是在这里被赋值的。


Component 调用是我们的函数组件被执行了,我们写的逻辑就是在这里被执行的。hooks也会依次按照顺序执行。


hookDispatchOnMount 和 hookDispatchOnUpdate


hookDispatchOnMount是第一次渲染,hookDispatchOnUpdate是更新阶段。

const HooksDispatcherOnMount: Dispatcher = {

  readContext,


  useCallback: mountCallback,

  useContext: readContext,

  useEffect: mountEffect,

  useImperativeHandle: mountImperativeHandle,

  useLayoutEffect: mountLayoutEffect,

  useMemo: mountMemo,

  useReducer: mountReducer,

  useRef: mountRef,

  useState: mountState,

  useDebugValue: mountDebugValue,

  useDeferredValue: mountDeferredValue,

  useTransition: mountTransition,

  useMutableSource: mountMutableSource,

  useOpaqueIdentifier: mountOpaqueIdentifier,


  unstable_isNewReconciler: enableNewReconciler,

};


const HooksDispatcherOnUpdate: Dispatcher = {

  readContext,


  useCallback: updateCallback,

  useContext: readContext,

  useEffect: updateEffect,

  useImperativeHandle: updateImperativeHandle,

  useLayoutEffect: updateLayoutEffect,

  useMemo: updateMemo,

  useReducer: updateReducer,

  useRef: updateRef,

  useState: updateState,

  useDebugValue: updateDebugValue,

  useDeferredValue: updateDeferredValue,

  useTransition: updateTransition,

  useMutableSource: updateMutableSource,

  useOpaqueIdentifier: updateOpaqueIdentifier,


  unstable_isNewReconciler: enableNewReconciler,

};

我们先来看看第一次渲染的mountState做了什么。

function mountState(initialState){

  const hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {

    initialState = initialState();

  }

  hook.memoizedState = hook.baseState = initialState;

  const queue = (hook.queue = { // 更新队列

    pending: null, // 待更新

    interleaved: null,

    lanes: NoLanes,

    dispatch: null, // 更新函数

    lastRenderedReducer: basicStateReducer, // 获取最新的state

    lastRenderedState: initialState, // 最后一次的state

  });

  

  // dispatchActio负责更新ui的函数

  const dispatch = (queue.dispatch = (dispatchAction.bind(

    null,

    currentlyRenderingFiber,

    queue,

  ));

  return [hook.memoizedState, dispatch];

}

首先调用了mountWorkInProgressHook函数得到一个hook(待会来详细看看这个方法干了什么),然后判断initialState是否function,是的话执行得到state数据。接着把state赋值给了memoizedState和baseState。申明了一个queue对象,queue是个待更新队列,dispatch是负责更新的函数,具体怎么更新的在dispatchAction方法内可查看。


mountWorkInProgressHook

function mountWorkInProgressHook() {

  const hook: Hook = {

    memoizedState: null, // 不同的hook保存的不同 useState保存的是state useEffect保存的是                 effect对象

    baseState: null, // 最新的值

    baseQueue: null, // 最新的队列

    queue: null, // 待更新队列


    next: null, // 指向下一个hook对象

  };


  // react hooks的数据结构是链表的方式,具体的逻辑就在这里。

  if (workInProgressHook === null) {

    // 函数内的第一个hooks就会走到这里

    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

  } else {

    // 接下来每个hook都会被添加到链接到未尾

    workInProgressHook = workInProgressHook.next = hook;

  }

  return workInProgressHook;

}

每次执行一个hooks函数都会调用这个方法来创建一个hook对象,这个对象里保存了不同hook所对应的数据,最新的state数据,更新的队列和指向下一个hook的对象。


来看个例子,看看为什么不能在条件语句中申明hook

import React, { useState, useEffect, useRef } from 'react';


const App = () => {

 const [name, setName] = useState('Damon');

 const [age, setAge] = useState(23);

  if (age !== 23) {

    const Ref = useRef(null);

  }

  

  useEffect(() => {

   console.log(name, age);

  }, []);

  

  return (

   <div>

     <span>{name}</span>

     <span>{age}</span>

    </div>

  )

}


export default App;

当这个App组件被渲染的时候,workInProgressHook.memoizedState中会以链表的形式来保存这些hook。

image

如果在条件语句中申明hook,那么在更新阶段链表结构会被破坏,Fiber树上缓存的hooks信息就会和当前的workInProgressHook不一致,不一致的情况下读取数据可能就会出现异常。


dispatchAction

dispatchAction这个是负责更新的函数,在mountState中通过bind绑定然后赋值给了dispatch。dispatch就是上面例子结构出来的setName。我们来看看这个函数又干了什么。

function dispatchAction(

  fiber, // 当前的fiber数

  queue, // mountState申明的更新队列

  action, // 这个就是我们setState传进来的参数

) {

    

   // 计算 expirationTime

  const eventTime = requestEventTime();

  const lane = requestUpdateLane(fiber);


  // react更新中都会有一个update

  const update = {

    lane,

    action,

    eagerReducer: null,

    eagerState: null,

    next: null,

  };


  const alternate = fiber.alternate;

    // 判断是否处于渲染阶段

  if (

    fiber === currentlyRenderingFiber ||

    (alternate !== null && alternate === currentlyRenderingFiber)

  ) {

    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;

    const pending = queue.pending;

    if (pending === null) {

      // This is the first update. Create a circular list.

      update.next = update;

    } else {

      update.next = pending.next;

      pending.next = update;

    }

    queue.pending = update;

  } else {

    if (isInterleavedUpdate(fiber, lane)) {

      const interleaved = queue.interleaved;

      if (interleaved === null) {

        // This is the first update. Create a circular list.

        update.next = update;

        // At the end of the current render, this queue's interleaved updates will

        // be transfered to the pending queue.

        pushInterleavedQueue(queue);

      } else {

        update.next = interleaved.next;

        interleaved.next = update;

      }

      queue.interleaved = update;

    } else {

      const pending = queue.pending;

      if (pending === null) {

        // This is the first update. Create a circular list.

        update.next = update;

      } else {

        update.next = pending.next;

        pending.next = update;

      }

      queue.pending = update;

    }


    if (

      fiber.lanes === NoLanes &&

      (alternate === null || alternate.lanes === NoLanes)

    ) {

      const lastRenderedReducer = queue.lastRenderedReducer;

      if (lastRenderedReducer !== null) {

        let prevDispatcher;

        try {

          const currentState = queue.lastRenderedState; // 上一次state

          // 这段逻辑会去进行浅对比,上一个state和当前state相等的话,就return界面不会更新

          const eagerState = lastRenderedReducer(currentState, action);

          update.eagerReducer = lastRenderedReducer;

          update.eagerState = eagerState;

          if (is(eagerState, currentState)) {

            return;

          }

        } catch (error) {

          // Suppress the error. It will throw again in the render phase.

        } finally {}

      }

    }

    

    // 渲染更新

    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);


    if (isTransitionLane(lane) && root !== null) {

      let queueLanes = queue.lanes;

      queueLanes = intersectLanes(queueLanes, root.pendingLanes);

      const newQueueLanes = mergeLanes(queueLanes, lane);

      queue.lanes = newQueueLanes;

      markRootEntangled(root, newQueueLanes);

    }

  }


  if (enableSchedulingProfiler) {

    markStateUpdateScheduled(fiber, lane);

  }

}

这段代码还是比较复杂的,有一些和fiber相关的逻辑。但在这里关于hooks的就是会创建一个update对象然后添加到queue链表上面,然后会判断当前是否处于渲染阶段,不是的话就会去获取上一个state和当前的state进行浅对比,相等就会return不会执行更新,不相等就会执行scheduleUpdateOnFiber进行更新。

useState 更新

上面讲了初始阶段react会给ReactCurrentDispatcher.current赋值HooksDispatcherOnMount,更新阶段赋值HooksDispatcherOnUpdate。在更新阶段实际上调用的是updateState。

function updateState(initialState){

  return updateReducer(basicStateReducer, initialState);

}


function basicStateReducer(state, action){

  // 这里的action就是例子中的Damon。 const [name, SetName] = useState('Damon');

  return typeof action === 'function' ? action(state) : action;

}

其实useState就是个简化版的useReducer,看下useReducer干了些啥。


updateReducer

function updateReducer(

  reducer,

  initialArg,

  init,

){

   //这里是获取当前的hooks,每一次函数更新的时候都会执行到hook,这个方法会保证每次更新状态不丢失

  const hook = updateWorkInProgressHook();

   //拿到更新队列

  const queue = hook.queue;

  invariant(

    queue !== null,

    'Should have a queue. This is likely a bug in React. Please file an issue.',

  );


    // 调用lastRenderedReducer可获取到state

  queue.lastRenderedReducer = reducer;


    // 拿到当前的函数内的hook

  const current = currentHook;


  let baseQueue = current.baseQueue;


  const pendingQueue = queue.pending;

  if (pendingQueue !== null) {

    // 这里主要是把baseQueue和pendingQueue做了交换,然后赋值到current上。

    if (baseQueue !== null) {

      const baseFirst = baseQueue.next;

      const pendingFirst = pendingQueue.next;

      baseQueue.next = pendingFirst;

      pendingQueue.next = baseFirst;

    }

    current.baseQueue = baseQueue = pendingQueue;

    queue.pending = null;

  }


  if (baseQueue !== null) {

    const first = baseQueue.next;

    let newState = current.baseState;


    let newBaseState = null;

    let newBaseQueueFirst = null;

    let newBaseQueueLast = null;

    let update = first;

    // 这里会循环的遍历update

    do {

      const updateLane = update.lane;

      // 这里有涉及到优先级相关到逻辑

      if (!isSubsetOfLanes(renderLanes, updateLane)) {

        const clone = {

          lane: updateLane,

          action: update.action,

          eagerReducer: update.eagerReducer,

          eagerState: update.eagerState,

          next: null,

        };

        if (newBaseQueueLast === null) {

          newBaseQueueFirst = newBaseQueueLast = clone;

          newBaseState = newState;

        } else {

          newBaseQueueLast = newBaseQueueLast.next = clone;

        }

        currentlyRenderingFiber.lanes = mergeLanes(

          currentlyRenderingFiber.lanes,

          updateLane,

        );

        markSkippedUpdateLanes(updateLane);

      } else {

         // This update does have sufficient priority. 

         // 这个更新有足够的优先级

        if (newBaseQueueLast !== null) {

          const clone = {

            lane: NoLane,

            action: update.action,

            eagerReducer: update.eagerReducer,

            eagerState: update.eagerState,

            next: null,

          };

          newBaseQueueLast = newBaseQueueLast.next = clone;

        }

        if (update.eagerReducer === reducer) {

          newState = update.eagerState;

        } else {

          const action = update.action;

          newState = reducer(newState, action);

        }

      }

      update = update.next;

    } while (update !== null && update !== first);


    if (newBaseQueueLast === null) {

      newBaseState = newState;

    } else {

      newBaseQueueLast.next = newBaseQueueFirst;

    }


    if (!is(newState, hook.memoizedState)) {

      markWorkInProgressReceivedUpdate();

    }

  

    // 替换hook上的值

    hook.memoizedState = newState;

    hook.baseState = newBaseState;

    hook.baseQueue = newBaseQueueLast;


    queue.lastRenderedState = newState;

  }


  const lastInterleaved = queue.interleaved;

  if (lastInterleaved !== null) {

    let interleaved = lastInterleaved;

    do {

      const interleavedLane = interleaved.lane;

      currentlyRenderingFiber.lanes = mergeLanes(

        currentlyRenderingFiber.lanes,

        interleavedLane,

      );

      markSkippedUpdateLanes(interleavedLane);

      interleaved = interleaved.next;

    } while (interleaved !== lastInterleaved);

  } else if (baseQueue === null) {

    queue.lanes = NoLanes;

  }


  const dispatch = queue.dispatch;

  return [hook.memoizedState, dispatch];

}

首先获取了当前正在工作的hook,然后把queue.pending合并到baseQueue,这样做其实是有可能新的更新还没有处理,一次更新中可能会有多个setName,所以需要把queue.pending中的update合并到baseQueue内。接着会通过循环遍历链表,执行每一次更新去得到最新的state,把hook对象的值更新到最新。然后返回最新的memoizedState和dispatch。这里的dispatch其实就是dispatchAction,dispatchAction主要是负责更新界面的函数。


updateWorkInProgressHook


react中每一个hook更新阶段都会调用updateWorkInProgressHook来获取当前的hook。

function updateWorkInProgressHook() {

  let nextCurrentHook;

  

  if (currentHook === null) {

    // 第一个hooks会走到这,然后在fiber memoizedState中获取

    // currentlyRenderingFiber.alternate 这个是当前的fiber树

    const current = currentlyRenderingFiber.alternate;

    if (current !== null) {

      nextCurrentHook = current.memoizedState;

    } else {

      nextCurrentHook = null;

    }

  } else {

    // 因为是链表数据结构 其他的hook直接从next获取

    nextCurrentHook = currentHook.next;

  }


  let nextWorkInProgressHook;

  if (workInProgressHook === null) {

    // 跟上面一样 第一次执行hooks时候

    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;

  } else {

    nextWorkInProgressHook = workInProgressHook.next;

  }


  if (nextWorkInProgressHook !== null) {

    workInProgressHook = nextWorkInProgressHook;

    nextWorkInProgressHook = workInProgressHook.next;


    currentHook = nextCurrentHook;

  } else {


    invariant(

      nextCurrentHook !== null,

      'Rendered more hooks than during the previous render.',

    );

    currentHook = nextCurrentHook;


    // 创建一个新的hook

    const newHook: Hook = {

      memoizedState: currentHook.memoizedState,


      baseState: currentHook.baseState,

      baseQueue: currentHook.baseQueue,

      queue: currentHook.queue,


      next: null,

    };


    if (workInProgressHook === null) {

      // 第一个hook添加到memoizedState和workInProgressHook

      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;

    } else {

      // 其他的hook添加到链表末尾

      workInProgressHook = workInProgressHook.next = newHook;

    }

  }

  return workInProgressHook;

}

这段代码的作用主要是当函数每次更新的时候都会执行到hook,需要从fiber树中找到对应的hook然后赋值到workInProgressHook上,这样每次函数更新的时候状态都不会丢失。


总结


最后来个分析的流程图吧(本人水平有限,哈哈哈。欢迎一起交流学习)

image


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

阿里云GTS能力中心(浩鲸智能),从交付的视角探讨数字化转型过程中大型软件开发实践、以及阿里云产品在各行业被集成的案例分享、技术沉淀等内容。敬请关注!

官方博客
官网链接