【问题标题】:Handle concurrent async updates to local state处理本地状态的并发异步更新
【发布时间】:2020-05-30 22:55:35
【问题描述】:

我有一系列从本地状态 S 读取的异步调用,根据其当前值执行一些计算,并返回本地状态 S' 的新更新值

所有这些都发生在运行时,所以我几乎无法控制这些操作的顺序。这是我所拥有的简化版本。

type State = {
  state: number
}

let localState: State = {
  state: 1000
}

const promiseTimeout = (time: number, value: number) => () => new Promise(
    (resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
  );


const post: (n: number, currentState: State) => Promise<void> = (n, c) => promiseTimeout(n, c.state)()
  .then(res => {
    localState.state = res
    console.log(localState)
  })

post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
// when both promises resolve, the final value of localState will be 4000 instead of 5000

Playground link

这个模型显然被破坏了,因为对post 的两次调用都将读取localState 的相同值,而它们应该按顺序执行。

如果所有调用都在编译时确定,我可以简单地有类似的东西

post(1000, localState)
  .then(() => post(3000, localState)) // localState at call time is now 2000

我将如何解决这个问题?

【问题讨论】:

  • 在运行时将所有承诺放入数组中,然后使用Array.reduce return [ task1, task2, task3, ].reduce((chain, task_i) =&gt; { }, initialPromise) 之类的东西
  • 您想按顺序执行,例如等待 1000 毫秒更新状态,然后等待 3000 毫秒更新状态?或者您希望它们并行运行?
  • @UchitKumar 无法正常工作,因为调用发生在运行时
  • @Max 他们需要按顺序运行
  • @UchitKumar - 我不确定你所说的“这种方法”是什么意思。无论有多少对post 的重叠调用,我们都不需要数组。

标签: javascript typescript asynchronous


【解决方案1】:

这是我个人多次遇到的问题。我的解决方案是创建一个队列类,负责确保所有 Promise 都在互斥中执行。我叫它PromiseQueue:

class PromiseQueue {
    constructor() {
        this._queue = new Array(); // Or an LinkedList for better performance
        this._usingQueue = false;
    }

    /**
     * Adds an element to the queue and runs the queue. It resolves when the promise has been executed and resolved.
     *
     * @param {Promise<any>} promise
     */
    add(promise) {
        const self = this;
        return new Promise((resolve, reject) => {
            const promiseData = {
                promise,
                resolve,
                reject,
            };
            self._queue.push(promiseData);
            self._runQueue();
        });
    }

    async _runQueue() {
        if (!this._usingQueue && this._queue.length > 0) {
            this._usingQueue = true;
            const nextPromiseData = this._queue.shift();
            const { promise, resolve, reject } = nextPromiseData;
            try {
                const result = await promise();
                resolve(result);
            } catch (e) {
                reject(e);
            }
            this._usingQueue = false;
            this._runQueue();
        }
    }
}

然后你会像这样使用它(未测试):

const myPromiseQueue = new PromiseQueue();

// This way you are making sure that the second post
// will be executed when the first one has finished
myPromiseQueue.add(async() => await post(1000, localState));
myPromiseQueue.add(async() => await post(3000, localState));

【讨论】:

    【解决方案2】:

    一种方法是让post 挂钩到一个承诺中,而不是直接在状态对象上工作。该承诺可以存储在状态对象本身中。它从状态对象开始。 post 更新如下:

    const post = (n, state) => {
        return state.promise = state.promise
            .then(state => {
                // ...do stuff here that updates (or replaces) `state`...
                return state;
            }));
    };
    

    这是一个使用 asyncAction 的示例(在 JavaScript 中,但您可以添加回类型注释)(就像您的 promiseTimeout,但没有让它返回我们立即调用的函数;不是

    "use strict";
    
    let localState = {
        state: 1000
    };
    localState.promise = Promise.resolve(localState);
    
    // I'm not sure why this *returns* a function that we
    // have to call, but...
    const promiseTimeout = (time, value) => () => new Promise((resolve) => setTimeout(resolve, time, value + time));
      
    const post = (n, state) => {
        return state.promise = state.promise
            .then(state => promiseTimeout(n, state.state)().then(newValue => {
                state.state = newValue;
                console.log(state.state);
                return state;
            }));
    };
    
    console.log("Running...");
    post(1000, localState); // localState at call time is 1000
    post(3000, localState); // localState at call time is still 1000

    由于对post 的每次调用都会同步地用新的promise 替换promise,因此链是由对post 的调用构建的。

    这是 TypeScript 中的内容(在一个地方有一点小技巧,你可能可以改进它); link to the playground.

    type State = {
      state: number,
      promise: Promise<State>
    };
    
    let localState: State = (() => {
        const s: Partial<State> = {
            state: 1000
        };
        // There's probably a better way to handle this than type assertions, but...
        s.promise = Promise.resolve(s as State);
        return s as State;
    })();
    
    // I'm not sure why this *returns* a function that we
    // have to call, but...
    const promiseTimeout = (time: number, value: number) => () => new Promise(
        (resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
    );
    
    const post = (n: number, state: State): Promise<State> => {
        return state.promise = state.promise
            .then(state => promiseTimeout(n, state.state)().then(newValue => {
                state.state = newValue;
                console.log(state.state);
                return state;
            }));
    };
    
    console.log("Running...");
    post(1000, localState); // localState at call time is 1000
    post(3000, localState); // localState at call time is still 1000
    

    值得注意的是,在这种状态可以像这样异步更改的情况下,在更改它而不是修改现有状态对象时,通常值得生​​成一个 new 状态对象 - 例如,将将方面声明为不可变。

    【讨论】:

    • 谢谢@T.J.克劳德,这很有意义。你能详细说明一下不可变的方法吗?那会有什么不同?
    • @sekiro999 - 基本上,不是state.x = y; return state;,而是return {...state, x: y};,创建一个新对象而不是更新现有对象。这样,任何临时使用状态对象的东西都知道它在拥有它时不会改变(例如,React 的状态是不可变的)。不变性解决了一些问题,创造了其他问题,所以它在很大程度上取决于更广泛的情况。 :-)
    【解决方案3】:

    我没有使用 TypeScript 的经验,因此您必须自己进行转换。

    您可以考虑将queue 方法添加到您的状态,该方法需要回调。如果回调返回一个承诺,它将等待它完成。如果不是,则立即执行队列中的下一项。

    function createQueue() {
      var promise = Promise.resolve();
      return function (fn) {
        promise = promise.then(() => fn(this));
        return promise;
      };
    }
    
    const localState = { state: 1000, queue: createQueue() };
    
    const timeout = (...args) => new Promise(resolve => setTimeout(resolve, ...args));
    const promiseTimeout = (time, value) => timeout(time, value + time);
    
    const post = (time, state) => state.queue(() => {
      return promiseTimeout(time, state.state).then(result => {
        state.state = result;
        console.log(state.state);
      });
    });
    
    post(1000, localState).then(() => console.log("post 1000 complete"));
    post(3000, localState).then(() => console.log("post 3000 complete"));

    【讨论】:

      猜你喜欢
      • 2023-01-12
      • 1970-01-01
      • 2012-06-29
      • 1970-01-01
      • 2016-07-18
      • 2022-01-05
      • 2015-07-18
      • 2020-11-12
      • 2014-03-10
      相关资源
      最近更新 更多