JavaScript Promise:去而复返

原文:http://www.html5rocks.com/en/tutorials/es6/promises/
作者:Jake Archibald
翻译:Amio

女士们先生们,请准备好驱逐 Web 开辟汗青上一个严重时刻……

[鼓声响起]

JavaScript 有了原生的 Promise!

[漫天的烟花绽放,人群沸腾了]

这时刻你大概是这三种人之一:

  • 你的身旁拥挤着喝彩的人群,然则你却不在个中,以至你还不大清楚“Promise”是什么。你耸耸肩,烟花的碎屑在你的身旁落下。如许的话,不要忧郁,我也是花了多年的时刻才邃晓 Promise 的意义,你能够从入门简介:他们都在冲动什么?最先看起。
  • 你一挥拳!太赞了对么!你已用过一些 Promise 的库,然则一切这些第三方实如今API上都略有差别,JavaScript 官方的 API 会是什么模样?看这里:Promise 术语
  • 你早就知道了,看着那些喝彩雀跃的新人你的嘴角出现一丝不屑的笑容。你能够平静享用一会儿优越感,然后直接去看 API 参考吧。

他们都在冲动什么?

JavaScript 是单线程的,这意味着任何两句代码都不能同时运转,它们得一个接一个来。在浏览器中,JavaScript 和其他使命同享一个线程,差别的浏览器略有差别,但大体上这些和 JavaScript 同享线程的使命包括重绘、更新款式、用户交互等,一切这些使命操纵都邑壅塞其他使命。

作为人类,你是多线程的。你能够用多个手指同时敲键盘,也能够一边开车一遍电话。唯一的全局壅塞函数是打喷嚏,打喷嚏时期一切其他事宜都邑停息。很烦人对么?尤其是当你开着车打着电话的时刻。我们都不喜好如许打喷嚏的代码。

你应当会用事宜加回调的要领来处置惩罚这类状态:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

如许就不打喷嚏了。我们增加几个监听函数,请求图片,然后 JavaScript 就住手运转了,直到触发某个监听函数。

上面的例子中唯一的题目是,事宜有可能在我们绑定监听器之前就已发作,所以我们先要搜检图片的complete属性:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

如许还不够,假如在增加监听函数之前图片加载发作毛病,我们的监听函数照样白搭,不幸的是 DOM 也没有为这个需求供应解决要领。而且,这还只是处置惩罚一张图片的状态,假如有一堆图片要处置惩罚那就更麻烦了。

事宜不是万金油

事宜机制最合适处置惩罚统一个对象上重复发作的事变—— keyup、touchstart 等等。你不需要斟酌当绑定监听器之前所发作的事变,当碰到异步请求胜利/失利的时刻,你想要的一般是如许:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

这就是 Promise。假如 HTML 图片元素有一个 ready() 要领的话,我们就能够如许:

img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});

基本上 Promise 照样有点像事宜回调的,除了:

  • 一个 Promise 只能胜利或失利一次,而且状态没法转变(不能从胜利变成失利,反之亦然)
  • 假如一个 Promise 胜利或许失利今后,你为其增加针对胜利/失利的回调,则相应的回调函数会立时实行

这些特征非常合适处置惩罚异步操纵的胜利/失利情形,你无需再忧郁事宜发作的时刻点,而只需对其做出相应。

Promise 相干术语

Domenic Denicola 审视了本文初稿,给我在术语方面打了个“F”,关了禁闭而且责令我打印 States and Fates 一百遍,还写了一封家长信给我父母。即便云云,我照样对术语有些含糊,不过基本上应当是如许:

一个 Promise 的状态能够是:

确认(fulfilled)– 胜利了
否认(rejected)– 失利了
守候(pending)– 还没有确认或许否认,举行中
终了(settled)– 已确认或许否认了

范例里还运用“thenable”来形貌一个对象是不是是“类 Promise”(具有名为“then”的要领)的。这个术语使我想起来前英格兰足球司理 Terry Venables 所以我只管少用它。

JavaScript 有了 Promise!

实在已有一些第三方库完成了 Promise:

上面这些库和 JavaScript 原生 Promise 都恪守一个通用的、规范化的范例:Promises/A+,jQuery 有个相似的要领叫 Deferreds,但不兼容 Promises/A+ 范例,因而会有点小题目,运用需谨慎。jQuery 另有一个Promise 范例,但只是 Deferreds 的缩减版,所以也有一样题目。

只管 Promise 的各路完成遵照统一范例,它们的 API 照样各不雷同。JavaScript Promise 的 API 比较靠近 RSVP.js,以下建立 Promise:

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。在回调函数中做一些异步操纵,胜利今后挪用 resolve,不然挪用 reject。

挪用 reject 的时刻通报给它一个 Error 对象只是个通例并不是必需,这和典范 JavaScript 中的 throw 一样。通报 Error 对象的优点是它包括了挪用客栈,在调试的时刻会有点用途。

如今来看看怎样运用 Promise:

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

then 吸收两个参数,胜利的时刻挪用一个,失利的时刻挪用另一个,两个都是可选的,所以你能够只处置惩罚胜利的状态或许失利的状态。

JavaScript Promise 最初以“Futures”的称号归为 DOM 范例,厥后改名为“Promises”,终究归入 JavaScript 范例。将其到场 JavaScript 而非 DOM 的优点是方便在非浏览器环境中运用,如Node.js(他们会不会在中心API中运用就是另一回事了)。

浏览器支撑和 Polyfill

现在的浏览器已(部份)完成了 Promise。

用 Chrome 的话,就像个 Chroman 一样装上 Canary 版,默许即启用了 Promise 支撑。假如是 Firefox 拥趸,装置最新的 nightly build 也一样。

不过这两个浏览器的完成都还不够完整完整,你能够在 bugzilla 上跟踪 Firefox 的最新进展或许到 Chromium Dashboard 检察 Chrome 的完成状态

要在这两个浏览器上抵达兼容规范 Promise,或许在其他浏览器以及 Node.js 中运用 Promise,能够看看这个 polyfill(gzip 今后 2K)

与其他库的兼容性

JavaScript Promise 的 API 会把任何包括有 then 要领的对象看成“类 Promise”(或许用术语来讲就是 thenable。叹息)的对象,这些对象经由 Promise.cast() 处置惩罚今后就和原生的 JavaScript Promise 实例没有任何区别了。所以假如你运用的库返回一个 Q Promise,那没题目,无缝融入新的 JavaScript Promise。

只管,如前所述,jQuery 的 Deferred 对象有点……没什么用,不过幸亏还能够转换成规范 Promise,你最好一拿到对象就立时加以转换:

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

这里 jQuery 的 $.ajax 返回一个 Deferred 对象,含有 then 要领,因而 Promise.cast 能够将其转换为 JavaScript Promise。不过有时刻 Deferred 对象会给它的回调函数通报多个参数,比方:

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

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

除了第一个参数,其他都邑被 JavaScript Promise 疏忽掉:

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

……还好这一般就是你想要的了,最少你能够用这个要领完成想要的。别的还要注重,jQuery 也没有遵照给否认回调函数通报 Error 对象的通例。

庞杂的异步代码变得更简朴了

OK,如今我们来写点现实的代码。假定我们想要:

  1. 显现一个加载指导图标
  2. 加载一篇小说的 JSON,包括小说名和每一章内容的 URL。
  3. 在页面中填上小说名
  4. 加载一切章节正文
  5. 在页面中增加章节正文
  6. 住手加载指导

……这个过程当中假如发作什么毛病了要关照用户,而且把加载指导停掉,不然它就会不断转下去,使人眼晕,或许搞坏界面什么的。

固然了,你不会用 JavaScript 去这么烦琐地显现一篇文章,直接输出 HTML 要快许多,不过这个流程是非常典范的 API 请求形式:猎取多个数据,当它们悉数完成今后再做一些事变。

起首搞定从收集加载数据的步骤:

将 Promise 用于 XMLHttpRequest

只需能坚持向后兼容,现有 API 都邑更新以支撑 Promise,XMLHttpRequest 是重点斟酌对象之一。不过如今我们先来写个 GET 请求:

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

点击这里检察代码运转页面,翻开控制台检察输出结果。如今我们能够直接提议 HTTP 请求而不需要手敲 XMLHttpRequest,如许以为好多了,能少看一次这个狂驼峰定名的 XMLHttpRequest 我就多快活一点。

链式挪用

“then”的故事还没完,你能够把这些“then”串连起来修改结果或许增加举行更多异步操纵。

值的处置惩罚

你能够对结果做些修改然后返回一个新值:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
});

回到前面的代码:

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

收到的相应是一个纯文本的 JSON,我们能够修改 get 函数,设置 responseType 请求服务器以 JSON 花样供应相应,不过照样用 Promise 的体式格局来搞定吧:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
});

既然 JSON.parse 只吸收一个参数,并返回转换后的结果,我们还能够再精简一点:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
});

点击这里检察代码运转页面,翻开控制台检察输出结果。事实上,我们能够把 getJSON 函数写得超等简朴:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON 会返回一个猎取 JSON 并加以剖析的 Promise。

行列的异步操纵

你也能够把 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 的异步请求,返回给我们更多 URL,然后我们会请求个中的第一个。Promise 最先初次显现出相较事宜回调的优越性了。你以至能够写一个抓取章节内容的自力函数:

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

我们一最先并不加载 story.json,直到第一次 getChapter,而今后每次 getChapter 的时刻都能够重用已加载完成的 story Promise,所以 story.json 只需要请求一次。Promise 好棒!

毛病处置惩罚

前面已看到,“then”吸收两个参数,一个处置惩罚胜利,一个处置惩罚失利(或许说确认和否认,按 Promise 术语):

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 被否认今后会跳转到今后第一个设置了否认回调的 then(或 catch,一样的)。关于 then(func1, func2) 来讲,必会挪用 func1 或 func2 之一,但绝不会两个都挪用。而 then(func1).catch(func2) 如许,假如 func1 返回否认的话 func2 也会被挪用,由于他们是链式挪用中自力的两个步骤。看下面这段代码:

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 代码块。这是上面那段代码的流程图(我最爱流程图了):

《JavaScript Promise:去而复返》

绿线是确认的 Promise 流程,红线是不是认的。

JavaScript 非常和 Promise

Promise 的否认回调能够由 Promise.reject() 触发,也能够由组织器回调中抛出的毛病触发:

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 否认。

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] 失利(http 500 或许用户掉线什么的)了,它会跳过今后一切针对胜利的回调,包括 getJSON 中将相应剖析为 JSON 的回调,和这里把第一张内容增加到页面里的回调。JavaScript 的实行会进入 catch 回调。结果就是前面任何章节请求失足,页面上都邑显现“Failed to show chapter”。

和 JavaScript 的 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';

假如只是要捕获非常做纪录输出,不打算在用户界面上对毛病举行反应的话,只需抛出 Error 就好了,这一步能够放在 getJSON 中:

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

如今我们已搞定第一章了,接下来搞定一切的。

并行和串行 —— 鱼与熊掌兼得

异步的头脑体式格局并不相符直觉,假如你以为起步难题,那就尝尝先写个同步的要领,就像这个:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none';

它实行起来完整一般!(检察示例)不过它是同步的,在加载内容时会卡住全部浏览器。要让它异步事情的话,我们用 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';
});

那末我们怎样遍历章节的 URL 而且顺次请求?如许是不可的

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
});

forEach 没有对异步操纵的支撑,所以我们的故事章节会依据它们加载完成的递次显现,基本上《低俗小说》就是这么写出来的。我们不写低俗小说,所以得修改它:

建立序列

我们要把章节 URL 数组转换成 Promise 的序列,照样用 then:

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

这是我们第一次用到 Promise.resolve,它会依据你传的任何值返回一个 Promise。假如你传给它一个类 Promise 对象(带有 then 要领),它会天生一个带有一样确认/否认回调的 Promise,基本上就是克隆。假如传给它任何别的值,如 Promise.resolve('Hello'),它会建立一个以这个值为完成结果的 Promise,假如不传任何值,则以 undefined 为完成结果。

另有一个对应的 Promise.reject(val),会建立以你传入的参数(或 undefined)为否认结果的 Promise。

我们能够用 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 回调会顺次运用在每一个数组元素上,第一轮里的“sequence”是 Promise.resolve(),今后的挪用里“sequence”就是上次函数实行的的结果。array.reduce 非常合适用于把一组值合并处置惩罚为一个值,恰是我们如今对 Promise 的用法。

汇总下上面的代码:

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

运转示例看这里,前面的同步代码革新成了完整异步的版本。我们还能够更进一步,如今页面加载的结果是如许:

《JavaScript Promise:去而复返》

浏览器很善于同时加载多个文件,我们这类一个接一个下载章节的要领非常不效力。我们愿望同时下载一切章节,悉数完成后一次搞定,恰好就有这么个 API:

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

依据衔接状态,革新的代码会比递次加载体式格局提速数秒,以至代码行数也更少。章节加载完成的递次不肯定,但它们显现在页面上的递次准确无误。

《JavaScript Promise:去而复返》

但是如许照样有进步空间。当第一章内容加载终了我们能够立时填进页面,如许用户能够在其他加载使命还没有完成之前就最先浏览;当第三章抵达的时刻我们若无其事,第二章也抵达今后我们再把第二章和第三章内容填入页面,以此类推。

为了抵达如许的结果,我们同时请求一切的章节内容,然后建立一个序列顺次将其填入页面:

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

哈哈(检察示例),鱼与熊掌兼得!加载一切内容的时刻未变,但用户能够更早看到第一章。

《JavaScript Promise:去而复返》

这个小例子中各部份章节加载差不多同时完成,逐章显现的战略在章节内容许多的时刻上风将会越发明显。

上面的代码假如用 Node.js 作风的回调或许事宜机制完成的话代码量约莫要翻一倍,更主要的是可读性也不云云例。但是,Promise 的凶猛不止于此,和其他 ES6 的新功用连系起来还能越发高效……

附赠章节:Promise 和 Generator

接下来的内容涉及到一大堆 ES6 的新特征,不过关于如今运用 Promise 来讲并不是必需,把它看成接下来的第二部奢华续集的预告片来看就好了。

ES6 还给我们带来了 Generator,许可函数在特定处所像 return 一样退出,然则稍后又能恢复到这个位置和状态上继承实行。

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

注重函数名前的星号,这示意该函数是一个 Generator。关键字 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

这对 Promise 有什么用呢?你能够用这类停息/继承的机制写出来和同步代码看上去差不多(明白起来也一样简朴)的代码。下面是一个辅佐函数(helper function),我们在 yield 位置守候 Promise 完成:

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.cast(result.value).then(onFulfilled, onRejected);
    }
  }
  var generator = generatorFunc();
  var onFulfilled = continuer.bind(continuer, "next");
  var onRejected = continuer.bind(continuer, "throw");
  return onFulfilled();
}

这段代码原样拷贝自 Q,只是改成 JavaScript Promise 的 API。把我们前面的终究计划和 ES6 最新特征连系在一起今后:

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

功用完整一样,读起来要简朴许多。这个例子现在能够在 Chrome Canary 中运转(检察示例),不过你得先到 about:flags 中开启 Enable experimental JavaScript 选项。

这里用到了一堆 ES6 的新语法:Promise、Generator、let、for-of。当我们把 yield 运用在一个 Promise 上,spawn 辅佐函数会守候 Promise 完成,然后才返回终究的值。假如 Promise 给出否认结果,spawn 中的 yield 则会抛出一个非常,我们能够用 try/catch 捕获到。如许写异步代码真是超等简朴!

Promise API 参考

除非分外说明,最新版的 Chrome(Canary) 和 Firefox(nightly) 均支撑以下一切要领。这个 Polyfill 则在一切浏览器内完成一样的接口。

静态要领

Promise.cast(promise);
返回一个 Promise(当且仅当 promise.constructor == Promise)
备注:现在唯一 Chrome 完成

Promise.cast(obj);
建立一个以 obj 为胜利结果的 Promise。
备注:现在唯一 Chrome 完成

Promise.resolve(thenable);
从 thenable 对象建立一个新的 Promise。一个 thenable(类 Promise)对象是一个带有“then”要领的对象。假如你传入一个原生的 JavaScript Promise 对象,则会建立一个新的 Promise。此要领涵盖了 Promise.cast 的特征,然则不如 Promise.cast 更简朴高效。

Promise.resolve(obj);
建立一个以 obj 为确认结果的 Promise。这类状态下等同于 Promise.cast(obj)。

Promise.reject(obj);
建立一个以 obj 为否认结果的 Promise。为了一致性和调试方便(如客栈追踪),obj 应当是一个 Error 实例对象。

Promise.all(array);
建立一个 Promise,当且仅当传入数组中的一切 Promise 都确认今后才确认,假如碰到数组中的任何一个 Promise 以否认终了,则抛出否认结果。每一个数组元素都邑起首经由 Promise.cast,所以数组能够包括类 Promise 对象或许其他对象。确认结果是一个数组,包括传入数组中每一个 Promise 的确认结果(且坚持递次);否认结果是传入数组中第一个碰到的否认结果。
备注:现在唯一 Chrome 完成

Promise.race(array);
建立一个 Promise,当数组中的恣意对象确认时将其结果作为确认终了,或许当数组中恣意对象否认时将其结果作为否认终了。
备注:我不大肯定这个接口是不是有效,我更倾向于一个 Promise.all 的对峙要领,仅当一切数组元素悉数给出否认的时刻才抛出否认结果

    原文作者:Amio
    原文地址: https://segmentfault.com/a/1190000000363893
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞