当我们谈论Promise时,我们说些什么
栏目: JavaScript · 发布时间: 6年前
内容简介:各类详细的JavaScript语言的一大特点就是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。为了解决单线程的堵塞问题,现在,我们的任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
各类详细的 Promise
教程已经满天飞了,我写这一篇也只是用来自己用来总结和整理用的。如果有不足之处,欢迎指教。
为什么我们要用Promise
JavaScript语言的一大特点就是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。
为了解决单线程的堵塞问题,现在,我们的任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。而我们可能会写出一个回调金字塔,维护大量的callback将是一场灾难:
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // ... }); }); }); }); 复制代码
而Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
(new Promise(step1)) .then(step2) .then(step3) .then(step4); 复制代码
简单实现一个Promise
关于 Promise
的学术定义和规范可以参考Promise/A+规范,中文版 【翻译】Promises/A+规范 。
Promise有三个状态 pending
、 fulfilled
、 rejected
: 三种状态切换只能有两种途径,只能改变一次:
- 异步操作从未完成(pending) => 成功(fulfilled)
- 异步操作从未完成(pending) => 失败(rejected)
Promise 实例的 then
方法,用来添加回调函数。
then
方法可以接受两个回调函数,第一个是异步操作成功时(变为 fulfilled
状态)时的回调函数,第二个是异步操作失败(变为 rejected
)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
下面是一个写好注释的简单实现的Promise的实现:
class Promise { constructor(executor) { // 初始化state为pending this.state = 'pending' // 成功的值 this.value = undefined // 失败的原因 this.reason = undefined // 异步操作,我们需要将所有then中的成功调用保存起来 this.onResolvedCallbacks = [] // 异步操作,我们需要将所有then中的失败调用保存起来 this.onRejectedCallbacks = [] let resolve = value => { // 检验state状态是否改变,如果改变了调用就会失败 if (this.state === 'pending') { // resolve调用后,state转化为成功态 this.state = 'fulfilled' // 储存成功的值 this.value = value // 执行成功的回调函数 this.onResolvedCallbacks.forEach(fn => fn) } } let reject = reason => { // 检验state状态是否改变,如果改变了调用就会失败 if (this.state === 'pending') { // reject调用后,state转化为失败态 this.state === 'rejected' // 储存失败的原因 this.reason = reason // 执行失败的回调函数 this.onRejectedCallbacks.forEach(fn => fn) } } // 如果executor执行报错,直接执行reject try { executor(resolve, reject) } catch (err) { reject(err) } } // then 方法 有两个参数onFulfilled onRejected then(onFulfilled, onRejected) { // 状态为fulfilled,执行onFulfilled,传入成功的值 if (this.state === 'fulfilled') { onFulfilled(this.value) } // 状态为rejected,执行onRejected,传入失败的原因 if (this.state === 'rejected') { onRejected(this.reason) } // 当状态state为pending时 if (this.state === 'pending') { // onFulfilled传入到成功数组 this.onResolvedCallbacks.push(()=>{ onFulfilled(this.value); }) // onRejected传入到失败数组 this.onRejectedCallbacks.push(()=>{ onRejected(this.reason); }) } } } 复制代码
如果需要实现链式调用和其它API,请查看下面参考文档链接中的手写Promise教程。
优雅的使用Promise
使用Promise封装一个HTTP请求
function get(url) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { if (req.status == 200) { resolve(req.responseText); } else { reject(Error(req.statusText)); } }; req.onerror = function() { reject(Error("Network Error")); }; req.send(); }); } 复制代码
现在让我们来使用这一功能:
get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.error("Failed!", error); }) // 当前收到的是纯文本,但我们需要的是JSON对象。我们将该方法修改一下 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); } 复制代码
then()
不是Promise的最终部分,可以将各个 then
链接在一起来改变值,或依次运行额外的异步操作。
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); }) 复制代码
错误处理
then()
包含两个参数 onFulfilled
, onRejected
。 onRejected
是失败时调用的函数。
对于失败,我们还可以使用 catch
,对于错误进行捕捉,但下面两段代码是有差异的:
get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.log("Failed!", error); }) 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
拒绝时两者均被调用,因为它们在该链中是单独的步骤。看看下面的代码:
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!"); }) 复制代码
以下是上述代码的流程图形式:
蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。与 JavaScript 的 try/catch 一样,错误被捕获而后续代码继续执行。
并行和顺序:两者兼得
假设我们获取了一个 story.json
文件,其中包含了文章的标题,和段落的下载地址。
1. 顺序下载,依次处理
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'; }) 复制代码
2. 并行下载,完成后统一处理
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) { addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; }) 复制代码
3. 并行下载,一旦顺序正确立即渲染
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'; }) 复制代码
async / await
async
函数返回一个 Promise 对象,可以使用 then
方法添加回调函数。当函数执行的时候,一旦遇到 await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
基本用法
我们可以重写一下之前的 getJSON
方法:
// promise 写法 function getJSON(url) { return get(url).then(JSON.parse).catch(err => { console.log('getJSON failed for', url, err); throw err; }) } // async 写法 async function getJSON(url) { try { let response = await get(url) return JSON.parse(response) } catch (err) { console.log('getJSON failed for', url, err); } } 复制代码
注意:避免太过循环
假定我们想获取一系列段落,并尽快按正确顺序将它们打印:
// promise 写法 function chapterInOrder(urls) { return urls.map(getJSON) .reduce(function(sequence, chapterPromise) { return sequence.then(function() { return chapterPromise; }).then(function(chapter) { console.log(chapter) }); }, Promise.resolve()) } 复制代码
* 不推荐的方式:
async function chapterInOrder(urls) { for (const url of urls) { const chapterPromise = await getJSON(url); console.log(chapterPromise); } } 复制代码
推荐写法:
async function chapterInOrder(urls) { const chapters = urls.map(getJSON); // log them in sequence for (const chapter of chapters) { console.log(await chapter); } } 复制代码
以上所述就是小编给大家介绍的《当我们谈论Promise时,我们说些什么》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 当我们谈论 DevOps 时,我们在谈论什么?
- 当我们在谈论单元测试时我们在谈论什么
- 当我们在谈论单测时我们在谈论什么
- 当我们在谈论synchronized的时候,我们在谈论什么?
- 当我们谈论锁,我们谈什么
- 当我们谈论Monad的时候(二)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。