在另一个单独的异步调用解决之前,如何防止/锁定函数返回?

Posted

技术标签:

【中文标题】在另一个单独的异步调用解决之前,如何防止/锁定函数返回?【英文标题】:how to prevent/lock a function from returning until another separate async call has resolved? 【发布时间】:2022-01-19 18:07:58 【问题描述】:

我正在处理路由身份验证,并将身份验证状态存储在上下文中,以便其他 React 组件可以检查用户是否已登录。代码的相关部分是:

const [loggedInUser, setLoggedInUser] = useState(null)
const authed = () => !!loggedInUser

useEffect(() => 
  async function fetchData() 
    const response = await fetch('/loggedInUser')
      .then(res => res.json())
    setLoggedInUser(response)
  
  fetchData()
, [])

此代码的快速解释是,我需要部分代码中的用户数据(例如 id),因此我需要存储 loggedInUser 对象。但是,对于更简单的任务,例如检查用户是否已登录,我使用函数 authed 来检查 loggedInUser 变量是否包含对象(用户已登录)或为空。

为了检查用户是否已登录,我使用的是 passport.js,我的 '/loggedInUser' 路由如下所示:

app.get('/loggedInUser', async (req, res) => 
  if (!req.isAuthenticated())
    return null

  const  id, name  = await req.user
    .then(res => res.dataValues)

  return res.json( id, name )
)

问题是,在useEffect()fetch() 能够访问GET 路由并使用对setLoggedInUser(response) 的API 响应之前,使用此上下文并检查authed() 的代码正在运行。所以使用authed() 总是返回false,即使API 响应稍后将loggedInUser 设置为现在authed() 为true 的某个对象值。显然这是一个竞争条件,但我不知道如何解决它。

是否有一个优雅的解决方案,我可以“锁定”authed() 从返回值直到 useEffect() 完全“设置”loggedInUser 的状态?

我设想的一个(糟糕的)解决方案可能是这样的:

const [loggedInUser, setLoggedInUser] = useState(null)
const isFinishedLoading = false // <--
function authed() 
  while (!isFinishedLoading)  // a crude lock
  return !!loggedInUser


useEffect(() => 
  async function fetchData() 
    const response = await fetch('/loggedInUser')
      .then(res => res.json())
    setLoggedInUser(response)
    
    isFinishedLoading = true // <--
  
  fetchData()
, [])

有没有更好的方法来“锁定”函数authed()直到加载完成?


编辑:

为了澄清我对 cme​​ts 的 authed() 用法,这里是我的 App.js 的精简版

export default function App() 
  return (
    <>
      <AuthProvider>
        <Router className="Router">
          <ProtectedRoute path="/" component=SubmittalTable />
          <Login path="/login" />
        </Router>
      </AuthProvider>
    </>
  )


function ProtectedRoute( component: Component, ...rest )
  const  authed  = useContext(AuthContext)

  if (!authed()) // due to race condition, authed() is always false
    return (<Redirect from="" to="login" noThrow />)

  return (<Component ...rest />)

【问题讨论】:

不要混用 await.then。他们正在尝试做类似的事情。 “上下文”是指 React.Context 吗?你传递给提供者什么?您是否尝试将 isLoggedIn 提供给 Provider? 这相当广泛,每次页面刷新时您都会发送一个 http 请求以检查用户是否仍然登录。我建议查看一些有关会话管理的文章。 您将难以实现“锁定”。一旦验证了身份验证并加载了数据,您是否只能通过条件语句呈现数据?或者,如果用户未通过身份验证,则将用户重定向到登录页面,并在确认身份验证后重定向回来?您可以将身份验证存储在集中状态,然后在请求从 api 返回 401 时重定向用户。无论哪种方式,您都不能阻止组件的渲染。 @AxisStarstreamer 不,我将其用作 AuthContext,类似于reactjs.org/docs/context.html#when-to-use-context。我将 value=isLoggedIn, authed 传递给提供者,但这并不能解决我的竞争条件问题。 isLoggedIn 将共享相同的竞争条件问题。 【参考方案1】:

我不明白为什么你需要authed() 成为一个函数,并且不要直接使用isLoggedIn。我也看不到你在哪里 设置 Context 值,但无论如何......

一般建议

通常:在 React 中,尝试思考

“我的应用在任何特定时刻的状态是什么”, 而不是“应该按什么顺序发生什么”

在你的情况下:

“根据状态,现在应该显示哪个页面”, 而不是“一有事情发生就重定向”。

用户授权或授权在任何给定时刻使用您的应用程序。那是一个状态,你可以把这个状态存储在某个地方。我们称这个状态为isAuthorized

存储状态的不同位置

    您可以将isAuthorized 存储在上下文 中,只要您知道它在您需要时可用。如果 Context 在您想知道用户是否被授权时可用(在您的应用中似乎就是这种情况),那么您不能使用用于存储isAuthorized 的上下文(至少不是单独的)。

    您可以在每次需要时获取 isAuthorized。然后 isAuthorized 在 fetch 响应之前不可用。您的应用程序现在的状态是什么?它是notReady(可能)。您可以将状态 notReady 存储在某处,例如再次在上下文中。 (notReady 最初始终是 true,因此只有当您明确表示时,您才知道应用程序已准备就绪。)应用程序可能会显示一个 Spinner,只要它是 notReady,就不会执行任何其他操作。

    您可以将isAuthorized 存储在例如浏览器存储(例如sessionStorage)。它们在页面加载时可用,因此您不必每次都获取状态。浏览器存储应该是同步的,但实际上我会将它们视为异步的,因为我所阅读的有关浏览器存储的内容并没有激发信心。

问题及解决办法

您要做的是将isAuthorized 存储在 (1) 上下文中并且 (2) 每次都获取它,因此您有 2 个需要同步的状态。无论如何,您确实需要在开始时至少获取一次isAuthorized,否则,应用程序将无法使用。因此,您确实需要同步和状态 (3) notReady(或 isReady)。

在 React 中使用 useEffect 完成同步状态(或使用“依赖项”),例如:

useEffect(() => 
  setIsFinishedLoading( false );    // state (3) "app is ready"
  fetchData().then( response => 
    setLoggedInUser( response );    // state (2) "isAuthorized" from fetch
    setIsFinishedLoading( true );
  ).catch( error => 
    setLoggedInUser( null );
    setIsFinishedLoading( true );
  );
, []);

useEffect(() => 
  if( isFinishedLoading )
    setIsAuthorized( !!response );      // state (1) "isAuthorized" in local state or Context
  
  
, [ isFinishedLoading, response ]);

“阻塞”

你可能不需要它,但是:

在这种意义上阻止代码执行在 javascript 中是不可能的。您将改为在其他时间执行代码,例如使用承诺。这再次需要以稍微不同的方式思考。

你不能这样做:

function authed()
  blockExecutionBasedOnCondition
  return !!loggedInUser

但你可以这样做:

function authed()
  return !!loggedInUser;


function executeAuthed()
  someConditionWithPromise.then( result => 
    authed();
  );

【讨论】:

以上是关于在另一个单独的异步调用解决之前,如何防止/锁定函数返回?的主要内容,如果未能解决你的问题,请参考以下文章

如何确保在从 Mongoose 中的函数返回之前执行异步调用?

使用不同的变量异步多次运行某些代码

调用异步的方法,单独得到异步的数据的解决办法

如何使单独的同步功能等待另一个异步功能?

使用UPDATE时如何防止SQL数据库被锁定?

如何防止UI在调用进程时锁定在AIR中?