开胃菜,先做如下思考:
- Promise 有几种状态?
- Promise 状态之间可以转化吗?
- Promise 中的异常可以被
try...catch
捕获吗?
Promise前世
callback hell
大家都知道JS是异步操作(执行)的,在传统的异步编程中最大的问题就是回调函数的嵌套,一旦嵌套次数过多,我们的代码就难以维护和理解,这就是所谓的 回调地狱callback hell
。以jQuery为例:
$.ajax({url: "/getA",success: function(a) {$.ajax({url: '/getB',data: a.data,success: function(b){$.ajax({url: '/getC',data: b.data,success: function(c){console.log('运行到这真不容易')}})}});}
});
复制代码
特别是在ajax(请求参数依赖上一次请求的结果)中经常可以出现这种现象。 当然实际情况,我们不会那样写(什么?你就是这样,赶紧改),而是会使用函数封装一下再调用:
$.ajax({url: "/getA",success: function(a) {getB(a.data)}
});getB(data){$.ajax({url: '/getB',data: data,success: function(b){getC(b.data)}})
}getC(data){$.ajax({url: '/getC',data: data,success: function(c){console.log('运行到这真不容易')}})
}
复制代码
but,这还是回调函数调用,只不过换了种便于维护的另一种写法。
除非是老项目不想进行改动,新项目中最好不要这么干了
jQuery.defered
为了解决上述情况,jQuery在v1.5 版本中引入了 deferred
对象,初步形成 promise
概念。ajax
也成了一个 deferred
对象。
$.ajax({url: "/getA",
}).then(function(){console.log('a')
});
复制代码
我们也可以这样来定义一个 deferred
对象:
function loadImg(src){var dtd = $.Deferred(); // 定义延迟对象var img = new Image();img.onload = function(){dtd.resolve(img)}img.onerror = function(){dtd.reject()}img.src = src;return dtd; // 记得要返回哦
}
var result = loadImg('img.png');
result.then(function(img){console.log(img.width)
},function(){console.log('fail')
})
复制代码
完整例子:戳我
在 ES6 Promise 出现之前,有很多典型的Promise库,如:bluebird、Q 、when 等。
bluebird
bluebird
是一个第三方Promise
规范实现库,它不仅完全兼容原生Promise
对象,且比原生对象功能更强大。
安装
Node:
npm install bluebird
复制代码
Then:
const Promise = require("bluebird");
复制代码
Browser: 直接引入js库即可,就可以得到一个全局的 Promise
和 P(别名)
对象。
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>
复制代码
使用
function loadImg(src) {return new Promise((resolve,reject) => {const img = new Image();img.onload = ()=>{resolve(img);}img.onerror = () => {reject()}img.src = src;})
}
const result = loadImg('http://file.ituring.com.cn/SmallCover/17114893a523520c7382');
result.then(img => {console.log(img.width)
},() => {console.log('fail')
});
console.log(Promise === P) // true
复制代码
为了与原生的 Promise
进行区别,我们在控制台中打印了 Promise === P
的结果。结果和预期一样:
完整demo:戳我
bluebird
相比原生规范实现来说,它的功能更强大,浏览器兼容性好(IE8没问题),提供了很多丰富的方法和属性:
- Promise.props
- Promise.any
- Promise.some
- Promise.map
- Promise.reduce
- Promise.filter
- Promise.each
- Promise.mapSeries
- cancel
- ...more
更多详细的功能查看 官网API 。
我们发现,这里的 Promise
是可以取消的。
Promise今生
Promise最早是由社区提出和实现的,ES6写成了语言标准,它给我们提供一个原生的构造函数Promise,无需使用第三方库或者造轮子来实现。
Promise 语法
function loadImg(src) {return new Promise((resolve,reject) => {const img = new Image();img.onload = ()=>{resolve(img);}img.onerror = () => {reject()}img.src = src;})
}
const result = loadImg('http://file.ituring.com.cn/SmallCover/17114893a523520c7382');
result.then(img => {console.log(img.width)
},() => {console.log('fail')
})
复制代码
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。在代码中,经常使用 resolve
来表示 fulfilled
状态。
Promise 特点
- 状态的不可改变,状态一旦由
pending
变为fulfilled
或从pending
变为rejected
,就不能再被改变了。 Promise
无法取消,一旦新建它就会立即执行,无法中途取消,对于ajax
类的请求无法取消,可能存在资源浪费情况。Promise
内部抛出的错误,不会反应到外部,无法被外部try...catch
捕获,只能设置catch
回调函数或者then(null, reject)
回调
try{new Promise((resolve,reject)=>{throw new Error('错误了')}).catch(()=>{console.log('只能被catch回调捕获')})
}catch(e){console.log('只怕永远到不了这里啦')
}
复制代码
方法
- Promise.prototype.then()
then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。then
会创建并返回一个新的promise,可以用来实现Promise 链式操作。
思考:Promise.then 链式和 jQuery的链式操作有何不同? jQuery的链式方法返回的都是jQuery当前对象
-
Promise.prototype.catch() 是
.then(null, rejection)
的别名,用于指定发生错误时或者状态变成rejected
的回调函数。 -
Promise.prototype.finally() ES 2018引入的标准,不管
promise
的状态如何,只要完成,都会调用该函数。 -
Promise.all() 将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
复制代码
只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,否则p
的状态就变成rejected
。 这种模式在传统上称为__门__:所有人到齐了才开门。 适用场景:需要等待多个并行任务完成之后才能继续下一个任务。 典型例子:一个页面有多个请求,在请求完成或失败前需要一直显示loading效果。
-
Promise.race() 和
Promise.all
一样,将多个 Promise 实例,包装成一个新的 Promise 实例。不同的是,只要p1
、p2
、p3
之中有一个实例率先改变状态(fulfilled
或者rejected
),p
的状态就跟着改变。 这种模式传统上称为__门闩__:第一个到达的人就打开门闩。 典型例子:超时检测 -
Promise.resolve() 将现有对象转为 Promise 对象,
Promise.resolve
等价于:
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
复制代码
- Promise.reject() 将现有对象转为 Promise 对象,
Promise.reject
等价于:
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
复制代码
Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数被调用后并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(Iterator对象)。
与普通函数相比,它有两个特征:
function
关键字与函数名之间有一个星号;- 函数体内部使用
yield
表达式
ES6 没有规定 function
关键字与函数名之间星号的位置,下面写法都能通过:
function * foo() { ··· }
function *foo() { ··· }
function* foo() { ··· }
function*foo() { ··· }
复制代码
function *helloWorldGen() {yield 'hello';yield 'world';return 'ending';
}const hw = helloWorldGen();
复制代码
定义之后,需要调用遍历器对象的next
方法。每次调用next
方法,从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。next
方法返回一个对象,包含value
和done
属性,value
属性是当前yield
表达式值或者return
语句后面表达式的值,如果没有,则是undefined
。done
属性表示是否遍历结束。
hw.next()
// { value: 'hello', done: false }hw.next()
// { value: 'world', done: false }hw.next()
// { value: 'ending', done: true }hw.next()
// { value: undefined, done: true }
复制代码
yield
yield
表达式就是暂停标志,只能用在 Generator 函数里。 Generator 函数可以不使用 yield
表达式,这样就变成一个单纯的暂缓执行函数。
function *slow(){console.log('调用next才执行呀')
}
const gen = slow();
gen.next();
复制代码
yield
可以接受 next
方法的参数作为上一个 yield
表达式的返回值。
function *foo(x) {var y = x * (yield);return y;
}
const it = foo(6);
it.next(); // 启动foo()
// {value: undefined, done: false}it.next(7)
// {value: 42, done: true}
复制代码
第一次使用
next
方法时,传递参数是无效的。
for...of循环
使用for...of
循环,可以自动遍历 Generator 函数生成的迭代器对象,此时不需要调用 next
方法。
function *foo() {yield 1;yield 2;yield 3;yield 4;yield 5;return 6;
}
// 到6时done:true,不会进入循环体内执行
for (let v of foo()) {console.log(v);
}
// 1 2 3 4 5
复制代码
for...of
循环在每次迭代中自动调用 next()
,不会给 next
传值,并且在接收到 done:true
之后自动停止(不包含此时返回的对象)。
对于一些迭代器总是返回
done:false
的,需要加一个break
条件,防止死循环。
我们也可以手动实现迭代器循环:
let it = foo();
// 这种的有点就是可以向next传递参数
for(let ret; (ret=it.next()) && !ret.done;) {console.log(ret.value)
}
复制代码
Generator + Promise
我们先看下基于Promise 的实现方法:
function getData() {return request('http://xxx.com')
}
getData().then((res)=> {console.log(res)},()=>{console.log('fail')})
复制代码
结合Generator使用:
function getData() {return request('http://xxx.com')
}
function *main(){try {const text = yield getData();console.log(text)} catch (error) {console.log(error)}
}
复制代码
执行main方法如下:
const it = main();
const p = it.next().value;
p.then((text)=>{console.log(text)
},(err)=>{console.log(err)
})
复制代码
尽管 Generator 函数将异步操作简化,但是执行的流程管理很不方便(需要手动调用 next
执行),有更好的方式吗?肯定是有的。
co
co 是TJ 大神发布的一个 Generator 函数包装工具,用于自动执行 Generator 函数。
co 模块可以让我们不用编写 next
进行迭代,就会自动执行:
const co = require('co');function getData() {return request('http://xxx.com')
}
const gen = function *(){const text = yield getData();console.log(text)
}co(gen).then(()=>{console.log('gen执行完成')
})
复制代码
co
函数返回一个Promise
对象,等到 Generator 函数执行结束,就会输出一行提示。
async & await
ES2017 标准引入了 async 函数,它就是 Generator 函数的语法糖。
Node V7.6+已经原生支持
async
了。 Koa2 也使用async
替代之前的Generator
版本。
基本用法
async function fetchImg(url) {const realUrl = await getMainUrl(url);const result = await downloadImg(realUrl);return result;
}fetchImg('https://detail.1688.com/offer/555302162390.html').then((result) => {console.log(result)
})
复制代码
和 Generator 函数对比,async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
。其功能和 co
类似,自动执行。
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。 async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。 只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
await
后面是一个 Promise 对象。如果不是,会被转成一个立即resolve
的 Promise 对象。
async function f() {return await 123;
}f().then(v => console.log(v))
// 123
复制代码
await 必须在 async 函数中执行!
实例:按顺序完成异步操作
讲一下一个可能会遇到的场景:经常遇到一组异步操作,需要按照顺序完成。比如,依次根据图片url下载图片,按照读取的顺序输出结果。
一个async的实现:
async function downInOrder(urls, path, win) {for(const url of urls) {try {await downloadImg(url, path)win.send('logger', `图片 ${url} 下载完成`)} catch (error) {win.send('logger', `图片 ${url} 下载出错`)}}
}
复制代码
上述这种实现,代码确实简化了,但是效率很差,需要一个操作完成,才能进行下一个操作(下载图片),不能并发执行。
并发执行,摘自我的一个半成品1688pic :
async function downInOrder(urls, path, win) {// 并发执行const imgPromises = urls.map(async url => {try {const resp = await downloadImg(url, path);return `图片 ${url} 下载完成`;} catch (error) {return `图片 ${url} 下载出错`;}})// 按顺序输出for (const imgPromise of imgPromises) {win.send('logger', await imgPromise);}
}
复制代码
上面代码中,虽然map
方法的参数是async
函数,但它是并发执行的,因为只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,因此实现了按顺序输出。
这块基本参考的是阮一峰老师的教程
总结
使用 async / await, 可以通过编写形似同步的代码来处理异步流程, 提高代码的简洁性和可读性。
参考文档:
- es6入门
- 你不知道的JavaScript(中卷)