JS Promises - ES6(ECMAScript 6)

Promise是一个回调对象,取代Callback函数,避免地狱式回调。

传统回调

function successCallback(result) {
 console.log("It succeeded with " + result);
}

function failureCallback(error) {
 console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);

Promise回调

let promise = doSomething(); 
promise.then(successCallback, failureCallback);

或者

doSomething().then(successCallback, failureCallback);

地狱式回调

doSomething(function(result) {
 doSomethingElse(result, function(newResult) {
  doThirdThing(newResult, function(finalResult) {
   console.log('Got the final result: ' + finalResult);
  }, failureCallback);
 }, failureCallback);
}, failureCallback);

Promise链式调用

doSomething().then(function(result) {
 return doSomethingElse(result);
})
.then(function(newResult) {
 return doThirdThing(newResult);
})
.then(function(finalResult) {
 console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

异常捕获处理

new Promise((resolve, reject) => {
  console.log('Initial');

  resolve();
})
.then(() => {
  throw new Error('Something failed');
     
  console.log('Do this');
})
.catch(() => {
  console.log('Do that');
})
.then(() => {
  console.log('Do this whatever happened before');
});

或者

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

异步方式

try {
 let result = syncDoSomething();
 let newResult = syncDoSomethingElse(result);
 let finalResult = syncDoThirdThing(newResult);
 console.log(`Got the final result: ${finalResult}`);
} catch(error) {
 failureCallback(error);
}

同步方式

ECMAScript 2017 中async/await 語法糖提供同步功能:

async function foo() {
 try {
  let result = await doSomething();
  let newResult = await doSomethingElse(result);
  let finalResult = await doThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
 } catch(error) {
  failureCallback(error);
 }
}

Promise组合

处理或拒绝:

Promise.resolve() ,Promise.reject()

并行处理工具:

Promise.all(),Promise.race() 

处理与拒绝

Promise.resolve(value) 
Promise.reject(reason)

语法:

Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable); 
Promise.reject(reason);

示例:

Promise.resolve('Success').then(function(value) {
 console.log(value); // "Success"
}, function(value) {
 // not called
});

判断阵列

var p = Promise.resolve([1,2,3]);
p.then(function(v) {
 console.log(v[0]); // 1
});

判段另一个Promise

var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
 console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

// logs, in order:
// original === cast ? true
// value: 33

判段thenables 及拋出 Errors

// Resolving a thenable object
var p1 = Promise.resolve({ 
 then: function(onFulfill, onReject) { onFulfill('fulfilled!'); }
});
console.log(p1 instanceof Promise) // true, object casted to a Promise

p1.then(function(v) {
  console.log(v); // "fulfilled!"
 }, function(e) {
  // not called
});

// Thenable throws before callback
// Promise rejects
var thenable = { then: function(resolve) {
 throw new TypeError('Throwing');
 resolve('Resolving');
}};

var p2 = Promise.resolve(thenable);
p2.then(function(v) {
 // not called
}, function(e) {
 console.log(e); // TypeError: Throwing
});

// Thenable throws after callback
// Promise resolves
var thenable = { then: function(resolve) {
 resolve('Resolving');
 throw new TypeError('Throwing');
}};

var p3 = Promise.resolve(thenable);
p3.then(function(v) {
 console.log(v); // "Resolving"
}, function(e) {
 // not called
});

Promise.reject()

Promise.reject(new Error('fail')).then(function(error) {
 // not called
}, function(error) {
 console.log(error); // Stacktrace
});

并行处理

Promise.all(iterable);

iterable对象可以是Array 或 String。

reduce方案:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Promise.all():

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
 setTimeout(resolve, 100, 'foo');
}); 

Promise.all([p1, p2, p3]).then(values => { 
 console.log(values); // [3, 1337, "foo"] 
});

等全部完成或一个拒绝。

Promise.race(iterable)

var p1 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 500, 'one'); 
});
var p2 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 100, 'two'); 
});

Promise.race([p1, p2]).then(function(value) {
 console.log(value); // "two"
 // Both resolve, but p2 is faster
});


一旦迭代器中的某个promise已完成或拒绝,就可以进行处理。

Promise特性

promises具有事件监听器的基本功能,并具备更多特性:

promises只能有一次成功或失败,也就是说它不会重复成功或失败,也不能从成功切换到失败,反之亦然。

promise收到成功或失败,即便之后加入成功/失败回调函数,这些回调函数也能被正确调用,即使事件发生在回调函数加入之前。

这对处理异步成功/失败非常有用,因为,通常只会对结果感兴趣,对确切的发生时间并不感兴趣。

Promise相关术语

fulfilled:与promise成功事件相关。

rejected:与promise失败事件相关。

pending:尚未完成或被拒绝。

settled:已完成或被拒绝

Promises的由来

Promises以库的形式出现已经有一段时间了,比如:QwhenWinJSRSVP.js

这些libraries与JavaScript中的Promises都遵循Promise/A+通用标准,也就是说他们的行为具有一定共同性。

jQuery中类似Promises的对象是Deferreds对象。

但是,Deferreds不兼容Promise/A+标准,所以,在使用时应注意到它们的差异。

jQuery也有一个Promise类型,但它只是Deferreds子集。

创建promise

虽然promise实现遵循标准化行为,但它们间的API略有不同。

JavaScript中的promises API与RSVP.js类似:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

promise构造函数接受一个参数,参数是一个回调函数,回调函数有两个参数,resolve和reject。

在回调(可能是async)中执行一些操作,如果一切正常调用resolve,否则调用reject。

以前习惯在JavaScript中抛出异常,使用Error对象拒绝是一种习惯,但并不是必需。

错误对象的好处是能够捕获跟踪堆栈,借助调试工具快速定位问题。

promise:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then函数接受两个参数,用于成功回调与失败回调。二者都是可选,可只为成功或失败添加回调。

JavaScript promise最初在DOM中被称为“future”,后来改名为“promise”,最后,直接加入到JavaScript。

在JavaScript中使用它们比依赖DOM好,因为,能够在非浏览器环境中使用,如:Node.js。

虽然是JavaScript特性,但不妨碍与DOM的协作。

事实上,所有使用异步方法的新DOM API都使用了promise。如:Quota ManagementFont Load Events, ServiceWorker, Web MIDI, Streams等方面。

浏览器支持

Chrome 32,Opera 19,Firefox 29,Safari 8、Microsoft Edge

默认都启用了promises。

缺少promises支持的浏览器可使用polyfill(2k gzip),rsvp.js的一个子集。

兼容性

JavaScript promise API使用then()方法处理问题,若使用其他的库(如:Q )返回promise,JavaScript promise也能很好的处理。

jQuery Deferreds虽然很棒,但也可以将jQuery Deferreds转成标准的promise:

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

jQuery $.ajax返回Deferred,由于Deferred也有一个then()方法, Promise.resolve()能够将它变成JavaScript promise。

但是,有时deferreds也会将多个参数传递给回调函数,如:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

此时JS promise会忽略第一个参数后的所有参数:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

注:jQuery不遵循将错误对象传递给rejections的约定。

简化异步代码

Promise风格的ajax:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

有序异步操作

链式调用模式多次调用then函数,以顺序方式运行异步操作。

第一个then()回调返回数据后,使用该值调用下一个then()。

如果,返回类似promise的东西,则下一个then()会等待它,并在promise成功/失败时调用。如:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

示例说明:一个异步请求story.json,在回调中发出另一个请求。

Promise实战

通过一个获取文章的示例,理解promise api。

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

重用Promise中的getChapter,只调用一次story.json。

Promise错误处理

正如之前看到的,then()需要两个论点,一个是成功的,一个是失败的:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

也可使用catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch()没有什么特别之处,只是then(undefined, func)函数的语法糖,增加代码可读性。两个示例行为并不相同,后者相当于:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

这种细微差别非常有用。

Promise rejections通过一个rejection回调(或catch())跳到下一个then()。

func1或func2将被调用,但永远不会同时调用。

但使用then(func1).catch(func2),若func1 rejects,两个函数都将被调用,它们之间都是独立。考虑以下事项:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

示例流程与普通JavaScript try / catch非常相似,在“try”中发生的错误会立即转到catch()块。流程图如下所示:

ec6-primers

蓝线表示Promise 完成,红色表示Promise拒绝。

JS exceptions与promises exceptions

Rejections在promises中是一种显式拒绝,若在构造函数回调中抛出一个错误是种隐式拒绝:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

在promise构造中传入回调函数,在其中执行与promise相关的工作很有用,错误会被自动捕获并被拒绝。

then()回调中抛出的错误也是如此。

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

实际处理错误时,使用catch显示错误:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

如果获取story.chapterUrls[0]失败,将跳过后续的成功回调,并转向catch回调,页面将显示“无法显示文章”。

就像JavaScript中的try/catch一样,错误被捕获后,后续代码继续。

以下内容为非阻塞异步版本:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

如果只想通过catch()记录日志,无需从错误中恢复,只需重新抛出错误。

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

并行和顺序

异步并不容易,下载会被同步并阻塞浏览器。

为了使这个工作异步,使用then()函数来让处理单元一个接一个地执行:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

创建序列

将chapterUrls数组转换为一系列Promise:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });

resolves接收传给它的任何value。

若传递的是一个实例,Promise直接返回此实例。

若传递一个类似Promise的对象(包含一个then()方法),将创建一个真正的Promise。

若传入其他值,如,Promise.resolve('Hello'),将创建一个使用该值实现的promise。

若没有传入任何参数,将用“undefined”来填充。

Promise.reject(val),将创建一个Promise,使用传入的值创建一个rejects(或undefined)。

array.reduce

使用array.reduce整理上述代码

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

与前个示例相同,但不需要单独的“sequence”变量。

reduce回调函数会为数组中每一项执行Promise.resolve,对于后续回调函数,“sequence”是上一个调用返回的值。

array.reduce对于将数组化简成单个值的场景非常有用。代码如下所示:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

ec6-promise

异步版本

浏览器擅长一次下载多个东西,所以,一个接一个地下载性能并不高。

同时下载内容:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all创建一个Promise数组,并构造一个在所有Promise完成时执行的Promise,将结果装入数组,顺序与传入的Promise保持一致。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

章按任何顺序下载,以正确顺序组织后显示在屏幕上。

ec6 promise

仍有提升空间,第一部分到达时,将它添加到页面中。

用户在剩余部分未到达时就可以开始阅读。

第三部分到来时,不再立即添加到页面中,而是等待第二部分到达后,将两部分按顺序添加到页面上。

到此,可以同时发出请求,创建一个序列,将结果加入dom中:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

这样做的好处:

用第一部分数据,争取用户体验。后续的数据以性能为标准合理安排。

ec6 promise

ES6生成器

对于ES6这些只是预告片,想看真正的大片还得再了解一点其他特性,如,生成器。

ES6生成器允许函数在特定点退出,如:“return”。

稍后从同一点恢复,如:

function *addGenerator() {
  var i = 0;
  while (true) {
    i += yield i;
  }
}

注:函数名前面的星号(*)是生成器与普通函数的区别。yield关键字是返回/恢复点:

var adder = addGenerator();
adder.next().value; // 0
adder.next(5).value; // 5
adder.next(5).value; // 10
adder.next(5).value; // 15
adder.next(50).value; // 65

这对promises意味着什么呢?

使用ES6生成器特性,能让异步代码如编写同步代码一样简单。

下面示例是常见写法与利用yield等待promises间的区别:

function spawn(generatorFunc) {
  function continuer(verb, arg) {
    var result;
    try {
      result = generator[verb](arg);
    } catch (err) {
      return Promise.reject(err);
    }
    if (result.done) {
      return result.value;
    } else {
      return Promise.resolve(result.value).then(onFulfilled, onRejected);
    }
  }
  var generator = generatorFunc();
  var onFulfilled = continuer.bind(continuer, "next");
  var onRejected = continuer.bind(continuer, "throw");
  return onFulfilled();
}

spawn(function *() {
  try {
    // 'yield' effectively does an async wait,
    // returning the result of the promise
    let story = yield getJSON('story.json');
    addHtmlToPage(story.heading);

    // Map our array of chapter urls to
    // an array of chapter json promises.
    // This makes sure they all download in parallel.
    let chapterPromises = story.chapterUrls.map(getJSON);

    for (let chapterPromise of chapterPromises) {
      // Wait for each chapter to be ready, then add it to the page
      let chapter = yield chapterPromise;
      addHtmlToPage(chapter.html);
    }

    addTextToPage("All done");
  }
  catch (err) {
    // try/catch just works, rejected promises are thrown here
    addTextToPage("Argh, broken: " + err.message);
  }
  document.querySelector('.spinner').style.display = 'none';
})