React v16源码之useState(Hook开篇)

前言

React Hook是16.8之后新增的新特性,在允许你在不编写class组件的情况下使用state和其他React特性,在16.8之前的版本中组件按照其定义方式可以分成class组件和函数组件,class组件可以使用内部状态state以及相关生命周期,函数组件数据来源只是外部prop。简单来说React Hook = 函数组件 + state/其他特性。关于为什么提供React Hook的相关介绍可以通过官网相关文章(Hook官网介绍)来了解,这里就不再赘述了。

本文是一系列Hook梳理的开篇,旨在了解React Hook相关内部实现、执行时机等来加深对其的理解。

React Hook处理简述

使用React Hook来编写组件,常用的hook有useState、useEffect、useLayoutEffect、useCallback、useContext、useRef等,这里会对每一个hook做梳理。在之前的Render处理文章中,知道了在React运行中会存在update对象,该对象的载荷就是组件的内容,而组件的执行逻辑是在performUnitOfWork下面的beginWork的处理逻辑中,相关组件处理逻辑如下:

switch (workInProgress.tag) {
	case IndeterminateComponent:
		...
    case LazyComponent:
    	...
    case FunctionComponent:
    case ClassComponent:
    case HostRoot:
    case HostComponent:
    case HostText:
    case SuspenseComponent:
    case HostPortal:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextProvider:
    case ContextConsumer:
   	case MemoComponent:
    case SimpleMemoComponent:
    case SuspenseListComponent:
}

就是针对于不同组件做不同的处理,需要注意的是这里的组件类型是React内部的相关区分,其中IndeterminateComponent就比较特殊,这里以React Hook组件为例(实际上就是函数)看看这边的处理:

function Hello() {
	const [date, setDate] = React.useState(Date.now())
    return (
    	React.createElement(
          'button',
          {
            onClick: () => {
              setDate(111)
            }
          },
          date,
        )
    )
}

实际上代码执行到beginWork,React使用Hook的函数组件的对应的类型就是IndeterminateComponent,会调用mountIndeterminateComponent来处理。

mountIndeterminateComponent

IndeterminateComponent类型的组件会调用该函数来处理相关逻辑,而该函数中主要的处理逻辑如下:

var value = renderWithHooks(...)
if (typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined) {
	...
	workInProgress.tag = ClassComponent;
	...
} else {
	...
	workInProgress.tag = FunctionComponent;
	...
	reconcileChildren(...)
	...
}

renderWithHooks简单概述就是函数组件执行,value值就是函数返回的是React元素,如果函数组件中返回是一个带有render函数的对象并且类型不知道就会被判定是class组件,否则就是FunctionComponent。

带有hook的函数组件就是通过该函数来处理,其中renderWithHooks中处理函数执行外,还有一个非常重要的逻辑点就是设置ReactCurrentDispatcher.current的值,具体逻辑如下:

function renderWithHooks() {
	...
   {
       if (current !== null && current.memoizedState !== null) {
         ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
       } else if (hookTypesDev !== null) {
         ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
       } else {
         ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
       }
    }
    var children = Component(props, secondArg);
    ...
}

从上面的逻辑中可知,挂载阶段ReactCurrentDispatcher.current会被赋值为HooksDispatcherOnMountInDEV或HooksDispatcherOnMountWithHookTypesInDEV,其中后者是处理边缘情况的正常挂载阶段都是HooksDispatcherOnMountInDEV,当后面函数组件执行时,其获取到的dispatcher就是这个了。

useState

该hook提供内部状态state的功能,state更新就会触发组件渲染继而视图更新,在React源码中useState的直接逻辑非常简单:

function useState(initialState) {
	// 获取ReactCurrentDispatcher.current值
	var dispatcher = resolveDispatcher();
	// 调用dispatcher的useState方法
    return dispatcher.useState(initialState);
}

而dispatcher对应的useState方法的具体逻辑如下:

{
	useState: function (initialState) {
		...
        var prevDispatcher = ReactCurrentDispatcher.current;
        // 临时替换dispatcher
        ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
        try {
           return mountState(initialState);
        } finally {
           ReactCurrentDispatcher.current = prevDispatcher;
        }
    },
}

最核心的处理还是调用mountState函数,该函数的主要逻辑如下:

function mountState(initialState) {
	var hook = mountWorkInProgressHook();
	
    if (typeof initialState === 'function') {
       initialState = initialState();
    }
    hook.memoizedState = hook.baseState = initialState;
    var queue = hook.queue = {
       pending: null,
       dispatch: null,
       lastRenderedReducer: basicStateReducer,
       lastRenderedState: initialState
    };
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
}

在该函数中会创建hook对象和queue对象,其中hook对象的属性有:

 var hook = {
 	memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
 };

需要注意的是这个hook对象会被添加到对应的Fiber对象上,通过memoizedState属性。useState值支持函数,初始化时initialState会被赋值给memoizedState、baseState属性,useState返回值实际上memoizedState属性的值,而其set方法是调用dispatchAction来实现的。

挂载阶段的过程React Hook的函数组件的处理并并没有什么大的区别,还是那一套可以看之前的文章详细了解

dispatchAction更新

该函数是useState返回的state更新的实际处理函数,该函数相关逻辑具体如下:

function dispatchAction(fiber, queue, action) {
	...
}

首先是相关参数说明,当调用useState后使用bind实现函数绑定,fiber、queue是已经被传递了,action就是开发者需要更新的state数据。接下来看看具体的逻辑:

function dispatchAction(fiber, queue, action) {
	 var update = {
       expirationTime: expirationTime,
       suspenseConfig: suspenseConfig,
       action: action,
       eagerReducer: null,
       eagerState: null,
       next: null,
       priority: getCurrentPriorityLevel()
     };
      
     var 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;
     }

	 var alternate = fiber.alternate;
 
     if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
     	...
     	// 渲染阶段更新相关处理,主要逻辑就是更新expirationTime属性值
     	
     } else {
     	...
     	scheduleWork(fiber, expirationTime)
     }
}

实际上面主要逻辑可以总结下面几点:

  • 创建update对象,涉及到相关执行时间、优先级等顺序
  • update队列排序,通过next构成链式结构
  • scheduleWork开始调度作业

在scheduleWork之前,会处理state相关逻辑,其中就包括:

  • 支持更新state传递函数,即action是函数的情况,会向函数中传递旧的state值
  • 通过Object.is对新旧state做对比,如果没有变化直接退出不会执行相关逻辑,是一种优化手段

对于useState hook来说,scheduleWork的处理流程理论上应该与class组件的setState逻辑相似,但是因为存在hook可能流程是有不同的处理,这里主要看下。

useState中scheduleWork相关处理

通过实例Debug实际上useState中scheduleWork的处理逻辑与class组件中setState的处理逻辑并没有什么大的区别,整体的处理还是:

scheduleWork -> scheduleUpdateOnFiber -> ensureRootIsScheduled -> scheduleSyncCallback -> flushSyncCallbackQueueImpl -> performSyncWorkOnRoot

注意flushSyncCallbackQueueImpl的触发时机并不是由setState来触发的,在performSyncWorkOnRoot函数中就会调用workLoopSync对所有组件进行更新渲染,在这个过程中class组件会再次调用render函数,纯函数组件会再次执行。那么存在React Hook的函数组件呢?也是会再次执行,那么问题来了这个过程中useState是如何处理的呢?接下来就看看更新阶段useState的处理过程。

更新渲染时useState处理流程

存在hook的函数组件在组件更新阶段会再次被调用,其中useState的逻辑是否存在区别呢?答案必然是存在的,主要是通过dispatcher的不同来控制的。而更新阶段的dispatcher的设置是在renderWithHooks中处理的,需要注意的是更新阶段此时带hook的函数组件就不是挂载阶段对应的IndeterminateComponent类型了,而是FunctionComponent类型。其对应的处理逻辑如下:

   case FunctionComponent:
         {
           var _Component = workInProgress.type;
           var unresolvedProps = workInProgress.pendingProps;
           var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
           return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderExpirationTime);
         }

可以先不看这边的具体处理,主要关注current参数,此时这个参数是通过beginWork传递过来的,本质上是Fiber对象alternate的属性即FiberNode对象。所以在更新阶段renderWithHooks的current是有值的,具体逻辑如下:

function renderWithHooks() {
	...
	{
       if (current !== null && current.memoizedState !== null) {
         ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
       }
     }
     var children = Component(props, secondArg); // Check if there was a render phase update
 
}

在更新阶段ReactCurrentDispatcher.current值是HooksDispatcherOnUpdateInDEV,后面函数执行时useState获取到的dispatcher就是HooksDispatcherOnUpdateInDEV,该对象useState方法的具体逻辑是:

HooksDispatcherOnUpdateInDEV = {
	useState: function (initialState) {
         currentHookNameInDev = 'useState';
         updateHookTypesDev();
         var prevDispatcher = ReactCurrentDispatcher.current;
         ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
 
         try {
           return updateState(initialState);
         } finally {
           ReactCurrentDispatcher.current = prevDispatcher;
         }
    },
}

会调用updateState函数来具体处理相关逻辑,其主要逻辑如下:

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

需要注意此时这个initialState函数挂载阶段时的初始值,而不是要更新的值。而且你会发现在再次调用useState传入的值竟然被丢弃了没有任何处理,如果此时传入的是一个新值,实际上是不生效的,useState对于新值得赋值必须是通过set方法来操作的。实际上有个场景就会存在问题:

function Child(props) {
	const [date, setDate] = useState(props.date)
	...
}

子组件使用父组件的props值作为useState的初始值,当props更新后此时useState内部是按照updateState来处理的,这个值就会被丢弃。如果要在子组件中依赖props维护自己的state,那么你只能调用set方法来处理了,此时可以借助useEffect或useMemo,这样会多一次渲染,但是有些场景可能导致其他问题,需要特别注意。

updateReducer函数的处理逻辑

更新阶段函数组件再次被调用最后会执行updateReducer函数,而该函数的主要处理逻辑如下:

function updateReducer(reducer, initialArg, init) {
     var hook = updateWorkInProgressHook();
     var queue = hook.queue;
     ...
     // 相关处理,逻辑相对多些
     queue.lastRenderedReducer = reducer;
     // queue的pending存储的就是当前最新值
     var pendingQueue = queue.pending;
     if (pendingQueue !== null) {
     	...
        baseQueue = pendingQueue;
       	queue.pending = null;
     }
     if (baseQueue !== null) {
     	...
     	hook.memoizedState = newState;
        hook.baseState = newBaseState;
        hook.baseQueue = newBaseQueueLast;
        queue.lastRenderedState = newState;
     }
     ...
     var dispatch = queue.dispatch;
     return [hook.memoizedState, dispatch];
   }

首先是调用updateWorkInProgressHook获取到挂载阶段定义的hook对象即保留了旧值的,上面中间逻辑实际上就是使用新值替换旧值,即将hook对象的memoizedState、baseState替换成新值,之后返回对应的新值和set方法。通过hook的queue的pending拿到setState时创建的update对象,该对象中action就是最新的值,而旧的hook对象则是通过从Fiber对象上获取的,即Fiber对象上保存了旧的hook对象,实际上是FiberNode对象的memoizedState属性的值。

问题说明
使用useState定义多个同时调用其set方法会导致函数执行多次吗?

不会,以事件处理程序中多次调用setState为例,在整个处理过程中setState不会直接触发视图渲染,视图渲染是通过performSyncWorkOnRoot来处理的,而更新阶段中该函数是通过flushSyncCallbackQueueImpl来触发的,而flushSyncCallbackQueueImpl函数的触发有很多情况,React事件处理保证在执行完所有同步代码后才触发视图渲染,这样在事件处理程序中所有setState都会执行完毕然后才进行一次视图渲染即调用flushSyncCallbackQueueImpl,其他flushSyncCallbackQueueImpl函数的触发逻辑可能与事件机制类似,理论上都应该是一次渲染要尽可能处理完所有setState避免频繁的视图渲染造成的性能问题

setState不会触发flushSyncCallbackQueueImpl调用从而视图渲染,而是通过其他机制不同时机触发的,从而控制渲染次数

setState相同值多次调用是如何处理?

在class组件中setState背后会是合并操作,既将多个setState合并也将其值合并,而useState中setState中值是替换操作。由前面的梳理实际上关心的是多个相同值都会触发scheduleWork调度作业吗?答案会,但是scheduleWork的执行逻辑是不同的,最主要的一点是只有第一次setState将performSyncWorkOnRoot放入syncQueue中,而后面的setState就不会存在此逻辑

setState支持传递函数,那么是如何做到累积生效?

useState中set操作和class组件中的setState背后的执行逻辑是存在不同的,其中useState的set更新是替换,但是当是函数时跟class组件中setState是相同效果即累加。实际上是通过hook对象的queue中next来形成链式结构把每一次setState的action保留下来,在更新渲染阶段再次调用函数时useState内部就会处理这个链式结构,从而得到累加的效果。

总结

通过上面整体梳理,知道useState的整体处理的主要逻辑点以及更新阶段如何做到更新的,这里总结下:

  • 初始化阶段,useState对应的dispacther核心逻辑是调用mountState,该函数会创建hook和queue对象,这两个对象是构建useState链式结构的核心
  • useState支持传递函数,函数的行为和class组件中setState传递函数相同会存在累加即所谓的同步行为
  • 更新阶段,通过set方法来更新实际上是通过更改dispatcher来实现的,更新阶段最后会调用updateState来实现更新
  • useState返回的更新方法对应着内部的dispatchAction方法,该函数会创建update对象最后调用scheduleWork开始调度作业,触发视图渲染的两个核心update对象和scheduleWork
  • useState的背后处理实际上和class组件的setState背后的处理逻辑相似,多个state更新的合并等特点也是一致的,需要注意的是setState本身不会触发视图渲染,而是通过其他机制在后续逻辑中触发的,所以从形式上表现出异步效果,但实际上setState本身是同步处理的,所谓的异步也不是调用异步API,实际上setState逻辑处理中也没有调用异步API,这种异步更准确的说是延迟,是由于相关机制导致的
  • 如果useState依赖父组件的props,那么注意更新阶段是无法生效的,可以通过useEffect/useMemo + set来触发更新,这样会多一次渲染,但是有些场景可能会存在其他问题,可以不维护自己的state来完成一些场景的逻辑
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值