前言
各类详细的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);
}
}
复制代码