How does useOptimistic() work internally in React?
为了更容易理解这个钩子,我建议首先查看以下帖子。
乐观UI是UI开发中常用的一种技巧,用于提高感知的性能。
useOptimistic()
是一个帮助构建它的钩子。让我们来看看它内部是如何工作的。
1. 什么是乐观UI?
假设我们要实现一个用户可以点击来点赞或取消点赞的”点赞”按钮,你会怎么做?
基本的思路很简单,当按钮被点击时我们调用API,并在API响应后更新UI。这种方法可行,但可能会非常卡顿,试试下面的演示吧。
这是点击处理器。
const toggle = () => {if (isFetching) returnsetIsFetching(true)mutateAPI({liked: !liked}, forceResult).then((data) => {setLiked(data.liked)Update state upon API response
}).catch((reason) => {toast(reason)}).finally(() => {setIsFetching(false)})}
const toggle = () => {if (isFetching) returnsetIsFetching(true)mutateAPI({liked: !liked}, forceResult).then((data) => {setLiked(data.liked)Update state upon API response
}).catch((reason) => {toast(reason)}).finally(() => {setIsFetching(false)})}
我们希望为用户提供即时反馈,但由于API请求的往返,这在技术上是不可能的,除非我们在不等待API响应的情况下更新UI。这实际上是好的,因为大多数时候我们都希望API获取能够成功。
所以在点击处理器中我们可以:
- 立即更新UI
- 触发API调用
- 在API响应(成功或错误)后,再次更新UI。
下面是一个启用了乐观UI的演示,它在感知性能上有了巨大的提升。
The change is as below.
const [liked, setLiked] = useState(false)const [optimisticLiked, setOptimisticLiked] = useState(liked)我们需要分别存储真实状态和乐观状态
以处理竞态条件
// this better be properly handled by AbortController// for demo purpose,// here I used a unique call id to only trust the last api callconst callIdRef = useRef(null)const toggle = () => {setOptimisticLiked(!optimisticLiked)const callId = uid()callIdRef.current = callIdmutateAPI({liked: !optimisticLiked}, forceResult).then((data) => {if (callIdRef.current === callId) {setOptimisticLiked(data.liked)setLiked(data.liked)}Discard the request except the last one
}).catch((reason) => {if (callIdRef.current === callId) {setOptimisticLiked(liked)}toast(reason)})}
const [liked, setLiked] = useState(false)const [optimisticLiked, setOptimisticLiked] = useState(liked)我们需要分别存储真实状态和乐观状态
以处理竞态条件
// this better be properly handled by AbortController// for demo purpose,// here I used a unique call id to only trust the last api callconst callIdRef = useRef(null)const toggle = () => {setOptimisticLiked(!optimisticLiked)const callId = uid()callIdRef.current = callIdmutateAPI({liked: !optimisticLiked}, forceResult).then((data) => {if (callIdRef.current === callId) {setOptimisticLiked(data.liked)setLiked(data.liked)}Discard the request except the last one
}).catch((reason) => {if (callIdRef.current === callId) {setOptimisticLiked(liked)}toast(reason)})}
以上代码仅用于演示,它并非无bug。
连续点击/竞态条件需要在两种方法中都得到适当的处理。你可以看到,要做对其实非常棘手。 在第一个演示中,添加了一个加载状态以防止多个进行中的请求。在第二个演示中,除最后一个请求外的所有请求都被丢弃。
2. 数据获取库通常内置了对乐观UI的支持。
看看我们的代码,乐观UI就是在API请求之前更新状态。所以,数据获取库支持乐观更新感觉很自然。
Relay通过optimisticUpdater()
in mutation支持这一点。
function StoryLikeButton({story}) {...function onLikeButtonClicked(newDoesLike) {commitMutation({variables: {id: data.id,doesViewerLike: newDoesLike,},optimisticUpdater: store => {// TODO fill in optimistic updater},})}...}
function StoryLikeButton({story}) {...function onLikeButtonClicked(newDoesLike) {commitMutation({variables: {id: data.id,doesViewerLike: newDoesLike,},optimisticUpdater: store => {// TODO fill in optimistic updater},})}...}
swr也通过其mutate()
中的optimisticData
选项支持它。
<buttontype="submit"onClick={async () => {setText("");const newTodo = {id: Date.now(),text};try {// Update the local state immediately and fire the// request. Since the API will return the updated// data, there is no need to start a new revalidation// and we can directly populate the cache.await mutate(addTodo(newTodo), {optimisticData: [...data, newTodo],rollbackOnError: true,populateCache: true,revalidate: false});toast.success("Successfully added the new item.");} catch (e) {// If the API errors, the original data will be// rolled back by SWR automatically.toast.error("Failed to add the new item.");}}}>Add</button>
<buttontype="submit"onClick={async () => {setText("");const newTodo = {id: Date.now(),text};try {// Update the local state immediately and fire the// request. Since the API will return the updated// data, there is no need to start a new revalidation// and we can directly populate the cache.await mutate(addTodo(newTodo), {optimisticData: [...data, newTodo],rollbackOnError: true,populateCache: true,revalidate: false});toast.success("Successfully added the new item.");} catch (e) {// If the API errors, the original data will be// rolled back by SWR automatically.toast.error("Failed to add the new item.");}}}>Add</button>
我们可以通过创建一个支持乐观更新器的API包装器来重写我们的点赞按钮演示。
import {useState, useRef, useEffect} from 'react'export function useAPI(fetcher) {const [state, setState] = useState({})const [optimisticState, setOptimisticState] = useState(state)const mutateIdRef = useRef(null)useEffect(() => {}, [])const mutate = (updater, options = {}) => {const mutateId = uid()mutateIdRef.current = mutateIdif (options.optimisticData) {setOptimisticState(state => ({...optimisticState,data: options.optimisticData}))}处理乐观更新
updater().then((data) => {if (mutateIdRef.current === mutateId) {setState(state => ({...state,data}))setOptimisticState(state => ({...optimisticState,data}))}}).catch((error) => {if (options.onError) {options.onError(error)}if (mutateIdRef.current === mutateId && options.rollbackOnError) {setOptimisticState(state)回滚到真实状态
}})}return {data: optimisticState.data,error: state.error,mutate}}const uid = (() => {let i = 0return () => i++})()
import {useState, useRef, useEffect} from 'react'export function useAPI(fetcher) {const [state, setState] = useState({})const [optimisticState, setOptimisticState] = useState(state)const mutateIdRef = useRef(null)useEffect(() => {}, [])const mutate = (updater, options = {}) => {const mutateId = uid()mutateIdRef.current = mutateIdif (options.optimisticData) {setOptimisticState(state => ({...optimisticState,data: options.optimisticData}))}处理乐观更新
updater().then((data) => {if (mutateIdRef.current === mutateId) {setState(state => ({...state,data}))setOptimisticState(state => ({...optimisticState,data}))}}).catch((error) => {if (options.onError) {options.onError(error)}if (mutateIdRef.current === mutateId && options.rollbackOnError) {setOptimisticState(state)回滚到真实状态
}})}return {data: optimisticState.data,error: state.error,mutate}}const uid = (() => {let i = 0return () => i++})()
Here is the demo with our new useAPI()
.
演示看起来不错,按钮组件变得更加简洁。
const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))const liked = !!data?.likedconst toggle = () => {const payload = {liked: !liked}mutate(() => mutateAPI(payload, forceResult),{optimisticData: payload,rollbackOnError: true,onError: toast})}
const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))const liked = !!data?.likedconst toggle = () => {const payload = {liked: !liked}mutate(() => mutateAPI(payload, forceResult),{optimisticData: payload,rollbackOnError: true,onError: toast})}
好的,对于乐观UI的背景知识已经足够了。现在让我们深入到React的世界。🤿
3. 尝试自己实现useOptimistic()
。
const [messages, setMessages] = useState([{ text: "Hello there!", sending: false, key: 1 }]);稳定时状态的真实来源
async function sendMessage(formData) {const sentMessage = await deliverMessage(formData.get("message"));setMessages((messages) => [...messages, { text: sentMessage }]);}const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,{text: newMessage,sending: true}]);我们可以乐观地修改的状态的副本
async function formAction(formData) {addOptimisticMessage(formData.get("message"));更新乐观状态
formRef.current.reset();await sendMessage(formData);触发变更API
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
const [messages, setMessages] = useState([{ text: "Hello there!", sending: false, key: 1 }]);稳定时状态的真实来源
async function sendMessage(formData) {const sentMessage = await deliverMessage(formData.get("message"));setMessages((messages) => [...messages, { text: sentMessage }]);}const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,{text: newMessage,sending: true}]);我们可以乐观地修改的状态的副本
async function formAction(formData) {addOptimisticMessage(formData.get("message"));更新乐观状态
formRef.current.reset();await sendMessage(formData);触发变更API
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
在我们的乐观UI实现中,当接收到API响应时,乐观状态会被实际状态覆盖。
从上面的代码,我们可以假设当状态参数(messages
)改变时,乐观状态会被重置。这是我们在useState()
或useReducer()
中没有的能力 -
我们不能直接修改状态,必须通过setState()
。
如果让我实现类似的功能,可能会得到以下的代码。
function useOptimistic(state, optimisticDispatcher) {const [optimisticState, setState] = useState(state)useLayoutEffect(() => {setState(optimisticState)}, [state])我们只能通过setState()更新状态
const dispatch = (action) => {setState((state) => optimisticDispatcher(state, action))}return [optimisticState,dispatch]}
function useOptimistic(state, optimisticDispatcher) {const [optimisticState, setState] = useState(state)useLayoutEffect(() => {setState(optimisticState)}, [state])我们只能通过setState()更新状态
const dispatch = (action) => {setState((state) => optimisticDispatcher(state, action))}return [optimisticState,dispatch]}
这是官方演示,其中的useOptimistic()
被我们的实现替换。
如果你尝试上面的演示,你会发现它实际上并不工作如果你尝试上面的演示,你会发现它实际上并不工作 - 乐观更新根本没有被渲染!问题在于<form/>
上的action
!
如果我们切换到老式的onSubmit()
,它有点能工作。
async function formAction(e) {e.preventDefault()const formData = new FormData(formRef.current)addOptimisticMessage(formData.get("message"));formRef.current.reset();await sendMessage(formData);}<form onSubmit={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
async function formAction(e) {e.preventDefault()const formData = new FormData(formRef.current)addOptimisticMessage(formData.get("message"));formRef.current.reset();await sendMessage(formData);}<form onSubmit={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
我说它”有点能工作”是因为它实际上并不完全像useOptimistic()
那样工作 - 尝试快速输入多条消息,你会注意到它与官方演示的差异。
4. useOptimistic()
在React内部是如何工作的?
useOptimistic()
的内部机制相当复杂。让我们先看看官方的总结。
它接受一些状态作为参数,并返回该状态的副本,该副本在异步操作(如网络请求)的期间可能会有所不同。 你提供一个函数,该函数接受当前状态和操作的输入,并返回在操作待定时要使用的乐观状态。
所以useOptimistic()
应该在异步操作下使用,它必须与过渡有关。
关于过渡的更多信息,请参考React中的useTransition()内部是如何工作的?。
4.1 mountOptimistic()
像往常一样,让我们从初始挂载开始。
function mountOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = mountWorkInProgressHook();hook.memoizedState = hook.baseState = passthrough;const queue: UpdateQueue<S, A> = {pending: null,lanes: NoLanes,dispatch: null,// Optimistic state does not use the eager update optimization.lastRenderedReducer: null,lastRenderedState: null,};hook.queue = queue;// This is different than the normal setState function.const dispatch: A => void = (dispatchOptimisticSetState.bind(null,currentlyRenderingFiber,true,queue,): any);queue.dispatch = dispatch;return [passthrough, dispatch];基本上与
useState()
相同!}function dispatchOptimisticSetState<S, A>(fiber: Fiber,throwIfDuringRender: boolean,queue: UpdateQueue<S, A>,action: A,): void {const transition = requestCurrentTransition();const update: Update<S, A> = {// An optimistic update commits synchronously.lane: SyncLane,在
useState()
中,这个通道是由requestUpdateLane()
确定的,它可以是任何优先级
但在这里它只使用
SyncLane
。我猜是因为useOptimistic()应该在异步操作中使用,如果不设置
为SynLane,它将会是过渡通道,永远不会被提交
// After committing, the optimistic update is "reverted" using the same// lane as the transition it's associated with.revertLane: requestTransitionLane(transition),
revertLane
只存在于useOptimistic()
中。它是为了告诉React当
revertLane
正在被处理时,乐观更新应该被撤销。
action,hasEagerState: false,eagerState: null,next: (null: any),};if (isRenderPhaseUpdate(fiber)) {} else {const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);与
useState()
相同,它将更新入队if (root !== null) {// NOTE: The optimistic update implementation assumes that the transition// will never be attempted before the optimistic update. This currently// holds because the optimistic update is always synchronous. If we ever// change that, we'll need to account for this.scheduleUpdateOnFiber(root, fiber, SyncLane);现在在SyncLane上的重新渲染已经被安排
// Optimistic updates are always synchronous, so we don't need to call// entangleTransitionUpdate here.}}}
function mountOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = mountWorkInProgressHook();hook.memoizedState = hook.baseState = passthrough;const queue: UpdateQueue<S, A> = {pending: null,lanes: NoLanes,dispatch: null,// Optimistic state does not use the eager update optimization.lastRenderedReducer: null,lastRenderedState: null,};hook.queue = queue;// This is different than the normal setState function.const dispatch: A => void = (dispatchOptimisticSetState.bind(null,currentlyRenderingFiber,true,queue,): any);queue.dispatch = dispatch;return [passthrough, dispatch];基本上与
useState()
相同!}function dispatchOptimisticSetState<S, A>(fiber: Fiber,throwIfDuringRender: boolean,queue: UpdateQueue<S, A>,action: A,): void {const transition = requestCurrentTransition();const update: Update<S, A> = {// An optimistic update commits synchronously.lane: SyncLane,在
useState()
中,这个通道是由requestUpdateLane()
确定的,它可以是任何优先级
但在这里它只使用
SyncLane
。我猜是因为useOptimistic()应该在异步操作中使用,如果不设置
为SynLane,它将会是过渡通道,永远不会被提交
// After committing, the optimistic update is "reverted" using the same// lane as the transition it's associated with.revertLane: requestTransitionLane(transition),
revertLane
只存在于useOptimistic()
中。它是为了告诉React当
revertLane
正在被处理时,乐观更新应该被撤销。
action,hasEagerState: false,eagerState: null,next: (null: any),};if (isRenderPhaseUpdate(fiber)) {} else {const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);与
useState()
相同,它将更新入队if (root !== null) {// NOTE: The optimistic update implementation assumes that the transition// will never be attempted before the optimistic update. This currently// holds because the optimistic update is always synchronous. If we ever// change that, we'll need to account for this.scheduleUpdateOnFiber(root, fiber, SyncLane);现在在SyncLane上的重新渲染已经被安排
// Optimistic updates are always synchronous, so we don't need to call// entangleTransitionUpdate here.}}}
所以一般来说,对于初始挂载,updateOptimistic()
与useState()
没有太大的不同。主要的区别是它的更新有revertLane
。如果渲染通道不是revertLane
,那么更新应该被提交。否则,它应该被撤销。
4.2 updateOptimistic()
我们将在初始挂载后的渲染中找到revertLane
是如何工作的。
function updateOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = updateWorkInProgressHook();return updateOptimisticImpl(hook,((currentHook: any): Hook),passthrough,reducer,);}function updateOptimisticImpl<S, A>(hook: Hook,current: Hook | null,passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {// Optimistic updates are always rebased on top of the latest value passed in// as an argument. It's called a passthrough because if there are no pending// updates, it will be returned as-is.//// Reset the base state to the passthrough. Future updates will be applied// on top of this.hook.baseState = passthrough;这与
useState()
不同。它重置了基础状态这解决了我们提到的问题 - 无法更新状态
除了setState()之外
// If a reducer is not provided, default to the same one used by useState.const resolvedReducer: (S, A) => S =typeof reducer === 'function' ? reducer : (basicStateReducer: any);return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);再次,它与
useState()
非常相似}
function updateOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = updateWorkInProgressHook();return updateOptimisticImpl(hook,((currentHook: any): Hook),passthrough,reducer,);}function updateOptimisticImpl<S, A>(hook: Hook,current: Hook | null,passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {// Optimistic updates are always rebased on top of the latest value passed in// as an argument. It's called a passthrough because if there are no pending// updates, it will be returned as-is.//// Reset the base state to the passthrough. Future updates will be applied// on top of this.hook.baseState = passthrough;这与
useState()
不同。它重置了基础状态这解决了我们提到的问题 - 无法更新状态
除了setState()之外
// If a reducer is not provided, default to the same one used by useState.const resolvedReducer: (S, A) => S =typeof reducer === 'function' ? reducer : (basicStateReducer: any);return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);再次,它与
useState()
非常相似}
状态钩子保存了一个不同优先级的更新列表, 它们在不同的批次中被处理,所以需要以下属性。
baseQueue
:所有的更新baseState
:更新的基础状态,因为更新像中间件一样按顺序处理。memoizedState
:在渲染中要使用的实际值。如果有待处理的更新,这个值是中间值。
updateReducer()
如何工作的更多细节,请参考 React中的useState()内部是如何工作的? 这解释了为什么我们需要在updateOptimisticImpl()
中更新baseState
。
因为队列中的更新应该是乐观的,意味着它们应该是中间状态,如果我们在这些更新后面放一个
关键更新,它会导致错误的状态。
function updateReducerImpl<S, A>(这与
useState()
相同,内部与useReducer()
相同hook: Hook,current: Hook,reducer: (S, A) => S,): [S, Dispatch<A>] {const baseState = hook.baseState;if (baseQueue === null) {} else {// We have a queue to process.const first = baseQueue.next;let newState = baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast: Update<S, A> | null = null;let update = first;let didReadFromEntangledAsyncAction = false;do {Updates are processed in this loop
// An extra OffscreenLane bit is added to updates that were made to// a hidden tree, so that we can distinguish them from updates that were// already there when the tree was hidden.const updateLane = removeLanes(update.lane, OffscreenLane);const isHiddenUpdate = updateLane !== update.lane;// Check if this update was made while the tree was hidden. If so, then// it's not a "base" update and we should disregard the extra base lanes// that were added to renderLanes when we entered the Offscreen tree.const shouldSkipUpdate = isHiddenUpdate? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane): !isSubsetOfLanes(renderLanes, updateLane);这一行很重要,它表示如果当前的渲染通道不是
lane of update, then we should skip the update.
if (shouldSkipUpdate) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.const clone: Update<S, A> = {lane: updateLane,revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,updateLane,);markSkippedUpdateLanes(updateLane);跳过并不意味着忽视,更新被克隆并放入队列等待下一次渲染
} else {// This update does have sufficient priority.// Check if this is an optimistic update.const revertLane = update.revertLane;if (!enableAsyncActions || revertLane === NoLane) {如果不是乐观更新
// This is not an optimistic update, and we're going to apply it now.// But, if there were earlier updates that were skipped, we need to// leave this update in the queue so it can be rebased later.if (newBaseQueueLast !== null) {const clone: Update<S, A> = {// This update is going to be committed so we never want uncommit// it. Using NoLane works because 0 is a subset of all bitmasks, so// this will never be skipped by the check above.lane: NoLane,revertLane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast = newBaseQueueLast.next = clone;}// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (updateLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}如果更新通道与当前过渡通道相同
那么这个标志就是用来标记状态变化依赖于异步操作
这个标志在这个函数的末尾被消耗
} else {由于存在
revertLane
,所以是乐观更新// This is an optimistic update. If the "revert" priority is// sufficient, don't apply the update. Otherwise, apply the update,// but leave it in the queue so it can be either reverted or// rebased in a subsequent render.if (isSubsetOfLanes(renderLanes, revertLane)) {---------------------------------------如前所述,如果revertLane是当前的渲染通道
这意味着我们需要撤销更新
// The transition that this optimistic update is associated with// has finished. Pretend the update doesn't exist by skipping// over it.update = update.next;// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (revertLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}continue;这里它通过简单地被忽视并且不放入下一个队列来被撤销]
这与跳过不同,因为在跳过中
更新仍然被克隆并放入下一个队列
} else {如果不是乐观更新,那么我们需要提交它!
但我们仍然需要将它放入队列。
因为直到异步操作完成,我们需要能够
设置乐观更新!所以在异步操作期间,乐观更新必须在队列中
const clone: Update<S, A> = {// Once we commit an optimistic update, we shouldn't uncommit it// until the transition it is associated with has finished// (represented by revertLane). Using NoLane here works because 0// is a subset of all bitmasks, so this will never be skipped by// the check above.lane: NoLane,将其设置为NoLane意味着在后续的渲染中不会被跳过
// Reuse the same revertLane so we know when the transition// has finished.revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}为新的渲染链式更新
// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,revertLane,);markSkippedUpdateLanes(revertLane);}}// Process this update.const action = update.action;if (shouldDoubleInvokeUserFnsInHooksDEV) {reducer(newState, action);}if (update.hasEagerState) {// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState = ((update.eagerState: any): S);} else {newState = reducer(newState, action);Update is committed here
}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();// Check if this update is part of a pending async action. If so, we'll// need to suspend until the action has finished, so that it's batched// together with future updates in the same action.// TODO: Once we support hooks inside useMemo (or an equivalent// memoization boundary like Forget), hoist this logic so that it only// suspends if the memo boundary produces a new value.if (didReadFromEntangledAsyncAction) {const entangledActionThenable = peekEntangledActionThenable();if (entangledActionThenable !== null) {// TODO: Instead of the throwing the thenable directly, throw a// special object like `use` does so we can detect if it's captured// by userspace.throw entangledActionThenable;}}}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;最后更新钩子。注意
baseQueue
也被更新了。queue.lastRenderedState = newState;}if (baseQueue === null) {// `queue.lanes` is used for entangling transitions. We can set it back to// zero once the queue is empty.queue.lanes = NoLanes;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];}
function updateReducerImpl<S, A>(这与
useState()
相同,内部与useReducer()
相同hook: Hook,current: Hook,reducer: (S, A) => S,): [S, Dispatch<A>] {const baseState = hook.baseState;if (baseQueue === null) {} else {// We have a queue to process.const first = baseQueue.next;let newState = baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast: Update<S, A> | null = null;let update = first;let didReadFromEntangledAsyncAction = false;do {Updates are processed in this loop
// An extra OffscreenLane bit is added to updates that were made to// a hidden tree, so that we can distinguish them from updates that were// already there when the tree was hidden.const updateLane = removeLanes(update.lane, OffscreenLane);const isHiddenUpdate = updateLane !== update.lane;// Check if this update was made while the tree was hidden. If so, then// it's not a "base" update and we should disregard the extra base lanes// that were added to renderLanes when we entered the Offscreen tree.const shouldSkipUpdate = isHiddenUpdate? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane): !isSubsetOfLanes(renderLanes, updateLane);这一行很重要,它表示如果当前的渲染通道不是
lane of update, then we should skip the update.
if (shouldSkipUpdate) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.const clone: Update<S, A> = {lane: updateLane,revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,updateLane,);markSkippedUpdateLanes(updateLane);跳过并不意味着忽视,更新被克隆并放入队列等待下一次渲染
} else {// This update does have sufficient priority.// Check if this is an optimistic update.const revertLane = update.revertLane;if (!enableAsyncActions || revertLane === NoLane) {如果不是乐观更新
// This is not an optimistic update, and we're going to apply it now.// But, if there were earlier updates that were skipped, we need to// leave this update in the queue so it can be rebased later.if (newBaseQueueLast !== null) {const clone: Update<S, A> = {// This update is going to be committed so we never want uncommit// it. Using NoLane works because 0 is a subset of all bitmasks, so// this will never be skipped by the check above.lane: NoLane,revertLane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast = newBaseQueueLast.next = clone;}// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (updateLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}如果更新通道与当前过渡通道相同
那么这个标志就是用来标记状态变化依赖于异步操作
这个标志在这个函数的末尾被消耗
} else {由于存在
revertLane
,所以是乐观更新// This is an optimistic update. If the "revert" priority is// sufficient, don't apply the update. Otherwise, apply the update,// but leave it in the queue so it can be either reverted or// rebased in a subsequent render.if (isSubsetOfLanes(renderLanes, revertLane)) {---------------------------------------如前所述,如果revertLane是当前的渲染通道
这意味着我们需要撤销更新
// The transition that this optimistic update is associated with// has finished. Pretend the update doesn't exist by skipping// over it.update = update.next;// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (revertLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}continue;这里它通过简单地被忽视并且不放入下一个队列来被撤销]
这与跳过不同,因为在跳过中
更新仍然被克隆并放入下一个队列
} else {如果不是乐观更新,那么我们需要提交它!
但我们仍然需要将它放入队列。
因为直到异步操作完成,我们需要能够
设置乐观更新!所以在异步操作期间,乐观更新必须在队列中
const clone: Update<S, A> = {// Once we commit an optimistic update, we shouldn't uncommit it// until the transition it is associated with has finished// (represented by revertLane). Using NoLane here works because 0// is a subset of all bitmasks, so this will never be skipped by// the check above.lane: NoLane,将其设置为NoLane意味着在后续的渲染中不会被跳过
// Reuse the same revertLane so we know when the transition// has finished.revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}为新的渲染链式更新
// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,revertLane,);markSkippedUpdateLanes(revertLane);}}// Process this update.const action = update.action;if (shouldDoubleInvokeUserFnsInHooksDEV) {reducer(newState, action);}if (update.hasEagerState) {// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState = ((update.eagerState: any): S);} else {newState = reducer(newState, action);Update is committed here
}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();// Check if this update is part of a pending async action. If so, we'll// need to suspend until the action has finished, so that it's batched// together with future updates in the same action.// TODO: Once we support hooks inside useMemo (or an equivalent// memoization boundary like Forget), hoist this logic so that it only// suspends if the memo boundary produces a new value.if (didReadFromEntangledAsyncAction) {const entangledActionThenable = peekEntangledActionThenable();if (entangledActionThenable !== null) {// TODO: Instead of the throwing the thenable directly, throw a// special object like `use` does so we can detect if it's captured// by userspace.throw entangledActionThenable;}}}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;最后更新钩子。注意
baseQueue
也被更新了。queue.lastRenderedState = newState;}if (baseQueue === null) {// `queue.lanes` is used for entangling transitions. We can set it back to// zero once the queue is empty.queue.lanes = NoLanes;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];}
让我们稍微总结一下这里的流程。
- 一个乐观更新有一个过渡通道的
revertLane
和一个SyncLane
的lane
- 更新在
SyncLane
中被处理,并且也在所有后续的渲染中被处理 2.1 但它不会被跳过,并且总是保留在下一个更新队列中 - 更新在
revertLane
(低优先级过渡通道)中被撤销,通过不被添加到下一个队列 3.1 但如果过渡通道上的异步操作还没有完成, 它通过抛出promise来暂停。异步操作完成后,将再次尝试撤销。
我们最后的谜题是什么是异步操作,它如何与过渡一起工作?
4.3 关于过渡的一些背景
请参考React中的useTransition()内部是如何工作的 获取更多细节,简单来说
- 有一个全局的
transition
对象来指示我们是否需要 在低优先级(过渡通道)中调度更新 startTransition()
是为了将这个transition
对象设置为非空,这样 它的范围内的setState()
调用会调度低优先级的更新。
4.4 <form/>
现在接受异步操作
<form>
的所有内容。只有理解useOptimistic()
所必需的部分,将会有另一集专门针对<form/>
根据React.dev,
<form/>
不再是一个简单的HTML标签,它被扩展为能够接受
一个异步操作。在我们的演示代码中,它是这样使用的。
async function formAction(formData) {addOptimisticMessage(formData.get("message"));这立即更新了乐观状态
formRef.current.reset();await sendMessage(formData);触发变更API
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
async function formAction(formData) {addOptimisticMessage(formData.get("message"));这立即更新了乐观状态
formRef.current.reset();await sendMessage(formData);触发变更API
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
这暗示了<form/>
的提交行为必须以某种方式被拦截,
是的,确实如此。
function submitForm() {if (nativeEvent.defaultPrevented) {// We let earlier events to prevent the action from submitting.return;} // Prevent native navigation.event.preventDefault();var formData;if (submitter) {// The submitter's value should be included in the FormData.// It should be in the document order in the form.// Since the FormData constructor invokes the formdata event it also// needs to be available before that happens so after construction it's too// late. We use a temporary fake node for the duration of this event.// TODO: FormData takes a second argument that it's the submitter but this// is fairly new so not all browsers support it yet. Switch to that technique// when available.var temp = submitter.ownerDocument.createElement("input");temp.name = submitter.name;temp.value = submitter.value;submitter.parentNode.insertBefore(temp, submitter);formData = new FormData(form);temp.parentNode.removeChild(temp);} else {formData = new FormData(form);}var pendingState = {pending: true,data: formData,method: form.method,action: action,};{Object.freeze(pendingState);}startHostTransition(formInst, pendingState, action, formData);}
function submitForm() {if (nativeEvent.defaultPrevented) {// We let earlier events to prevent the action from submitting.return;} // Prevent native navigation.event.preventDefault();var formData;if (submitter) {// The submitter's value should be included in the FormData.// It should be in the document order in the form.// Since the FormData constructor invokes the formdata event it also// needs to be available before that happens so after construction it's too// late. We use a temporary fake node for the duration of this event.// TODO: FormData takes a second argument that it's the submitter but this// is fairly new so not all browsers support it yet. Switch to that technique// when available.var temp = submitter.ownerDocument.createElement("input");temp.name = submitter.name;temp.value = submitter.value;submitter.parentNode.insertBefore(temp, submitter);formData = new FormData(form);temp.parentNode.removeChild(temp);} else {formData = new FormData(form);}var pendingState = {pending: true,data: formData,method: form.method,action: action,};{Object.freeze(pendingState);}startHostTransition(formInst, pendingState, action, formData);}
pendingState
是表单组件的未来状态。一旦
过渡完成,<form/>
将在React运行时拥有内部状态。
startHostTransition()
是将状态钩子注入到组件中的方法。
function startHostTransition(formFiber, pendingState, callback, formData) {var queue;if (formFiber.memoizedState === null) {// Upgrade this host component fiber to be stateful. We're going to pretend// it was stateful all along so we can reuse most of the implementation// for function components and useTransition.//// Create the state hook used by TransitionAwareHostComponent. This is// essentially an inlined version of mountState.var newQueue = {pending: null,lanes: NoLanes,// We're going to cheat and intentionally not create a bound dispatch// method, because we can call it directly in startTransition.dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: NotPendingTransition,};queue = newQueue;var stateHook = {memoizedState: NotPendingTransition,baseState: NotPendingTransition,baseQueue: null,queue: newQueue,next: null,}; // Add the state hook to both fiber alternates. The idea is that the fiber// had this hook all along.formFiber.memoizedState = stateHook;var alternate = formFiber.alternate;if (alternate !== null) {alternate.memoizedState = stateHook;}Since <form/> doesn't have state by default, here it creates
a fresh new state hook for it!
Keep in mind that this is the state for the <form/> itself,
containing info like the pending state.
Its initial state is
NotPendingTransition
and will transition tothe pendingState created in
submitForm()
} else {// This fiber was already upgraded to be stateful.var _stateHook = formFiber.memoizedState;queue = _stateHook.queue;}startTransition(formFiber,queue,pendingState,NotPendingTransition, // TODO: We can avoid this extra wrapper, somehow. Figure out layering// once more of this function is implemented.function () {return callback(formData);This callback is the async
formAction()
, in whichwe trigger the
setOptimisticState()
});Internally startTransition is called
}
function startHostTransition(formFiber, pendingState, callback, formData) {var queue;if (formFiber.memoizedState === null) {// Upgrade this host component fiber to be stateful. We're going to pretend// it was stateful all along so we can reuse most of the implementation// for function components and useTransition.//// Create the state hook used by TransitionAwareHostComponent. This is// essentially an inlined version of mountState.var newQueue = {pending: null,lanes: NoLanes,// We're going to cheat and intentionally not create a bound dispatch// method, because we can call it directly in startTransition.dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: NotPendingTransition,};queue = newQueue;var stateHook = {memoizedState: NotPendingTransition,baseState: NotPendingTransition,baseQueue: null,queue: newQueue,next: null,}; // Add the state hook to both fiber alternates. The idea is that the fiber// had this hook all along.formFiber.memoizedState = stateHook;var alternate = formFiber.alternate;if (alternate !== null) {alternate.memoizedState = stateHook;}Since <form/> doesn't have state by default, here it creates
a fresh new state hook for it!
Keep in mind that this is the state for the <form/> itself,
containing info like the pending state.
Its initial state is
NotPendingTransition
and will transition tothe pendingState created in
submitForm()
} else {// This fiber was already upgraded to be stateful.var _stateHook = formFiber.memoizedState;queue = _stateHook.queue;}startTransition(formFiber,queue,pendingState,NotPendingTransition, // TODO: We can avoid this extra wrapper, somehow. Figure out layering// once more of this function is implemented.function () {return callback(formData);This callback is the async
formAction()
, in whichwe trigger the
setOptimisticState()
});Internally startTransition is called
}
现在事情变得更清楚了。
<form/>
有一个异步操作,当它被提交时,为其内部创建了一个状态钩子 并开始了一个过渡。- 在
async
操作期间,乐观状态更新被调度revertLane
是过渡通道 - 直到异步操作完成,每次尝试
撤销更改时,
useOptimistic()
都会暂停。
当异步操作完成时会发生什么?
function startTransition(fiber,queue,pendingState,finishedState,callback,options) {var previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));var prevTransition = ReactCurrentBatchConfig$3.transition;var currentTransition = {_callbacks: new Set(),};{// We don't really need to use an optimistic update here, because we// schedule a second "revert" update below (which we use to suspend the// transition until the async action scope has finished). But we'll use an// optimistic update anyway to make it less likely the behavior accidentally// diverges; for example, both an optimistic update and this one should// share the same lane.ReactCurrentBatchConfig$3.transition = currentTransition;As mentioned, here a new global transition is created
dispatchOptimisticSetState(fiber, false, queue, pendingState);}{ReactCurrentBatchConfig$3.transition._updatedFibers = new Set();}try {if (enableAsyncActions) {this is just a feature flag, it is true in our case
var returnValue = callback();our async
formAction()
is called here, it returns a Promise// Check if we're inside an async action scope. If so, we'll entangle// this new action with the existing scope.//// If we're not already inside an async action scope, and this action is// async, then we'll create a new async scope.//// In the async case, the resulting render will suspend until the async// action scope has finished.if (returnValue !== null &&typeof returnValue === "object" &&typeof returnValue.then === "function") {var thenable = returnValue;notifyTransitionCallbacks(currentTransition, thenable);// Create a thenable that resolves to `finishedState` once the async// action has completed.var thenableForFinishedState = chainThenableValue(thenable,finishedState);dispatchSetState(fiber, queue, thenableForFinishedState);} else {dispatchSetState(fiber, queue, finishedState);}所以
<form>
接收到一个新的状态更新,这次不是乐观的为什么它不是乐观的?我认为是因为表单状态很重要
并且应该立即提交
}} catch (error) {}
function startTransition(fiber,queue,pendingState,finishedState,callback,options) {var previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));var prevTransition = ReactCurrentBatchConfig$3.transition;var currentTransition = {_callbacks: new Set(),};{// We don't really need to use an optimistic update here, because we// schedule a second "revert" update below (which we use to suspend the// transition until the async action scope has finished). But we'll use an// optimistic update anyway to make it less likely the behavior accidentally// diverges; for example, both an optimistic update and this one should// share the same lane.ReactCurrentBatchConfig$3.transition = currentTransition;As mentioned, here a new global transition is created
dispatchOptimisticSetState(fiber, false, queue, pendingState);}{ReactCurrentBatchConfig$3.transition._updatedFibers = new Set();}try {if (enableAsyncActions) {this is just a feature flag, it is true in our case
var returnValue = callback();our async
formAction()
is called here, it returns a Promise// Check if we're inside an async action scope. If so, we'll entangle// this new action with the existing scope.//// If we're not already inside an async action scope, and this action is// async, then we'll create a new async scope.//// In the async case, the resulting render will suspend until the async// action scope has finished.if (returnValue !== null &&typeof returnValue === "object" &&typeof returnValue.then === "function") {var thenable = returnValue;notifyTransitionCallbacks(currentTransition, thenable);// Create a thenable that resolves to `finishedState` once the async// action has completed.var thenableForFinishedState = chainThenableValue(thenable,finishedState);dispatchSetState(fiber, queue, thenableForFinishedState);} else {dispatchSetState(fiber, queue, finishedState);}所以
<form>
接收到一个新的状态更新,这次不是乐观的为什么它不是乐观的?我认为是因为表单状态很重要
并且应该立即提交
}} catch (error) {}
这里有点复杂,有两个更新被派发。
- 对表单状态的一个乐观更新,带有动作的
js
{action: {action : formAction,data: FormData {},method: "get"pending: true},lane: 2,revertLane: 128}
js
{action: {action : formAction,data: FormData {},method: "get"pending: true},lane: 2,revertLane: 128}
这个更新将被提交,因为它是高优先级的,
并且它被保留在通道中,等待revertLane
被处理。
- 另一个更新来解决表单状态。
js
{action: {reason: null,status: 'pending',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}},lane: 128,revertLane: 0}
js
{action: {reason: null,status: 'pending',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}},lane: 128,revertLane: 0}
因为它是正常的状态更新,并且这个更新是在过渡下派发的,
它的更新通道是过渡通道,没有revertLane
。
这两个更新中的所有操作都只是对象,而不是函数,所以 后者实际上在减少时替换了第一个。
当过渡通道被处理时,如果异步操作
仍在进行中,第一个更新将会抛出异常(但它的父
组件<Thread/>
已经抛出异常)。
当异步操作完成时,React重新渲染所有内容。现在第一个更新 不会抛出异常,第二个更新被应用,最终它将有一个如下的状态
js
{reason: null,status: 'fulfilled',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}}
js
{reason: null,status: 'fulfilled',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}}
在React提交更改后,更新队列被清空,我们现在有一个干净的工作区。
所以第一个更新实际上是中间的,就像
useTransition()
中的isPending
选项。
5. 注意:重叠的乐观更新
尝试在官方演示中快速输入多条消息。 你会注意到,即使在异步操作完成后,乐观的 更新仍然被渲染。
实际上,即使我们只输入1条消息,也不那么明显。问题
是当useOptimistic()
接收到一个新的状态参数(透传)时,它立即更新
基础状态,但是现有的乐观更新的撤销将在
未来的重新渲染中。换句话说,乐观更新的撤销在过渡
通道(低优先级)中,所以它可能会被中断,并在未来的任何时间运行。
这是一个已知问题,React团队正在 解决它。在解决之前,我们可以尝试手动过滤掉重复的 通过更新调度器。
const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,...state.filter(msg => msg.text !== newMessage),{text: newMessage,sending: true}]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,...state.filter(msg => msg.text !== newMessage),{text: newMessage,sending: true}]);
现在这里有一个经过调整的官方演示版本,没有重叠的问题。
6. 总结
useOptimistic()
有很多内容,因为它是建立在状态钩子、
过渡和悬挂之上的。它相当复杂,我不认为我们应该
在没有集成它的库的情况下使用它。这是另一个用例
来展示并发模式的力量。
以下是关于useOptimistic()
如何工作的简要总结。希望对你有所帮助!
<form/>
接受一个异步操作,当它被提交时,<form/>
将在内部有一个新的状态钩子,并且一个全局过渡开始 对异步操作,直到它完成。useOptimistic()
创建一个新的状态钩子,它接受一个透传状态, 透传状态被用作更新的基础状态,这就是如何 在没有setState()
的情况下更新状态。- 当一个乐观更新被派发时,它的
revertLane
被设置为 过渡通道。 - 当乐观更新被处理时,它立即被提交,但仍然 保留在更新队列中,以便它可以在后面的低优先级(过渡通道)中被撤销。
- 当乐观更新在过渡通道中被撤销时,如果全局过渡仍在进行中,它会暂停。

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!