Promise必知必会
前端开发中经常会进行一些异步操作,常见的异步有:
- 网络请求:ajax
- IO操作: readFile
- 定时器:setTimeout
回调
最基础的异步解决方案莫过于回调函数了
前端经常会在成功时和失败时分别注册回调函数
const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { // 成功的回调 if (req.status === 200) { console.log(req.statusText) } }; req.onerror = function () { // 失败的回调 console.log(req.statusText) }; req.send();
node的异步api,则通常只注册一个回调函数,通过约定的参数来判断到底是成功还是失败:
const fs = require("fs"); fs.readFile('input.txt', function (err, data) { // 回调函数 // 第一个参数是err,如果有err,则表示调用失败 if (err) { return console.error(err); } console.log("异步读取: " + data.toString()); });
回调的异步解决方案本身也简单易懂,但是它有一个致命的缺点:无法优雅的控制异步流程
什么意思?
单个异步当然可以很简单的使用回调函数,但是对于多个异步操作,就会陷入回调地狱中
// 请求data1成功后再请求data2,最后请求data3 const ajax = $.ajax({ url: 'data1.json', success: function(data1) { console.log(data1); $.ajax({ url: 'data2.json', success: function(data2) { console.log(data2); $.ajax({ url: 'data3.json', success: function(data3) { console.log(data3); } }) } }) } })
这种要按顺序进行异步流程控制的场景,回调函数就显得捉襟见肘了。这时,Promise的异步解决方案就被提了出来。
Promise
当初在学Promise时,看得我真是一脸懵逼,完全不明白这货到底怎么用。其实,Promise的api要分成两部分来理解:
- Promise构造函数:resolve reject (改变内部状态)
- Promise对象: then catch (流程控制)
Promise对象
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)
初始时,该对象状态为pending,之后只能变成fulfilled和rejected其中的一个
then方法有两个参数,分别对应状态为fulfilled和rejected时的回调函数,其中第二个参数可选
promise.then(function(value) { // success }, function(error) { // failure });
通常我们会省略then的第二个参数,而改用catch来注册状态变为rejected时的回调函数
promise.then(function(value) { // success }).catch(function(error) { // failure });
Promise构造函数
Promise对象怎么生成的呢?就是通过构造函数new出来的。
const promise = new Promise(function(resolve, reject) { });
Promise构造函数接收一个函数作为参数,这个函数可以接收两个参数:resolve和reject
resolve, reject是两个函数,由JavaScript引擎提供,不用自己编写
前面我们说过,Promise对象有三种状态,初始时为pending,之后可以变成fulfilled或者rejected,那怎么改变状态呢?答案就是调用resolve或者reject
调用resolve时,状态变成fulfilled,表示异步已经完成;调用reject时,状态变成rejected,表示异步失败。
回调和Promise的对比
其实这里就是Promise最难理解的地方了,我们先看下例子:
回调函数封装
function getURL(URL, success, error) { const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { success(req.responseText); } else { error(new Error(req.statusText)); } }; req.onerror = function () { error(new Error(req.statusText)); }; req.send(); } const URL = "http://httpbin.org/get"; getURL(URL, function onFulfilled(value) { console.log(value); }, function onRejected(error) { console.error(error); })
Promise封装
function getURL(URL) { return new Promise(function (resolve, reject) { const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } const URL = "http://httpbin.org/get"; getURL(URL).then(function onFulfilled(value){ console.log(value); }).catch(function onRejected(error){ console.error(error); });
两段代码最大的区别就是:
用回调函数封装的getURL函数,需要明显的传给它成功和失败的回调函数,success和error的最终调用是在getURL里被调用的
用Promise封装的getURL函数,完全不关心成功和失败的回调函数,它只需要在ajax成功时调用resolve(),告诉promise对象,你现在的状态变成了fulfilled,在ajax失败时,调用reject()。而真正的回调函数,是在getURL的外面被调用的,也就是then和catch中调用
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
function getURL(URL) { return new Promise(function (resolve, reject) { const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } const URL = "http://httpbin.org/get"; const URL2 = "http://deepred5.com/cors.php?search=ntr"; getURL(URL).then(function onFulfilled(value){ console.log(value); // 返回了一个新的Promise对象 return getURL(URL2) }).then(function onFulfilled(value){ console.log(value); }).catch(function onRejected(error){ console.error(error); });
这段代码就充分说明了Promise对于流程控制的优势:读取URL的数据后再读取URL2,没有了之前的回调地狱问题。
Promise应用
Promise经常用于对函数的异步流程封装
function getURL(URL) { return new Promise(function (resolve, reject) { const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); }
const preloadImage = function (path) { return new Promise(function (resolve, reject) { const image = new Image(); image.onload = resolve; image.onerror = reject; image.src = path; }); };
const fs = require('fs') const path = require('path') const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) } else { resolve(data.toString()) } }) }) }
结合上面几个例子,我们可以看出Promise封装代码的基本套路:
const methodPromise = function() { return new Promise((resolve, reject) => { // 异步流程 if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }) }
Promise.race Promise.all
Promise.all 接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve的时候,它才会去调用then方法,如果其中有一个变为rejected,就直接调用catch方法
传给then方法的是一个数组,里面分别对应promise返回的结果
function getURL(URL) { return new Promise(function (resolve, reject) { const req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } Promise.all([getURL('http://deepred5.com/cors.php?search=ntr'), getURL('http://deepred5.com/cors.php?search=rbq')]) .then((dataArr) => { const [data1, data2] = dataArr; }).catch((err) => { console.log(err) })
Promise.race类似,只不过只要有一个Promise变成resolve就调用then方法
Promise.resolve Promise.reject
Promise.resolve(42); // 等价于 new Promise(function(resolve){ resolve(42); }); Promise.reject(new Error("出错了")) // 等价于 new Promise(function(resolve,reject){ reject(new Error("出错了")); });
Promise.resolve(42).then(function(value){ console.log(value); }); Promise.reject(new Error("出错了")).catch(function(error){ console.error(error); });
Promise.resolve方法另一个作用就是将thenable对象转换为promise对象
const promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象 promise.then(function(value){ console.log(value); });
thenable对象指的是具有then方法的对象:
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
异常捕获
理想状态下,Promise可以通过catch捕获到异常,但是如果我们没有使用catch,那么虽然控制台会打印错误,但是这次错误并不会终止脚本执行
<script> const a = b.c.d; console.log(1); // 代码报错,不会运行到此处 </script> <script> console.log(2); // 代码运行 </script>
上述代码只会打印2
<script> const promise = new Promise((resolve, reject) => { const a = b.c.d; resolve('ok'); }) promise.then(data => { console.log(data) }) console.log(1); // 代码报错,但是会运行到此处 </script> <script> console.log(2); // 代码运行 </script>
打印1和2
解决方法:
window有一个unhandledRejection事件,专门监听未捕获的reject错误
window.onunhandledrejection = function(e) { console.log(e.reason); } const promise = new Promise((resolve, reject) => { const a = b.c.d; resolve('ok'); }) promise.then(data => { console.log(data) })