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

IT技术 javascript reactjs react-native asynchronous async-await
2021-05-08 14:08:30

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

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

 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

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

任何想法以这样的方式做我想做的事情,即我得到的结果是有序的并避免等待写入数据库的时间?

谢谢你。

2个回答

解决方法是立即禁用该按钮。使用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");
  };

@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;
      });
}