Giải quyết hết lần này đến lần khác (tức là theo trình tự)?


269

Hãy xem xét đoạn mã sau để đọc một mảng các tệp theo cách nối tiếp / tuần tự. readFilestrả về một lời hứa, chỉ được giải quyết khi tất cả các tệp đã được đọc theo trình tự.

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => 

    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

   readSequential(0); // Start!

  });
};

Đoạn mã trên hoạt động, nhưng tôi không muốn phải đệ quy cho những thứ xảy ra tuần tự. Có cách nào đơn giản hơn để mã này có thể được viết lại để tôi không phải sử dụng readSequentialchức năng kỳ lạ của mình không?

Ban đầu tôi đã cố gắng sử dụng Promise.all, nhưng điều đó khiến tất cả các readFilecuộc gọi xảy ra đồng thời, đó không phải là điều tôi muốn:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};

2
Bất cứ điều gì phải chờ cho một hoạt động không đồng bộ trước đó kết thúc đều phải được thực hiện trong một cuộc gọi lại. Sử dụng lời hứa không thay đổi điều đó. Vì vậy, bạn cần đệ quy.
Barmar

1
FYI, đây không phải là đệ quy về mặt kỹ thuật vì không có xây dựng khung stack. Cái trước readFileSequential()đã được trả về trước khi cái tiếp theo được gọi (vì nó không đồng bộ, nó hoàn thành rất lâu sau khi lệnh gọi hàm ban đầu đã được trả về).
jfriend00

1
@ jfriend00 Tích lũy khung stack không cần thiết cho đệ quy - chỉ tự tham khảo. Đây chỉ là một kỹ thuật mặc dù.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - quan điểm của tôi là hoàn toàn không có gì sai khi có chức năng tự gọi để khởi động lần lặp tiếp theo. Không có nhược điểm nào với nó và trên thực tế, đó là một cách hiệu quả để sắp xếp các hoạt động không đồng bộ. Vì vậy, không có lý do gì để tránh điều gì đó giống như đệ quy. Có những giải pháp đệ quy cho một số vấn đề không hiệu quả - đây không phải là một trong những vấn đề đó.
jfriend00

1
Xin chào, theo một cuộc thảo luận và yêu cầu trong phòng JavaScript, tôi đã chỉnh sửa câu trả lời này để chúng tôi có thể chỉ cho người khác xem nó là một quy tắc. Nếu bạn không đồng ý xin vui lòng cho tôi biết và tôi sẽ khôi phục nó và mở một cái riêng.
Benjamin Gruenbaum

Câu trả lời:


337

Cập nhật 2017 : Tôi sẽ sử dụng chức năng async nếu môi trường hỗ trợ nó:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

Nếu bạn muốn, bạn có thể trì hoãn việc đọc các tệp cho đến khi bạn cần chúng bằng cách sử dụng trình tạo async (nếu môi trường của bạn hỗ trợ nó):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

Cập nhật: Trong suy nghĩ thứ hai - tôi có thể sử dụng vòng lặp for thay thế:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

Hoặc gọn hơn, với giảm:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

Trong các thư viện hứa hẹn khác (như khi và Bluebird), bạn có các phương thức tiện ích cho việc này.

Ví dụ: Bluebird sẽ là:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

Mặc dù thực sự không có lý do gì để không sử dụng async đang chờ đợi ngày hôm nay.


2
@ EmreTapcı, không. Hàm mũi tên "=>" đã ngụ ý trả về.
Tối đa

Nếu bạn sử dụng TypeScript, tôi nghĩ giải pháp vòng lặp "for in" là tốt nhất. Giảm lợi nhuận đệ quy Lời hứa, vd. Kiểu trả về cuộc gọi đầu tiên là Promise <void>, sau đó thứ hai là Promise <Promise <void >>, v.v. - không thể gõ mà không sử dụng bất kỳ điều gì tôi nghĩ
Artur Tagisow

@ArturTagisow TypeScript (ít nhất là các phiên bản mới) có các kiểu đệ quy và sẽ giải quyết các loại chính xác ở đây. Không có thứ gọi là Lời hứa <Lời hứa <T >> vì lời hứa "đồng hóa đệ quy". Promise.resolve(Promise.resolve(15))là giống hệt với Promise.resolve(15).
Benjamin Gruenbaum


72

Đây là cách tôi thích chạy các nhiệm vụ theo chuỗi.

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

Còn những trường hợp có nhiều nhiệm vụ thì sao? Giống như, 10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
Và những gì về trường hợp bạn không biết chính xác số lượng nhiệm vụ?
chết tiệt

1
Và những gì về khi bạn biết số lượng các nhiệm vụ, nhưng chỉ trong thời gian chạy?
joeytwiddle

10
"bạn hoàn toàn không muốn thực hiện một loạt các lời hứa. Theo thông số hứa, ngay khi lời hứa được tạo, nó bắt đầu thực thi. Vì vậy, điều bạn thực sự muốn là một loạt các nhà máy hứa hẹn" xem Lỗi nâng cao # 3 tại đây : bagdb.com/2015/05/18/we-have-a-probols-with-promises.html
edelans

5
Nếu bạn đang giảm nhiễu đường truyền, bạn cũng có thể viếtresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster có, nhưng hãy cẩn thận, vì nếu task () trả về một giá trị, nó sẽ được chuyển sang lệnh gọi tiếp theo. Nếu tác vụ của bạn có các đối số tùy chọn, điều này có thể gây ra tác dụng phụ. Mã hiện tại nuốt kết quả và gọi một cách rõ ràng nhiệm vụ tiếp theo mà không có đối số.
JHH

63

Câu hỏi này đã cũ, nhưng chúng ta sống trong một thế giới của ES6 và JavaScript chức năng, vì vậy hãy xem chúng ta có thể cải thiện như thế nào.

Bởi vì các lời hứa được thực thi ngay lập tức, chúng ta không thể tạo ra một loạt các lời hứa, tất cả chúng sẽ bắn ra song song.

Thay vào đó, chúng ta cần tạo ra một loạt các hàm trả về một lời hứa. Mỗi chức năng sau đó sẽ được thực hiện tuần tự, sau đó bắt đầu lời hứa bên trong.

Chúng ta có thể giải quyết điều này một vài cách, nhưng cách yêu thích của tôi là sử dụng reduce.

Nó có một chút khó khăn khi sử dụng reducekết hợp với những lời hứa, vì vậy tôi đã chia nhỏ một lớp lót thành một số vết cắn nhỏ dễ tiêu hóa dưới đây.

Bản chất của chức năng này là sử dụng reducebắt đầu bằng một giá trị ban đầu Promise.resolve([])hoặc một lời hứa chứa một mảng trống.

Lời hứa này sau đó sẽ được truyền vào reducephương thức như promise. Đây là chìa khóa để xâu chuỗi từng lời hứa với nhau một cách tuần tự. Lời hứa tiếp theo được thực hiện là funcvà khi thenxảy ra hỏa hoạn, kết quả được nối lại và lời hứa đó được trả lại, thực hiện reducechu trình với chức năng hứa tiếp theo.

Khi tất cả các lời hứa đã được thực hiện, lời hứa được trả lại sẽ chứa một mảng tất cả các kết quả của mỗi lời hứa.

Ví dụ ES6 (một lớp lót)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

Ví dụ ES6 (chia nhỏ)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

Sử dụng:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))

1
rất tốt, cảm ơn, Array.prototype.concat.bind(result)là phần tôi đã bị mất, phải thực hiện để đạt được kết quả bằng tay nhưng hoạt động kém hơn
zavr

Vì tất cả chúng ta đều nói về JS hiện đại, tôi tin rằng console.log.bind(console)tuyên bố trong ví dụ cuối cùng của bạn bây giờ thường không cần thiết. Những ngày này bạn chỉ có thể vượt qua console.log. Ví dụ. serial(funcs).then(console.log). Đã thử nghiệm trên nodejs và Chrome hiện tại.
Molomby

Điều này hơi khó khăn để quấn đầu tôi nhưng việc giảm về cơ bản là làm điều này đúng? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
danecando

@danecando, vâng điều này có vẻ đúng. Bạn cũng có thể thả Promise.resolve trong phần trả về, mọi giá trị được trả về sẽ tự động được giải quyết trừ khi bạn gọi Promise.reject trên chúng.
joelnet

@joelnet, để đáp lại bình luận của danecando, tôi nghĩ những gì giảm phải làm đúng hơn trong biểu thức sau đây, bạn có đồng ý không? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))và v.v.
đệmover76

37

Để làm điều này đơn giản trong ES6:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}

1
Có vẻ như nó sử dụng gạch dưới. Bạn có thể đơn giản hóa files.forEachnếu các tập tin là một mảng.
Gustavo

2
Chà ... đó là ES5. Cách ES6 sẽ là for (file of files) {...}.
Gustavo

1
Bạn nói rằng bạn không nên sử dụng Promise.resolve()để tạo ra một lời hứa đã được giải quyết trong cuộc sống thực. Tại sao không? Promise.resolve()Có vẻ sạch sẽ hơn new Promise(success => success()).
canac

8
@canac Xin lỗi, đó chỉ là một trò đùa với một từ chơi chữ ("những lời hứa suông .."). Chắc chắn sử dụng Promise.resolve();trong mã của bạn.
Shridhar Gupta

1
Giải pháp đẹp, dễ làm theo. Tôi đã không bao gồm của tôi trong một chức năng, vì vậy để giải quyết vào cuối thay vì đặt return sequence;tôisequence.then(() => { do stuff });
Joe Coyle

25

Sử dụng đơn giản cho lời hứa Node.js tiêu chuẩn:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

CẬP NHẬT

item-hứa là một gói NPM sẵn sàng để làm tương tự.


6
Tôi rất thích thấy điều này được giải thích chi tiết hơn.
Tyguy7

Tôi đã cung cấp một biến thể của câu trả lời này với lời giải thích dưới đây. Cảm ơn
Sarsaparilla

Đây chính xác là những gì tôi làm trong môi trường tiền Node 7 không có quyền truy cập vào async / await. Đẹp và sạch sẽ.
JHH

11

Tôi đã phải chạy rất nhiều nhiệm vụ tuần tự và sử dụng những câu trả lời này để tạo ra một chức năng sẽ đảm nhiệm việc xử lý bất kỳ nhiệm vụ tuần tự nào ...

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

Hàm có 2 đối số + 1 tùy chọn. Đối số đầu tiên là mảng mà chúng ta sẽ làm việc. Đối số thứ hai là chính nhiệm vụ, một chức năng trả về một lời hứa, nhiệm vụ tiếp theo sẽ chỉ được bắt đầu khi lời hứa này được giải quyết. Đối số thứ ba là một cuộc gọi lại để chạy khi tất cả các nhiệm vụ đã được thực hiện. Nếu không có cuộc gọi lại nào được thông qua, thì hàm sẽ trả về lời hứa mà nó đã tạo để chúng ta có thể xử lý kết thúc.

Đây là một ví dụ về việc sử dụng:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );

Hy vọng nó sẽ giúp ai đó tiết kiệm thời gian ...


Giải pháp đáng kinh ngạc, đó là giải pháp tốt nhất tôi tìm thấy trong gần một tuần bị mắc kẹt .... Nó được giải thích rất rõ, có tên bên trong hợp lý, một ví dụ tốt (có thể tốt hơn), tôi có thể gọi nó một cách an toàn lần khi cần và nó bao gồm tùy chọn để đặt lại cuộc gọi. đơn giản là NICE! (Chỉ cần thay đổi tên thành một cái gì đó khiến tôi có ý nghĩa hơn) .... KIẾN NGHỊ cho người khác ... bạn có thể lặp lại một đối tượng bằng cách sử dụng 'Object.keys ( myObject )' là 'object_array' của bạn
DavidTaubmann

Cám ơn bạn đã góp ý! Tôi cũng không sử dụng tên đó, nhưng tôi muốn làm cho nó rõ ràng hơn / đơn giản hơn ở đây.
Salketer

5

Giải pháp tốt nhất mà tôi có thể tìm ra là với bluebirdnhững lời hứa. Bạn chỉ có thể làm những gì Promise.resolve(files).each(fs.readFileAsync);đảm bảo rằng những lời hứa được giải quyết tuần tự theo thứ tự.


1
Thậm chí tốt hơn : Promise.each(filtes, fs.readFileAsync). Btw, bạn không phải làm gì .bind(fs)?
Bergi

Không ai ở đây dường như hiểu được sự khác biệt giữa một mảng và một chuỗi, rằng cái sau ngụ ý kích thước không giới hạn / động.
Vitaly-t

Lưu ý rằng Mảng trong Javascript không liên quan gì đến mảng kích thước cố định trong ngôn ngữ kiểu C. Chúng chỉ là các đối tượng có quản lý khóa số được bắt vít và không có kích thước hoặc giới hạn theo quy định ( đặc biệt là khi sử dụng new Array(int). Tất cả những gì được đặt trước lengthcặp giá trị khóa, ảnh hưởng đến số lượng chỉ mục được sử dụng trong quá trình lặp theo chiều dài. ảnh hưởng đến việc lập chỉ mục hoặc giới hạn chỉ số của mảng thực tế)
Mike 'Pomax' Kamermans

4

Đây là một biến thể nhỏ của một câu trả lời khác ở trên. Sử dụng Lời hứa bản địa:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

Giải trình

Nếu bạn có những nhiệm vụ này [t1, t2, t3], thì ở trên là tương đương với Promise.resolve().then(t1).then(t2).then(t3). Đó là hành vi giảm.

Cách sử dụng

Trước tiên, bạn cần xây dựng một danh sách các nhiệm vụ! Một nhiệm vụ là một chức năng chấp nhận không có đối số. Nếu bạn cần truyền đối số cho hàm của mình, thì hãy sử dụng bindhoặc các phương thức khác để tạo một tác vụ. Ví dụ:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)

4

Giải pháp ưa thích của tôi:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Nó không khác biệt cơ bản với những người khác được xuất bản ở đây nhưng:

  • Áp dụng hàm cho các mục trong chuỗi
  • Giải quyết một loạt các kết quả
  • Không yêu cầu async / await (hỗ trợ vẫn còn khá hạn chế, vào khoảng năm 2017)
  • Sử dụng các chức năng mũi tên; tốt đẹp và súc tích

Ví dụ sử dụng:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

Đã thử nghiệm trên Chrome hiện tại hợp lý (v59) và NodeJS (v8.1.2).


3

Sử dụng Array.prototype.reducevà nhớ bọc lời hứa của bạn trong một chức năng nếu không chúng sẽ chạy!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

tốt đẹp và dễ dàng ... bạn sẽ có thể sử dụng lại cùng một hạt giống cho hiệu suất, v.v.

Điều quan trọng là phải bảo vệ chống lại các mảng hoặc mảng trống chỉ có 1 phần tử khi sử dụng giảm , vì vậy kỹ thuật này là lựa chọn tốt nhất của bạn:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

và sau đó gọi nó như:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

Tôi đã tạo phương thức đơn giản này trên đối tượng Promise:

Tạo và thêm phương thức Promise. Resultence vào đối tượng Promise

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

Sử dụng:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

Điều tốt nhất về phần mở rộng này cho đối tượng Promise, là nó phù hợp với phong cách của những lời hứa. Promise.all và Promise. Hậu quả được gọi theo cùng một cách, nhưng có ngữ nghĩa khác nhau.

Thận trọng

Chạy liên tục các lời hứa thường không phải là một cách rất tốt để sử dụng lời hứa. Sử dụng Promise.all thường tốt hơn và để trình duyệt chạy mã nhanh nhất có thể. Tuy nhiên, có những trường hợp sử dụng thực sự cho nó - ví dụ như khi viết một ứng dụng di động bằng javascript.


Không, bạn không thể so sánh Promise.allvà của bạn Promise.sequence. Một cái có thể lặp đi lặp lại lời hứa, cái còn lại có một loạt các hàm trả về lời hứa.
Bergi

Btw, tôi khuyên bạn nên tránh phản ứng xây dựng lời hứa
Bergi

Không biết rằng nó đã mất một vòng lặp. Nên dễ dàng để viết lại nó mặc dù. Bạn có thể giải thích lý do tại sao đây là antipotype xây dựng lời hứa? Tôi đã đọc bài viết của bạn ở đây: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Tôi đã cập nhật mã để hỗ trợ các trình vòng lặp. Tôi vẫn không thấy rằng đây là một antipotype. Các antipotype thường được coi là hướng dẫn để tránh các lỗi mã hóa và việc tạo các hàm (thư viện) phá vỡ các hướng dẫn đó là hoàn toàn hợp lệ.
frodeborli

Vâng, nếu bạn coi đó là một chức năng thư viện thì không sao, nhưng trong trường hợp này reduce, câu trả lời giống như trong câu trả lời của Benjamin chỉ đơn giản hơn nhiều.
Bergi

2

Bạn có thể sử dụng chức năng này nhận được Danh sách PromFactories:

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Promise Factory chỉ là một chức năng đơn giản trả về một Promise:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Nó hoạt động vì một nhà máy hứa không tạo ra lời hứa cho đến khi được yêu cầu. Nó hoạt động tương tự như chức năng sau đó - trên thực tế, đó là điều tương tự!

Bạn không muốn thực hiện một loạt các lời hứa. Theo thông số Promise, ngay khi một lời hứa được tạo, nó bắt đầu thực thi. Vì vậy, những gì bạn thực sự muốn là một loạt các nhà máy hứa hẹn ...

Nếu bạn muốn tìm hiểu thêm về Lời hứa, bạn nên kiểm tra liên kết này: https://pouchdb.com/2015/05/18/we-have-a-probols-with-promises.html


2

Câu trả lời của tôi dựa trên https://stackoverflow.com/a/31070150/7542429 .

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

Giải pháp này trả về kết quả dưới dạng một mảng như Promise.all ().

Sử dụng:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

Tôi thực sự thích câu trả lời của @ joelnet, nhưng với tôi, phong cách mã hóa đó hơi khó tiêu hóa, vì vậy tôi đã dành một vài ngày để cố gắng tìm ra cách tôi thể hiện cùng một giải pháp theo cách dễ đọc hơn và đây là mất, chỉ với một cú pháp khác nhau và một số ý kiến.

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

Như Bergi nhận thấy, tôi nghĩ giải pháp tốt nhất và rõ ràng là sử dụng BlueBird.each, mã dưới đây:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

Đầu tiên, bạn cần hiểu rằng một lời hứa được thực hiện tại thời điểm tạo.
Vì vậy, ví dụ nếu bạn có một mã:

["a","b","c"].map(x => returnsPromise(x))

Bạn cần thay đổi nó thành:

["a","b","c"].map(x => () => returnsPromise(x))

Sau đó, chúng ta cần phải tuần tự chuỗi lời hứa:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )

thực thi after(), sẽ đảm bảo rằng lời hứa được tạo (và được thực hiện) chỉ khi thời gian đến.


1

Tôi sử dụng đoạn mã sau để mở rộng đối tượng Promise. Nó xử lý từ chối các lời hứa và trả về một loạt các kết quả

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

Thí dụ

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

Nếu bạn muốn, bạn có thể sử dụng giảm để thực hiện một lời hứa tuần tự, ví dụ:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

nó sẽ luôn hoạt động liên tục.


1

Sử dụng ES hiện đại:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

Với Async / Await (nếu bạn có sự hỗ trợ của ES7)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}

(bạn phải sử dụng forvòng lặp và không phải forEachvì async / await có vấn đề khi chạy trong vòng lặp forEach)

Không có Async / Await (sử dụng Promise)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}

2
Chờ đợi bên trong forEach không được khuyến khích.
Marcelo Agimóvel

@ MarceloAgimóvel - Tôi đã cập nhật giải pháp để không hoạt động forEach(theo điều này )
Gil Epshtain

0

Trên cơ sở các tiêu đề của câu hỏi, "lời hứa Giải quyết cái khác (ví dụ: theo thứ tự)?", Chúng ta có thể hiểu rằng OP là quan tâm nhiều hơn trong việc xử lý tuần tự những lời hứa về giải quyết hơn các cuộc gọi liên tục cho mỗi gia nhập .

Câu trả lời này được cung cấp:

  • để chứng minh rằng các cuộc gọi tuần tự là không cần thiết để xử lý tuần tự các phản hồi.
  • để hiển thị các mẫu thay thế khả thi cho khách truy cập của trang này - bao gồm cả OP nếu anh ta vẫn quan tâm hơn một năm sau đó.
  • mặc dù OP khẳng định rằng anh ta không muốn thực hiện các cuộc gọi đồng thời, điều này thực sự có thể là trường hợp nhưng cũng có thể là một giả định dựa trên mong muốn xử lý tuần tự các câu trả lời như tiêu đề.

Nếu các cuộc gọi đồng thời thực sự không muốn thì hãy xem câu trả lời của Benjamin Gruenbaum bao gồm các cuộc gọi liên tiếp (vv) một cách toàn diện.

Tuy nhiên, nếu bạn quan tâm (để cải thiện hiệu suất) trong các mẫu cho phép các cuộc gọi đồng thời theo sau là xử lý tuần tự các phản hồi, thì vui lòng đọc tiếp.

Thật hấp dẫn khi nghĩ rằng bạn phải sử dụng Promise.all(arr.map(fn)).then(fn)(như tôi đã thực hiện nhiều lần) hoặc một loại đường ưa thích của Promise lib (đặc biệt là của Bluebird), tuy nhiên (với tín dụng cho bài viết này ), một arr.map(fn).reduce(fn)mô hình sẽ thực hiện công việc, với những ưu điểm của nó:

  • hoạt động với bất kỳ lời hứa lib nào - ngay cả các phiên bản jQuery tuân thủ trước - chỉ .then() được sử dụng.
  • Có tính linh hoạt để bỏ qua lỗi hoặc dừng lỗi, bất cứ khi nào bạn muốn với một mod dòng.

Đây là, viết cho Q.

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Lưu ý: chỉ có một đoạn đó, Q()dành riêng cho Q. Đối với jQuery, bạn cần đảm bảo rằng readFile () trả về một lời hứa jQuery. Với A + libs, những lời hứa nước ngoài sẽ được đồng hóa.

Chìa khóa ở đây là của giảm sequencelời hứa, mà trình tự các xử lý củareadFile lời hứa nhưng không phải là sự sáng tạo của họ.

Và một khi bạn đã hấp thụ điều đó, có lẽ hơi khó chịu khi bạn nhận ra rằng .map()sân khấu không thực sự cần thiết! Toàn bộ công việc, các cuộc gọi song song cộng với xử lý nối tiếp theo đúng thứ tự, có thể đạt được reduce()một mình, cộng với lợi thế tăng thêm tính linh hoạt để:

  • chuyển đổi từ các cuộc gọi async song song sang các cuộc gọi async nối tiếp bằng cách di chuyển một dòng - có khả năng hữu ích trong quá trình phát triển.

Đây là, cho Qmột lần nữa.

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Đó là mô hình cơ bản. Nếu bạn cũng muốn cung cấp dữ liệu (ví dụ: các tệp hoặc một số biến đổi của chúng) cho người gọi, bạn sẽ cần một biến thể nhẹ.


Tôi không nghĩ rằng đó là một ý tưởng tốt để trả lời các câu hỏi trái với ý định của OPS
Bergi

1
Điều này sequence.then(() => filePromise)là một antipotype - nó không truyền lỗi ngay khi họ có thể (và tạo ra các unhandledRejectionlib hỗ trợ chúng). Bạn nên sử dụng Q.all([sequence, filePromise])hay $.when(sequence, filePromise). Phải thừa nhận rằng, hành vi này có thể là những gì bạn muốn khi bạn cố gắng bỏ qua hoặc bỏ qua lỗi, nhưng ít nhất bạn nên đề cập đến điều này như một bất lợi.
Bergi

@Bergi, tôi hy vọng OP sẽ bước vào và đưa ra phán quyết về việc liệu điều này có thực sự trái với ý định của anh ấy hay không. Nếu không, tôi sẽ xóa câu trả lời tôi đoán, trong khi đó tôi hy vọng tôi đã biện minh cho vị trí của mình. Cảm ơn đã thực hiện nó đủ nghiêm túc để cung cấp thông tin phản hồi tốt. Bạn có thể giải thích thêm về mô hình chống, hoặc cung cấp một tài liệu tham khảo xin vui lòng? Có áp dụng tương tự cho bài viết mà tôi tìm thấy mô hình cơ bản ?
Roamer-1888

1
Có, phiên bản thứ ba của mã của anh ấy (đó là "cả song song và tuần tự") có cùng một vấn đề. "Antipotype" cần xử lý lỗi tinh vi và có xu hướng đính kèm các trình xử lý không đồng bộ, gây ra unhandledRejectioncác sự kiện. Trong Bluebird, bạn có thể giải quyết vấn đề này bằng cách sử dụng sequence.return(filePromise)hoạt động tương tự nhưng xử lý tốt các từ chối. Tôi không biết bất kỳ tài liệu tham khảo nào, tôi chỉ nghĩ ra nó - Tôi không nghĩ "mẫu (chống)" đã có tên.
Bergi

1
@Bergi, bạn có thể thấy rõ điều gì đó tôi không thể :( Tôi tự hỏi liệu mô hình chống mới này có cần được ghi lại ở đâu đó không?
Roamer-1888

0

Cách tiếp cận của bạn không phải là xấu, nhưng nó có hai vấn đề: nó nuốt lỗi và nó sử dụng Mô hình xây dựng hứa hẹn rõ ràng.

Bạn có thể giải quyết cả hai vấn đề này và làm cho mã sạch hơn, trong khi vẫn sử dụng cùng một chiến lược chung:

var Q = require("q");

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

Nếu người khác cần một cách được bảo đảm về cách giải quyết tuần tự NGHIÊM TRỌNG khi thực hiện các thao tác CRUD, bạn cũng có thể sử dụng mã sau đây làm cơ sở.

Miễn là bạn thêm 'return' trước khi gọi từng hàm, mô tả một Promise và sử dụng ví dụ này làm cơ sở, lệnh gọi hàm .then () tiếp theo sẽ bắt đầu sau khi hoàn thành hàm trước:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

Phương pháp đẩy và pop mảng có thể được sử dụng cho chuỗi các lời hứa. Bạn cũng có thể thúc đẩy những lời hứa mới khi bạn cần thêm dữ liệu. Đây là mã, tôi sẽ sử dụng trong React Infinite loader để tải chuỗi trang.

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

Hầu hết các câu trả lời không bao gồm kết quả của TẤT CẢ các lời hứa riêng lẻ, vì vậy trong trường hợp ai đó đang tìm kiếm hành vi cụ thể này, đây là một giải pháp khả thi bằng cách sử dụng đệ quy.

Nó theo phong cách của Promise.all:

  • Trả về mảng kết quả trong cuộc .then()gọi lại.

  • Nếu một số lời hứa thất bại, nó sẽ trả lại ngay lập tức trong cuộc .catch()gọi lại.

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

Lưu ý về taskskhai báo mảng :

Trong trường hợp này không thể sử dụng ký hiệu như Promise.allsau sẽ sử dụng:

const tasks = [promise(1), promise(2)]

Và chúng ta phải sử dụng:

const tasks = [() => promise(1), () => promise(2)]

Lý do là JavaScript bắt đầu thực hiện lời hứa ngay sau khi được tuyên bố. Nếu chúng ta sử dụng các phương thức như thế Promise.all, nó chỉ kiểm tra trạng thái của tất cả chúng là fulfilledhay rejected, nhưng không tự khởi động. Sử dụng () => promise()chúng tôi dừng thực thi cho đến khi nó được gọi.


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

Ở đây, chìa khóa là cách bạn gọi chức năng ngủ. Bạn cần phải vượt qua một loạt các chức năng mà chính nó trả về một lời hứa thay vì một loạt các lời hứa.


-1

Điều này là để mở rộng về cách xử lý một chuỗi các lời hứa theo cách chung chung hơn, hỗ trợ các chuỗi động / vô hạn, dựa trên việc thực hiện spex. Hậu quả :

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

var readFile = function (file) {
    // returns a promise;
};

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

Không chỉ giải pháp này sẽ hoạt động với các chuỗi có kích thước bất kỳ, mà bạn có thể dễ dàng thêm điều chỉnh dữ liệu và cân bằng tải cho nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.