How does useOptimistic() work internally in React?

乐观UI是UI开发中常用的一种技巧,用于提高感知的性能。 useOptimistic()是一个帮助构建它的钩子。让我们来看看它内部是如何工作的。

1. 什么是乐观UI?

假设我们要实现一个用户可以点击来点赞或取消点赞的”点赞”按钮,你会怎么做?

基本的思路很简单,当按钮被点击时我们调用API,并在API响应后更新UI。这种方法可行,但可能会非常卡顿,试试下面的演示吧。

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <h2>non-optimistic UI</h2>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

这是点击处理器。

const toggle = () => {
if (isFetching) return
setIsFetching(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) return
setIsFetching(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获取能够成功。

所以在点击处理器中我们可以:

  1. 立即更新UI
  2. 触发API调用
  3. 在API响应(成功或错误)后,再次更新UI。

下面是一个启用了乐观UI的演示,它在感知性能上有了巨大的提升。

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <h2>optimistic UI</h2>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

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 call
const callIdRef = useRef(null)
const toggle = () => {
setOptimisticLiked(!optimisticLiked)
const callId = uid()
callIdRef.current = callId
mutateAPI({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 call
const callIdRef = useRef(null)
const toggle = () => {
setOptimisticLiked(!optimisticLiked)
const callId = uid()
callIdRef.current = callId
mutateAPI({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)
})
}

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选项支持它。

<button
type="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>
<button
type="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(() => {
{...}
fetcher().then((data) => {
setState(state => ({
...state,
data
}))
setOptimisticState(state => ({
...optimisticState,
data
}))
}).catch((error) => {
setState(state => ({
...state,
error
}))
setOptimisticState(state => ({
...optimisticState,
error
}))
})
}, [])
const mutate = (updater, options = {}) => {
const mutateId = uid()
mutateIdRef.current = mutateId
if (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 = 0
return () => 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(() => {
{...}
fetcher().then((data) => {
setState(state => ({
...state,
data
}))
setOptimisticState(state => ({
...optimisticState,
data
}))
}).catch((error) => {
setState(state => ({
...state,
error
}))
setOptimisticState(state => ({
...optimisticState,
error
}))
})
}, [])
const mutate = (updater, options = {}) => {
const mutateId = uid()
mutateIdRef.current = mutateId
if (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 = 0
return () => i++
})()

Here is the demo with our new useAPI().

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

演示看起来不错,按钮组件变得更加简洁。

const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))
const liked = !!data?.liked
const 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?.liked
const toggle = () => {
const payload = {liked: !liked}
mutate(
() => mutateAPI(payload, forceResult),
{
optimisticData: payload,
rollbackOnError: true,
onError: toast
}
)
}

好的,对于乐观UI的背景知识已经足够了。现在让我们深入到React的世界。🤿

3. 尝试自己实现useOptimistic()

这是useOptimistic()的官方演示

import { useState, useRef, useOptimistic } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

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()被我们的实现替换。

import { useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
import { useOptimistic } from "./useOptimistic.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

如果你尝试上面的演示,你会发现它实际上并不工作如果你尝试上面的演示,你会发现它实际上并不工作 - 乐观更新根本没有被渲染!问题在于<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>
import { useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
import { useOptimistic } from "./useOptimistic.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(e) {
    e.preventDefault()
    const formData = new FormData(formRef.current)
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true,
      },
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form onSubmit={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

我说它”有点能工作”是因为它实际上并不完全像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)) {
{...}
// When calling startTransition during render, this warns instead of
// throwing because throwing would be a breaking change. setOptimisticState
// is a new API so it's OK to throw.
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
} else {
// startTransition was called during render. We don't need to do anything
// besides warn here because the render phase update would be overidden by
// the second update, anyway. We can remove this branch and make it throw
// in a future release.
if (__DEV__) {
console.error('Cannot call startTransition while rendering.');
}
}
} 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)) {
{...}
// When calling startTransition during render, this warns instead of
// throwing because throwing would be a breaking change. setOptimisticState
// is a new API so it's OK to throw.
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
} else {
// startTransition was called during render. We don't need to do anything
// besides warn here because the render phase update would be overidden by
// the second update, anyway. We can remove this branch and make it throw
// in a future release.
if (__DEV__) {
console.error('Cannot call startTransition while rendering.');
}
}
} 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()非常相似

}

状态钩子保存了一个不同优先级的更新列表, 它们在不同的批次中被处理,所以需要以下属性。

  1. baseQueue:所有的更新
  2. baseState:更新的基础状态,因为更新像中间件一样按顺序处理。
  3. memoizedState:在渲染中要使用的实际值。如果有待处理的更新,这个值是中间值。

这解释了为什么我们需要在updateOptimisticImpl()中更新baseState。 因为队列中的更新应该是乐观的,意味着它们应该是中间状态,如果我们在这些更新后面放一个 关键更新,它会导致错误的状态。

function updateReducerImpl<S, A>(

这与useState()相同,内部与useReducer()相同

hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
{...}
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
queue.lastRenderedReducer = reducer;
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const baseState = hook.baseState;
if (baseQueue === null) {
{...}
// If there are no pending updates, then the memoized state should be the
// same as the base state. Currently these only diverge in the case of
// useOptimistic, because useOptimistic accepts a new baseState on
// every render.
hook.memoizedState = baseState;
// We don't need to call markWorkInProgressReceivedUpdate because
// baseState is derived from other reactive values.
} 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 state
newState = ((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;
}
}

这部分最重要

如果状态发生了变化,并且它依赖于正在进行的异步操作

那么它会通过抛出promise来暂停。

关于Suspense的更多细节,请参考我关于Suspense的文章

}
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 queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
queue.lastRenderedReducer = reducer;
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const baseState = hook.baseState;
if (baseQueue === null) {
{...}
// If there are no pending updates, then the memoized state should be the
// same as the base state. Currently these only diverge in the case of
// useOptimistic, because useOptimistic accepts a new baseState on
// every render.
hook.memoizedState = baseState;
// We don't need to call markWorkInProgressReceivedUpdate because
// baseState is derived from other reactive values.
} 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 state
newState = ((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;
}
}

这部分最重要

如果状态发生了变化,并且它依赖于正在进行的异步操作

那么它会通过抛出promise来暂停。

关于Suspense的更多细节,请参考我关于Suspense的文章

}
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];
}

让我们稍微总结一下这里的流程。

  1. 一个乐观更新有一个过渡通道的revertLane和一个SyncLanelane
  2. 更新在SyncLane中被处理,并且也在所有后续的渲染中被处理 2.1 但它不会被跳过,并且总是保留在下一个更新队列中
  3. 更新在revertLane(低优先级过渡通道)中被撤销,通过不被添加到下一个队列 3.1 但如果过渡通道上的异步操作还没有完成, 它通过抛出promise来暂停。异步操作完成后,将再次尝试撤销。

我们最后的谜题是什么是异步操作,它如何与过渡一起工作?

4.3 关于过渡的一些背景

请参考React中的useTransition()内部是如何工作的 获取更多细节,简单来说

  1. 有一个全局的transition对象来指示我们是否需要 在低优先级(过渡通道)中调度更新
  2. startTransition()是为了将这个transition对象设置为非空,这样 它的范围内的setState()调用会调度低优先级的更新。

4.4 <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) {
{...}
if (formFiber.tag !== HostComponent) {
throw new Error(
"Expected the form instance to be a HostComponent. This " +
"is a bug in React."
);
}
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 to

the 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 which

we trigger the setOptimisticState()

}
);

Internally startTransition is called

}
function startHostTransition(formFiber, pendingState, callback, formData) {
{...}
if (formFiber.tag !== HostComponent) {
throw new Error(
"Expected the form instance to be a HostComponent. This " +
"is a bug in React."
);
}
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 to

the 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 which

we trigger the setOptimisticState()

}
);

Internally startTransition is called

}

现在事情变得更清楚了。

  1. <form/>有一个异步操作,当它被提交时,为其内部创建了一个状态钩子 并开始了一个过渡。
  2. async操作期间,乐观状态更新被调度 revertLane是过渡通道
  3. 直到异步操作完成,每次尝试 撤销更改时,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);

这是表单的状态,而不是我们设置的乐观状态。

表单的状态在useFormStatus()中暴露出来,

我们将在另一集中介绍它。

}
{
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) {
{...}
{
// This is a trick to get the `useTransition` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
var rejectedThenable = {
then: function () {},
status: "rejected",
reason: error,
};
dispatchSetState(fiber, queue, rejectedThenable);
}
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition;
{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;
currentTransition._updatedFibers.clear();
if (updatedFibersCount > 10) {
warn(
"Detected a large number of updates inside startTransition. " +
"If this is due to a subscription please re-write it to use React provided hooks. " +
"Otherwise concurrent mode guarantees are off the table."
);
}
}
}
}
}
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);

这是表单的状态,而不是我们设置的乐观状态。

表单的状态在useFormStatus()中暴露出来,

我们将在另一集中介绍它。

}
{
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) {
{...}
{
// This is a trick to get the `useTransition` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
var rejectedThenable = {
then: function () {},
status: "rejected",
reason: error,
};
dispatchSetState(fiber, queue, rejectedThenable);
}
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition;
{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;
currentTransition._updatedFibers.clear();
if (updatedFibersCount > 10) {
warn(
"Detected a large number of updates inside startTransition. " +
"If this is due to a subscription please re-write it to use React provided hooks. " +
"Otherwise concurrent mode guarantees are off the table."
);
}
}
}
}
}

这里有点复杂,有两个更新被派发。

  1. 对表单状态的一个乐观更新,带有动作的
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被处理。

  1. 另一个更新来解决表单状态。
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
}
]
);

现在这里有一个经过调整的官方演示版本,没有重叠的问题。

import { useState, useRef, useOptimistic } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state.filter(msg => msg.text !== newMessage),
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

6. 总结

useOptimistic()有很多内容,因为它是建立在状态钩子、 过渡和悬挂之上的。它相当复杂,我不认为我们应该 在没有集成它的库的情况下使用它。这是另一个用例 来展示并发模式的力量。

以下是关于useOptimistic()如何工作的简要总结。希望对你有所帮助!

  1. <form/>接受一个异步操作,当它被提交时, <form/>将在内部有一个新的状态钩子,并且一个全局过渡开始 对异步操作,直到它完成。
  2. useOptimistic()创建一个新的状态钩子,它接受一个透传状态, 透传状态被用作更新的基础状态,这就是如何 在没有setState()的情况下更新状态。
  3. 当一个乐观更新被派发时,它的revertLane被设置为 过渡通道。
  4. 当乐观更新被处理时,它立即被提交,但仍然 保留在更新队列中,以便它可以在后面的低优先级(过渡通道)中被撤销。
  5. 当乐观更新在过渡通道中被撤销时,如果全局过渡仍在进行中,它会暂停。

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

😳 Share my post ?    
or sponsor me

Next: Deeper Dive Chrome插件 - 增强React.dev