[英]Promise Chaining Race-Condition
我目前正在研究一個相當簡單的邏輯來處理排隊的 ZPL 打印作業,這些作業存儲在一個數組中,然后迭代將每個作業的 n 個副本發送到打印機。
我將數組簡化為 promise 鏈,混合在每個作業的子鏈中,將副本發送到打印機。 對打印機的調用是同步的(我知道...),所以我將它們中的每一個都包裝到一個 Promise 中,僅在打印機收到副本時才解析,從而確保順序處理。
在傳輸失敗的情況下,當前的 promise 會拒絕並在主鏈中捕獲手工錯誤。
到目前為止,該理論似乎在子鏈之間存在一種競爭條件。
我盡力了,但我根本看不到它......
這里有一些簡化的代碼+小提琴,注意子鏈是如何不運行的:
['job1', 'job2'].reduce((pMain, item, curIndex) => {
var pSub = Promise.resolve();
for (let i = 0; i < 2; i++) pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
return pMain.then(() => pSub);
}, Promise.resolve())
.then(() => /* print all done */)
.catch( handleError );
任何建議都將受到高度贊賞。 被如此瑣碎的事情困住是一件很糟糕的事情。
您的pSub
鏈都是在reduce
調用期間創建並同步運行的。 要成為順序,他們需要 go 在then
回調中:
['job1', 'job2'].reduce((pMain, item, curIndex) => {
return pMain.then(() => {
var pSub = Promise.resolve();
for (let i = 0; i < 2; i++)
pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
return pSub;
});
}, Promise.resolve())
或者在兩個循環中只構建一個鏈:
['job1', 'job2'].reduce((promise, item, outerIndex) => {
return Array.from({length: 2}).reduce((promise, _, innerIndex) => {
return promise.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
}, promise);
}, Promise.resolve())
當然@jfriend 是對的,對於順序任務,您應該只編寫async
/ await
代碼:
for (const item of ['job1', 'job2']) {
for (let i = 0; i < 2; i++) {
await new Promise((resolve, reject) => setTimeout(reject, 2000));
}
}
您還可以使用該解決方案輕松地將try
塊放在正確的級別。
所以,到現在為止,你已經明白你在使用.reduce()
序列化 Promise 時做錯了什么。 在評論中,我提出了幾個建議:
async/await
(必要時進行轉譯).reduce()
循環。 如果 #1 或 #2 不實用,我建議您制作自己測試過的實用程序函數,因為.reduce()
序列化方法很容易出錯,而且對於那些還沒有看過代碼知道它在做什么的人來說並不總是微不足道的而一個適當命名的實用程序 function 編寫和測試一次更易於使用和理解(一旦編寫 function),顯然它也使重用變得實用。
對於預構建的庫,Bluebird 和 Async 都具有這些功能(我個人更喜歡 Bluebird),並且我自己在運行舊版本 JS 的嵌入式項目(Raspberry Pi)中使用了 Bluebird。
至於經過測試的實用功能,這里有幾個您可以快速使用的功能。
iterateAsync()
就像一個異步的.forEach()
mapAsync()
就像一個異步.map()
reduceAsync()
就像一個異步的 .reduce .reduce()
都將一個數組作為第一個參數,將一個 function 返回一個 promise 作為第二個參數。 這些是 ES5 兼容的,但假設Promise
可用。 以下是三個功能:
// iterate an array sequentially, calling a function (that returns a promise)
// on each element of the array
// The final resolved value is whatever the last call to fn(item) resolves to
// like an asynchronous .forEach()
function iterateAsync(array, fn) {
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item);
});
}, Promise.resolve());
}
// iterate an array sequentially, calling a function (that returns a promise)
// on each element of the array
// The final resolved value is an array of what all the fn(item) calls resolved to
// like an asynchronous .map()
function mapAsync(array, fn) {
var results = [];
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item).then(function(val) {
results.push(val);
return val;
});
});
}, Promise.resolve()).then(function() {
return results;
});
}
// iterate an array sequentially, calling a function fn(item, val)
// (that returns a promise) on each element of the array. Like array.reduce(),
// the next fn(accumulator, item) is passed the previous resolved value and the promise
// that fn() returns should resolve to the value you want passed to the next
// link in the chain
// The final resolved value is whatever the last call to fn(item, val) resolves to
// like an asynchronous .reduce()
function reduceAsync(array, fn, initialVal) {
return array.reduce(function(p, item) {
return p.then(function(accumulator) {
return fn(accumulator, item);
});
}, Promise.resolve(initialVal));
}
請注意,使用現代 Javascript 功能(尤其是async/await
),所有這些通常都更簡單,因此這些主要用於當這些現代功能不可用或轉譯不實用時。
為了完整起見,我將補充一點,以這種方式使用.reduce()
可能不適用於迭代非常大的 arrays。 這是因為它的作用是同步預構建一個 promise 鏈p.then().then().then().then()
,其中.then()
() 的數量等於數組的長度。 如果您的數組非常大(數萬或數十萬個元素長),則可能需要大量的 memory 來預先構建所有這些承諾並將它們鏈接在一起。
對於非常大的 arrays 在您所指的“有限環境”中,您可能希望像這樣手動迭代更多,它不會預先構建任何大型結構,並且一次只使用一個承諾:
function iterateAsync(list, fn) {
return new Promise(function(resolve, reject) {
var index = 0;
function next(val) {
if (index < list.length) {
try {
fn(list[index++]).then(next, reject);
} catch(e) {
reject(e);
}
} else {
resolve(val);
}
}
next();
});
}
我想有很多方法可以實現這一點,但就我個人而言,我總是做的是創建一個返回 Promise 的函數數組(你可能會說是 PromiseFactory)。
const promiseFactoryA = () => {
return new Promise(resolve => {
console.log('PromiseA started...');
setTimeout(() => {
console.log('PromiseA resolved after 300ms');
resolve();
})
}, 300);
}
const promiseFactories = [promiseFactoryA, promiseFactoryA];
然后,我們可以將數組傳遞給這個 function,它將按順序運行它們:
const runPromiseSequentially = promiseFactories => {
let result = Promise.resolve();
promiseFactories.forEach(
(promiseFactory) => {
result = result.then(() => promiseFactory());
},
);
return result;
}
runPromiseSequentially(promiseFactories);
基本上它的作用是在我們希望開始操作時讓 PromiseFactory 創建 Promise。
示例REPL
但是,如果您可以使用async
和await
這將是不必要的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.