如何在 React (async/await) 中创建一个原子进程?

Posted

技术标签:

【中文标题】如何在 React (async/await) 中创建一个原子进程?【英文标题】:How to make an atomic process in React (async/await)? 【发布时间】:2021-01-31 20:51:12 【问题描述】:

想象一下按下按钮时可以点赞的帖子。此按钮会修改远程数据库,因此将点赞关联到特定帖子需要一点时间。

现在,如果用户开始使用此代码快速按下按钮:

 state = 
    isLiked: false,
 

 handlePress = () => 
    this.setState(
      
        isLiked: !this.state.isLiked,
      ,
      this.handleLike
    );
  ;

  handleLike = async () => 
    const  postId  = this.props;

    try 
      console.log(isLiked ? "Liking" : "Disliking")
      await db.processLike(postId);
     catch (err) 
      // If an error has occurred, reverse the 'isLiked' state
      this.setState(
        isLiked: !this.state.isLiked,
      );

      // TODO - Alert the error to the user in a toast
      console.log(err);
    

    console.log("DONE");
  ;

由于一切都是异步的,所以有可能看到这种情况:

喜欢

不喜欢

完成

完成

我曾考虑创建一个状态“isLiking”以避免在所有异步作业完成之前运行代码。像这样的:

 state = 
    isLiking: false,
    isLiked: false,
 

 handlePress = () => 

    if (this.state.isLiking) return; <------------------------------------

    this.setState(
      
        isLiking: true, <------------------------------------
        isLiked: !this.state.isLiked,
      ,
      this.handleLike
    );
  ;

  handleLike = async () => 
    const  postId  = this.props;

    try 
      console.log(isLiked ? "Liking" : "Disliking"); 
      await db.processLike(postId);
     catch (err) 
      // If an error has occurred, reverse the 'isLiked' state
      this.setState(
        isLiked: !this.state.isLiked,
      );

      // TODO - Alert the error to the user in a toast
      console.log(err);
    

    this.setState( isLiking: false ); <------------------------------------

    console.log("DONE");
  ;

这样一切正常,但如果用户快速按下按钮,他将无法看到 GUI 变化(喜欢按钮颜色(喜欢红色,如果不喜欢,则白色)),直到描述的所有过程在上面的代码中完成。

我也想过像这样制作一个去抖函数(用于handlePress):

export const debounce = (func, wait, immediate) => 
  /*
    Returns a function, that, as long as it continues to be invoked, will not
    be triggered. The function will be called after it stops being called for
    N milliseconds. If `immediate` is passed, trigger the function on the
    leading edge, instead of the trailing.
  */

  let timeout;
  return function () 
    let context = this,
      args = arguments;

    let later = function () 
      timeout = null;
      if (!immediate) func.apply(context, args);
    ;

    let callNow = immediate && !timeout;

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);

    if (callNow) func.apply(context, args);
  ;
;

...

debuncedHandlePress = debounce(this.handlePress, 500); // Now, when the button is pressed, it will call this function, instead of the original handlePress

但是有了这个,我唯一要做的就是减少得到混乱结果的机会。也就是说,我仍然遇到与第一个代码相同的问题。

有什么想法可以按照我想要的方式来做我得到的结果是有序的并避免等待写入数据库的时间吗?

谢谢。

【问题讨论】:

【参考方案1】:

解决方法是立即禁用该按钮。使用setState,您不能期望立即更新isLinking,这就是您生气的原因。 一种解决方案是使用flag variable 而不是state

你可以这样修复。

 state = 
    isLiked: false,
 

 constructor(props) 
    this.isLiking = false; <------------------------------------
 
 

 handlePress = () => 
    this.isLiking = true; <------------------------------------
    this.setState(
      
        isLiked: !this.state.isLiked,
      ,
      this.handleLike
    );
  ;

  handleLike = async () => 
    const  postId  = this.props;

    try 
      console.log(isLiked ? "Liking" : "Disliking"); 
      await db.processLike(postId);
     catch (err) 
      // If an error has occurred, reverse the 'isLiked' state
      this.setState(
        isLiked: !this.state.isLiked,
      );

      // TODO - Alert the error to the user in a toast
      console.log(err);
    

    this.isLiking = false; <------------------------------------

    console.log("DONE");
  ;

【讨论】:

【参考方案2】:

@Prime 的答案有效,但当您的操作分散在整个应用程序中并且很难同步所有内容时,它就不够了。

在我的例子中,它是 API 令牌刷新。由于 API 请求分散在整个应用程序中,因此几乎不可能使用状态变量来阻止调用。

因此我提出另一种解决方案:

/*
    The long running operation
*/

const myLongRunningOperation = async () => 
    // Do an API call, for instance


/*
    Promise locking-queueing structure
*/

var promiesCallbacks = [];

const resolveQueue = value => 
  promiesCallbacks.forEach(x => x.resolve(value));
  promiesCallbacks = [];
;
const rejectQueue = value => 
  promiesCallbacks.forEach(x => x.reject(value));
  promiesCallbacks = [];
;
const enqueuePromise = () => 
  return new Promise((resolve, reject) => 
    promiesCallbacks.push(resolve, reject);
  );
;

/*
    The atomic function!
*/

var actionInProgress = false;

const doAtomicAction = () => 
    if (actionInProgress) 
      return enqueuePromise();
    

    actionInProgress = true;

    return myLongRunningOperation()
      .then(( access ) => 
        resolveQueue(access);
        return access;
      )
      .catch((error) => 
        rejectQueue(error);
        throw error;
      )
      .finally(() => 
        actionInProgress = false;
      );

【讨论】:

以上是关于如何在 React (async/await) 中创建一个原子进程?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Async / Await 和 React 钩子?

如何使用 react-hooks-testing-library 测试自定义 async/await 钩子

[react] 在React中怎么使用async/await?

在 React 中使用 Async/Await 进行 API 响应

如何在 componentWillUnmount 中中止运行 async/await xmlHttpRequest?

在 ES6 React .JS 中使用 Async/Await