从源码剖析 useState
的执行过程。
示例代码:
import React, { useState } from 'react';import './App.css';export default function App() {const [count, setCount] = useState(0);const [name, setName] = useState('Star');// 调用三次setCount便于查看更新队列的情况const countPlusThree = () => {setCount(count + 1);setCount(count + 2);setCount(count + 3);};return (<div className="App"><p>{name} Has Clicked <strong>{count}</strong> Times</p><button onClick={countPlusThree}>Click *3</button></div>);}
代码非常简单,点击 button
使 count+3
,count
的值会显示在屏幕上。
本节主要概念:
我们来看一个简单的 Greeting
组件,它支持定义成类和函数两种性质。在使用它时,不用关心他是如何定义的。
// 是类还是函数 —— 无所谓<Greeting /> // <p>Hello</p>
如果 Greeting
是一个函数,React 需要调用它:
// Greeting.jsfunction Greeting() {return <p>Hello</p>;}// React 内部const result = Greeting(props); // <p>Hello</p>
但如果 Greeting
是一个类,React 需要先将其实例化,再调用刚才生成实例的 render
方法:
// Greeting.jsclass Greeting extends React.Component {render() {return <p>Hello</p>;}}// React 内部const instance = new Greeting(props); // Greeting {}const result = instance.render(); // <p>Hello</p>
React 通过以下方式来判断组件的类型:
// React 内部class Component {}Component.prototype.isReactComponent = {};// 检查方式class Greeting extends React.Component {}console.log(Greeting.prototype.isReactComponent); // {}
本节主要概念(了解即可):
Fiber(可译为丝)比线程还细的控制粒度,是 React16 中的新特性,旨在对渲染过程做更精细的调整。
产生原因:
reconciler
(被称为 Stack reconciler)自顶向下的递归 mount/update
,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验React Fiber 的方式:
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
React Fiber 把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。 维护每一个分片的数据结构,就是 Fiber。
有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情
Fiber 的调度过程分为以下两个阶段:
render/reconciliation
阶段 — 里面的所有生命周期函数都可能被执行多次,所以尽量保证状态不变
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
Commit
阶段 — 不能被打断,只会执行一次
componentDidMount
componentDidUpdate
compoenntWillunmount
Fiber 的增量更新需要更多的上下文信息,之前的 VirtualDOM Tree 显然难以满足,所以扩展出了 Fiber Tree(即 Fiber 上下文的 VirtualDOM Tree),更新过程就是根据输入数据以及现有的 Fiber Tree 构造出新的 Fiber Tree(workInProgress tree
)。
与 Fiber 有关的所有代码位于 packages/react-reconciler 中,一个 Fiber 节点的详细定义如下:
function FiberNode(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode) {// Instancethis.tag = tag;this.key = key;this.elementType = null;this.type = null;this.stateNode = null;// Fiberthis.return = null;this.child = null;this.sibling = null;this.index = 0;this.ref = null;this.pendingProps = pendingProps;this.memoizedProps = null;this.updateQueue = null;// 重点this.memoizedState = null;this.contextDependencies = null;this.mode = mode;// Effects/** 细节略 **/}
我们只关注一下 this.memoizedState
。
这个 key
用来存储在上次渲染过程中最终获得的节点的 state
,每次 render
之前,React 会计算出当前组件最新的 state
然后赋值给组件,再执行 render
。类组件和使用 useState
的函数组件均适用。
记住上面这句话,后面还会经常提到 memoizedState
。
有关 Fiber 每个 key 的具体含义可以参见 源码的注释
本节主要概念:
由于 React 体系的复杂性以及目标平台的多样性。react
包只暴露一些定义组件的 API。绝大多数 React 的实现都存在于 渲染器(renderers)中。
react-dom
、react-dom/server
、 react-native
、 react-test-renderer
、 react-art
都是常见的渲染器
这就是为什么不管目标平台是什么,react
包都是可用的。从 react
包中导出的一切,比如 React.Component
、React.createElement
、 React.Children
和 Hooks
都是独立于目标平台的。无论运行 React DOM
,还是 React DOM Server
,或是 React Native
,组件都可以使用同样的方式导入和使用。
所以当我们想使用新特性时,react
和 react-dom
都需要被更新。
例如,当 React 16.3 添加了 Context API,
React.createContext()
API 会被 React 包暴露出来。但是
React.createContext()
其实并没有实现context
。因为在React DOM
和React DOM Server
中同样一个 API 应当有不同的实现。所以createContext()
只返回了一些普通对象: **所以,如果你将react
升级到了 16.3+,但是不更新react-dom
,那么你就使用了一个尚不知道Provider
和Consumer
类型的渲染器。**这就是为什么老版本的react-dom
会报错说这些类型是无效的。
这就是 setState
尽管定义在 React 包中,调用时却能够更新 DOM 的原因。它读取由 React DOM
设置的 this.updater
,让 React DOM
安排并处理更新。
Component.setState = function (partialState, callback) {// setState 所做的一切就是委托渲染器创建这个组件的实例this.updater.enqueueSetState(this, partialState, callback, 'setState');};
各个渲染器中的 updater
触发不同平台的更新渲染
// React DOM 内部const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMUpdater;// React DOM Server 内部const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMServerUpdater;// React Native 内部const inst = new YourComponent();inst.props = props;inst.updater = ReactNativeUpdater;
至于 updater
的具体实现,就不是这里重点要讨论的内容了,下面让我们正式进入本文的主题:React Hooks。
本节主要概念:
useState
是如何被引入以及调用的useState
为什么能触发组件更新所有的 Hooks 在 React.js 中被引入,挂载在 React 对象中
// React.jsimport {useCallback,useContext,useEffect,useImperativeHandle,useDebugValue,useLayoutEffect,useMemo,useReducer,useRef,useState,} from './ReactHooks';
我们进入 ReactHooks.js 来看看,发现 useState
的实现竟然异常简单,只有短短两行
// ReactHooks.jsexport function useState<S>(initialState: (() => S) | S) {const dispatcher = resolveDispatcher();return dispatcher.useState(initialState);}
看来重点都在这个 dispatcher
上,dispatcher
通过 resolveDispatcher()
来获取,这个函数同样也很简单,只是将 ReactCurrentDispatcher.current
的值赋给了 dispatcher
。
// ReactHooks.jsfunction resolveDispatcher() {const dispatcher = ReactCurrentDispatcher.current;return dispatcher;}
所以 useState(xxx)
等价于 ReactCurrentDispatcher.current.useState(xxx)
。
看到这里,我们回顾一下第一章第三小节所讲的 React 渲染器与 setState
,是不是发现有点似曾相识。
与 updater
是 setState
能够触发更新的核心类似,ReactCurrentDispatcher.current.useState
是 useState
能够触发更新的关键原因,这个方法的实现并不在react
包内。下面我们就来分析一个具体更新的例子。
以全文开头给出的代码为例。
我们从 Fiber 调度的开始:ReactFiberBeginwork
来谈起
之前已经说过,React 有能力区分不同的组件,所以它会给不同的组件类型打上不同的 tag
, 详见 shared/ReactWorkTags.js。
所以在 beginWork
的函数中,就可以根据 workInProgess
(就是个 Fiber
节点)上的 tag
值来走不同的方法来加载或者更新组件。
// ReactFiberBeginWork.jsfunction beginWork(current: Fiber | null,workInProgress: Fiber,renderExpirationTime: ExpirationTime,): Fiber | null {/** 省略与本文无关的部分 **/// 根据不同的组件类型走不同的方法switch (workInProgress.tag) {// 不确定组件case IndeterminateComponent: {const elementType = workInProgress.elementType;// 加载初始组件return mountIndeterminateComponent(current,workInProgress,elementType,renderExpirationTime,);}// 函数组件case FunctionComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);// 更新函数组件return updateFunctionComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime,);}// 类组件case ClassComponent {/** 细节略 **/}}}
下面我们来找出 useState
发挥作用的地方。
mount
过程执行 mountIndeterminateComponent
时,会执行到 renderWithHooks
这个函数
function mountIndeterminateComponent(_current,workInProgress,Component,renderExpirationTime,) {/** 省略准备阶段代码 **/// value 就是渲染出来的 APP 组件let value;value = renderWithHooks(null,workInProgress,Component,props,context,renderExpirationTime,);/** 省略无关代码 **/}workInProgress.tag = FunctionComponent;reconcileChildren(null, workInProgress, value, renderExpirationTime);return workInProgress.child;}
执行前:nextChildren = value
执行后:value = 组件的虚拟 DOM 表示
至于这个 value
是如何被渲染成真实的 DOM 节点,我们并不关心,state
值我们已经通过 renderWithHooks
取到并渲染。
点击一下按钮:此时 count
从 0 变为 3。
更新过程执行的是 updateFunctionComponent
函数,同样会执行到 renderWithHooks
这个函数,我们来看一下这个函数执行前后发生的变化:
执行前:nextChildren = undefined
执行后: nextChildren = 更新后的组件的虚拟 DOM 表示
同样的,至于这个 nextChildren
是如何被渲染成真实的 DOM 节点,我们并不关心,最新的 state
值我们已经通过 renderWithHooks
取到并渲染。
所以,renderWithHooks
函数就是处理各种 hooks
逻辑的核心部分。
ReactFiberHooks.js 包含着各种关于 Hooks 逻辑的处理,本章中的代码均来自该文件。
在之前的章节有介绍过,Fiber 中的 memorizedStated
用来存储 state
。
在类组件中 state
是一整个对象,可以和 memoizedState
一一对应。但是在 Hooks 中,React 并不知道我们调用了几次 useState
,所以 React 通过将一个 Hook 对象挂载在 memorizedStated
上来保存函数组件的 state
。
Hook 对象的结构如下:
// ReactFiberHooks.jsexport type Hook = {memoizedState: any,baseState: any,baseUpdate: Update<any, any> | null,queue: UpdateQueue<any, any> | null,next: Hook | null,};
重点关注 memoizedState
和 next
。
memoizedState
是用来记录当前 useState
应该返回的结果的。
queue
:缓存队列,存储多次更新行为next
:指向下一次 useState
对应的 Hook 对象。结合示例代码来看:
import React, { useState } from 'react';import './App.css';export default function App() {const [count, setCount] = useState(0);const [name, setName] = useState('Star');// 调用三次setCount便于查看更新队列的情况const countPlusThree = () => {setCount(count + 1);setCount(count + 2);setCount(count + 3);};return (<div className="App"><p>{name} Has Clicked <strong>{count}</strong> Times</p><button onClick={countPlusThree}>Click *3</button></div>);}
第一次点击按钮触发更新时,memoizedState
的结构如下
只是符合之前对 Hook 对象结构的分析,只是 queue
中的结构貌似有点奇怪,我们将在第三章第 2 节中进行分析。
renderWithHooks
的运行过程如下:
// ReactFiberHooks.jsexport function renderWithHooks(current: Fiber | null,workInProgress: Fiber,Component: any,props: any,refOrContext: any,nextRenderExpirationTime: ExpirationTime): any {renderExpirationTime = nextRenderExpirationTime;currentlyRenderingFiber = workInProgress;// 如果 current 的值为空,说明还没有 hook 对象被挂载// 而根据 hook 对象结构可知,current.memoizedState 指向下一个 currentnextCurrentHook = current !== null ? current.memoizedState : null;// 用 nextCurrentHook 的值来区分 mount 和 update,设置不同的 dispatcherReactCurrentDispatcher.current =nextCurrentHook === null? // 初始化时HooksDispatcherOnMount: // 更新时HooksDispatcherOnUpdate;// 此时已经有了新的 dispatcher,在调用 Component 时就可以拿到新的对象let children = Component(props, refOrContext);// 重置ReactCurrentDispatcher.current = ContextOnlyDispatcher;const renderedWork: Fiber = (currentlyRenderingFiber: any);// 更新 memoizedState 和 updateQueuerenderedWork.memoizedState = firstWorkInProgressHook;renderedWork.updateQueue = (componentUpdateQueue: any);/** 省略与本文无关的部分代码,便于理解 **/}
核心: 创建一个新的 hook,初始化 state
, 并绑定触发器。
初始化阶段 ReactCurrentDispatcher.current
会指向 HooksDispatcherOnMount
对象
// ReactFiberHooks.jsconst HooksDispatcherOnMount: Dispatcher = {/** 省略其它Hooks **/useState: mountState,};// 所以调用 useState(0) 返回的就是 HooksDispatcherOnMount.useState(0),也就是 mountState(0)function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {// 访问 Hook 链表的下一个节点,获取到新的 Hook 对象const hook = mountWorkInProgressHook();// 如果入参是 function 则会调用,但是不提供参数if (typeof initialState === 'function') {initialState = initialState();}// 进行 state 的初始化工作hook.memoizedState = hook.baseState = initialState;// 进行 queue 的初始化工作const queue = (hook.queue = {last: null,dispatch: null,eagerReducer: basicStateReducer, // useState 使用基础 reducereagerState: (initialState: any),});// 返回触发器const dispatch: Dispatch<BasicStateAction<S>,>= (queue.dispatch = (dispatchAction.bind(null,// 绑定当前 fiber 节点和 queue((currentlyRenderingFiber: any): Fiber),queue,));// 返回初始 state 和触发器return [hook.memoizedState, dispatch];}// 对于 useState 触发的 update action 来说(假设 useState 里面都传的变量),basicStateReducer 就是直接返回 action 的值function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;}
重点讲一下返回的这个更新函数 dispatchAction
。
function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {/** 省略 Fiber 调度相关代码 **/// 创建新的新的 update, action 就是我们 setCount 里面的值 (count+1, count+2, count+3…)const update: Update<S, A> = {expirationTime,action,eagerReducer: null,eagerState: null,next: null,};// 重点:构建 queue// queue.last 是最近的一次更新,然后 last.next 开始是每一次的 actionconst last = queue.last;if (last === null) {// 只有一个 update, 自己指自己-形成环update.next = update;} else {const first = last.next;if (first !== null) {update.next = first;}last.next = update;}queue.last = update;/** 省略特殊情况相关代码 **/// 创建一个更新任务scheduleWork(fiber, expirationTime);}
在 dispatchAction
中维护了一份 queue
的数据结构。
queue
是一个有环链表,规则:
queue.last
指向最近一次更新last.next
指向第一次更新last
,形成一个环。所以每次插入新 update
时,就需要将原来的 first
指向 queue.last.next
。再将 update
指向 queue.next
,最后将 queue.last
指向 update
。
下面结合示例代码来画图说明一下:
前面给出了第一次点击按钮更新时,memorizedState
中的 queue
值。
其构建过程如下图所示:
即保证 queue.last
始终为最新的 action
, 而 queue.last.next
始终为 action: 1
**核心:**获取该 Hook 对象中的 queue
,内部存有本次更新的一系列数据,进行更新
更新阶段 ReactCurrentDispatcher.current
会指向 HooksDispatcherOnUpdate
对象
// ReactFiberHooks.js// 所以调用 useState(0) 返回的就是 HooksDispatcherOnUpdate.useState(0),也就是 updateReducer(basicStateReducer, 0)const HooksDispatcherOnUpdate: Dispatcher = {/** 省略其它Hooks **/useState: updateState,}function updateState(initialState) {return updateReducer(basicStateReducer, initialState);}// 可以看到 updateReducer 的过程与传的 initalState 已经无关了,所以初始值只在第一次被使用// 为了方便阅读,删去了一些无关代码// 查看完整代码:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606function updateReducer(reducer, initialArg, init) {// 获取初始化时的 hookconst hook = updateWorkInProgressHook();const queue = hook.queue;// 开始渲染更新if (numberOfReRenders > 0) {const dispatch = queue.dispatch;if (renderPhaseUpdates !== null) {// 获取 Hook 对象上的 queue,内部存有本次更新的一系列数据const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);if (firstRenderPhaseUpdate !== undefined) {renderPhaseUpdates.delete(queue);let newState = hook.memoizedState;let update = firstRenderPhaseUpdate;// 获取更新后的 statedo {const action = update.action;// 此时的 reducer 是 basicStateReducer,直接返回 action 的值newState = reducer(newState, action);update = update.next;} while (update !== null);// 对 更新 hook.memoizedhook.memoizedState = newState;// 返回新的 state,及更新 hook 的 dispatch 方法return [newState, dispatch];}}}// 对于 useState 触发的 update action 来说(假设 useState 里面都传的变量),basicStateReducer 就是直接返回 action 的值function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;
单个 Hooks 的更新行为全都挂在 Hooks.queue
下,所以能够管理好 queue
的核心就在于
mountState
dispatchAction
updateReducer
结合示例代码:
[count, setCount] = useState(0)
时,创建一个 queue
setCount(x)
,就 dispach
一个内容为 x
的 action
(action
的表现为:将 count
设为 x
),action
存储在 queue
中,以前面讲述的有环链表规则来维护action
最终在 updateReducer
中被调用,更新到 memorizedState
上,使我们能够获取到最新的 state
值官方文档对于使用 hooks 有以下两点要求:
以 useState
为例:
和类组件存储 state 不同,React 并不知道我们调用了几次 useState
,对 hooks
的存储是按顺序的(参见 Hook 结构),一个 hook
对象的 next
指向下一个 hooks
。所以当我们建立示例代码中的对应关系后,Hook
的结构如下:
// hook1: const [count, setCount] = useState(0) — 拿到state1{memorizedState: 0;next: {// hook2: const [name, setName] = useState('Star') - 拿到state2memorizedState: 'Star';next: {null;}}}// hook1 => Fiber.memoizedState// state1 === hook1.memoizedState// hook1.next => hook2// state2 === hook2.memoizedState
所以如果把 hook1
放到一个 if
语句中,当这个没有执行时,hook2
拿到的 state
其实是上一次 hook1
执行后的 state
(而不是上一次 hook2
执行后的)。这样显然会发生错误。
关于这块内容如果想了解更多可以看一下 这篇文章
只有函数组件的更新才会触发 renderWithHooks
函数,处理 Hooks 相关逻辑。
还是以 setState
为例,类组件和函数组件重新渲染的逻辑不同 :
setState
触发 updater
,重新执行组件中的 render
方法useState
返回的 setter
函数来 dispatch
一个 update action
,触发更新(dispatchAction
最后的 scheduleWork
),用 updateReducer
处理更新逻辑,返回最新的 state
值(与 Redux 比较像)说了这么多,最后再简要总结下 useState
的执行流程。
dispatcher
函数和初始值。dispatcher
函数,按序插入 update
(其实就是一个 action
)update
,调度一次 React 的更新ReactCurrentDispatcher.current
指向负责更新的 Dispatcher
App()
时,useState
会被重新执行,在 resolve dispatcher
的阶段拿到了负责更新的 dispatcher
。useState
会拿到 Hook 对象,Hook.queue
中存储了更新队列,依次进行更新后,即可拿到最新的 state
App()
执行后返回的 nextChild
中的 count
值已经是最新的了。FiberNode
中的 memorizedState
也被设置为最新的 state