一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async
JS异步编程实践理解
回顾JS异步编程方法的发展,主要有以下几种方式:
- Callback
- Promise
- Generator
- Async
需求
显示购物车商品列表的页面,用户可以勾选想要删除商品(单选或多选),点击确认删除按钮后,将已勾选的商品清除购物车,页面显示剩余商品。
为了便于本文内容阐述,假设后端没有提供一个批量删除商品的接口,所以对用户选择的商品列表,需要逐个调用删除接口。
用一个定时器代表一次接口请求。那思路就是遍历存放用户已选择商品的id数组,逐个发起删除请求del,待全部删除完成后,调用获取购物车商品列表的接口get
实现
let ids = [1, 2, 3] // 假设已选择三个商品
let len = ids.length
let count = 0
let start // 便于后面计算执行时间
1. callback
传统常规的写法,如果是多个继行任务就会陷入回调地狱。比如此例中get作为del的回调函数
let get = () => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
}, 1000)
}
let del = (id, cb) => {
setTimeout(() => {
console.log(id)
count++
if (count === len) {
cb()
}
}, 1000)
}
let confirmDel = () => {
start = new Date()
for (id of ids) {
del(id, get)
}
console.log(`done:${new Date() -start}ms`)
}
confirmDel()
注意观察和对比done的打印顺序和get完成时间。
setTimeout是异步执行的,没有阻塞主流程的执行,所以done最先打印。
三个del任务是并行的,加上一个回调执行时间,所以整个点击删除按钮事件耗时2秒左右
done:1ms
1
2
3
get:2007ms
2. Promise
let getP = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
resolve()
}, 1000)
})
}
let delP = (id, cb) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(id)
count++
if (count === len) {
cb()
}
resolve()
}, 1000)
})
}
let confirmDelP = () => {
start = new Date()
for (id of ids) {
delP(id, getP)
}
console.log(`done:${new Date() -start}ms`)
}
confirmDelP()
单纯常用Promise写法,看上去结构跟回调写法一样,而且运行时间也一样。
done:2ms
1
2
3
get:2007ms
但是,如果使用Promise.all方法,就能很好将并发任务(三个del)和继发任务(get)区分开了,就是get不用嵌入回调中了。
3. Promise.all
Promise对象then / catch / all / race / finally,以及resolve / reject更多内容请参阅MDN
let delP_1 = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(id)
resolve()
}, 1000)
})
}
let getP_1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
resolve()
}, 1000)
})
}
let confirmDelP_all = () => {
start = new Date()
let p_Arr = ids.map(id => delP_1(id))
Promise.all(p_Arr)
.then(() => {
return getP_1()
})
.then(() => {
console.log(`done:${new Date() -start}ms`)
})
}
confirmDelP_all()
在这里,代码的语义就很直观了,先并发三个删除del,全部成功后执行get,get成功后done。
注意看done的打印顺序
1
2
3
get:2008ms
done:2010ms
4. Generator
Generator类型是一种特殊的函数,它拥有自己独特的语法和方法属性。比如函数名前加*,配合yield 返回异步回调结果, 通过next 传入函数、next返回特殊的包含value和done属性的对象等等,具体见MDN
Generator是一种惰性求值函数,执行一次next()才开启一次执行,到yield又中断,等待下一次next()。所以本人更喜欢叫它步进函数,非常适合执行继发任务
假设现在每一个接口请求都是继发任务,就是说只有当上一个请求成功后,才开始下一个请求。在实际的场景中,通常是当前请求需要使用上一个请求返回的结果数据。此时使用Generator函数是最好的方式。
let generator
let getG = () => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
generator.next()
}, 1000)
}
let delG = (id) => {
setTimeout(() => {
console.log(id)
generator.next()
}, 1000)
}
function *confimrDelG () {
start = new Date()
for (id of ids) {
yield delG(id)
}
yield getG()
console.log(`done:${new Date() -start}ms`)
}
generator = confimrDelG()
generator.next()
console.log('会被阻塞吗?')
观察打印的时间,四个异步任务4秒左右。
注意"阻塞“文字最先打印
会被阻塞吗?
1
2
3
get:4009ms
done:4011ms
我理解Generator就是一个用来装载异步继发任务的容器,不阻塞容器外部流程,但是容器内部任务用yield设置断点,用next步进执行,可以通过next向下一步任务传值,或者直接使用yield返回的上一任务结果。
5. async / await
async 函数
我们先看MDN上关于async function怎么说的:
When an async function is called, it returns a Promise. When the async function returns a value, the Promise will be resolved with the returned value. When the async function throws an exception or some value, the Promise will be rejected with the thrown value.
也就是说async函数会返回一个Promise对象。
- 如果async函数中是return一个值,这个值就是Promise对象中resolve的值;
- 如果async函数中是throw一个值,这个值就是Promise对象中reject的值。
例子显示下,我们先用Promise写法
function imPromise(num) {
return new Promise(function (resolve, reject) {
if (num > 0) {
resolve(num);
} else {
reject(num);
}
})
}
imPromise(1).then(function (v) {
console.log(v); // 1
})
imPromise(0).catch(function (v) {
console.log(v); // 0
})
再用Async写法
async function imAsync(num) {
if (num > 0) {
return num // 这里相当于resolve(num)
} else {
throw num // 这里相当于reject(num)
}
}
imAsync(1).then(function (v) {
console.log(v); // 1
});
// 注意这里是catch
imAsync(0).catch(function (v) {
console.log(v); // 0
})
所以理解Async为new Promise的语法糖也是这个原因。但要注意一点的是上面imPromise函数和imAsync函数调用返回的结果区别。
`new Promise`生成的是一个`pending`状态的`Promise`对象,而`async`返回的是一个`resolved`或`rejected`状态的`Promise`对象,就是一个已经终结状态的`promise`对象。理解这点,对下面的`await`理解很重要。
let p = imPromise(1)
console.log(p) // Promise { pending }
let a = imAsync(1)
console.log(a) // Promise { resolved }
await
再来看看MDN对于await是怎么说的:
An async function can contain an await expression, that pauses the execution of the async function and watis for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value.
await会暂停当前async函数的执行,等待后面的Promise的计算结果返回以后再继续执行当前的async函数
- await 等待什么??
await等待一个Promise对象从pending状态到resoled或rejected状态的这段时间。
所以如果要实现中断步进执行的效果,await后面接的必须是一个pedding状态的promise对象,其它状态的promise对象或非promise对象一概不等待。
这也是await和yield的区别(yield不管后面是什么,执行完紧接着的表达式就中断)。
async / await 解决了什么问题
Promise解决callback嵌套导致回调地狱的问题,但实际上并不彻底,还是在then中使用了回调函数。而async / await使得异步回调在写法上完成没有,就像同步写法一样。
看个例子:
// callback
get((a) => {
(a,b) => {
(b,c) => {
(c,d) => {
(d,e) => {
console.log(e)
}
}
}
}
})
// promise
get()
.then(a => p1(a))
.then(b => p1(b))
.then(c => p1(c))
.then(d => p1(d))
.then(e => {console.log(e)})
// async / await
(async (a) => {
const b = await A(a);
const c = await A(b);
const d = await A(c);
const e = await A(d);
console.log(e)
})()
async / await 实现继发任务
我们用async / await改写上面Generator的例子
let delP_1 = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(id)
resolve()
}, 1000)
})
}
let getP_1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
resolve()
}, 1000)
})
}
async function confimrDelAsync () {
start = new Date()
for (id of ids) {
await delP_1(id)
}
await getP_1()
console.log(`done:${new Date() -start}ms`)
}
confimrDelAsync()
console.log('被阻塞了吗?')
打印结果基本跟generator一样。但在语义上更明确。
被阻塞了吗?
1
2
3
get:4014ms
done:4016ms
async / await 实现并发任务
let delP_1 = (id) => {
setTimeout(() => {
console.log(id)
}, 1000)
}
let getP_1 = () => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
}, 1000)
}
async function confimrDelAsync () {
start = new Date()
for (id of ids) {
await delP_1(id)
}
await getP_1()
console.log(`done:${new Date() -start}ms`)
}
confimrDelAsync()
console.log('被阻塞了吗?')
不返回Promise对象,或者使promise对象处理resoled状态,就可以不执行等待。但这样的写法跟直接用同步方式写一样,所以并不推荐,显得多此一举。
done:4ms
1
2
3
get:1009ms
async / await 实现并发和继发的混合任务
如果事件函数中并发任务和继发任务都有,此时使用async / await才是最好的解决方式。其中的并发任务用promise.all实现,因为它返回的正是await可用的pending状态的Promise对象。
let delP_1 = (id) => {
setTimeout(() => {
console.log(id)
resolve()
}, 1000)
}
let getP_1 = () => {
setTimeout(() => {
console.log(`get:${new Date() -start}ms`)
resolve()
}, 1000)
}
async function confimrDelAsync_all () {
start = new Date()
let p_Arr = ids.map(id => delP_1(id))
await Promise.all(p_Arr)
await getP_1()
console.log(`done:${new Date() -start}ms`)
}
confimrDelAsync_all()
console.log('被阻塞了吗?')
观察时间是继发任务的一半。且不阻塞主流程。
被阻塞了吗?
1
2
3
get:2009ms
done:2010ms
所以说async是promise的语法糖,但是函数返回的promise的状态是不一样的。说await是yield的语法糖,但是await只能接受pending状态的promise对象
async可以单独使用,await不能单独使用,只能在async函数体内使用
所以针对开头的需求:
显示购物车商品列表的页面,用户可以勾选想要删除商品(单选或多选),点击确认删除按钮后,将已勾选的商品清除购物车,页面显示剩余商品。
最好的解决方案是:
`promise.all` 与 `async / await`结合
其次是:
`promise.all`
在实际项目中还应该加上捕获错误的代码。
在async / await中结合try...catch
在promise中,因为错误具有冒泡以性质,所以在结尾加上.catch即可。
尾声
文章只是自己的一个并发和继发混合需求引发的知识总结。但JS编程还有很多内容,包括异步事件、事件循环(浏览器和nodejs区别)、异步任务错误的捕获、promise/generator/async具体API细节等。还需要继续学习。
参考链接
https://blog.csdn.net/ken_ding/article/details/81201248
https://segmentfault.com/a/1190000009070711?from=timeline&isappinstalled=0#articleHeader5
《Javascript ES6 函数式编程入门指南》 第10章 使用Generator
一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async的更多相关文章
- JS异步编程 (2) - Promise、Generator、async/await
JS异步编程 (2) - Promise.Generator.async/await 上篇文章我们讲了下JS异步编程的相关知识,比如什么是异步,为什么要使用异步编程以及在浏览器中JS如何实现异步的.最 ...
- js中异步方案比较完整版(callback,promise,generator,async)
JS 异步已经告一段落了,这里来一波小总结 1. 回调函数(callback) setTimeout(() => { // callback 函数体 }, 1000) 缺点:回调地狱,不能用 t ...
- js异步编程终级解决方案 async/await
在最新的ES7(ES2017)中提出的前端异步特性:async.await. async.await是什么 async顾名思义是“异步”的意思,async用于声明一个函数是异步的.而await从字 ...
- 深入理解JS异步编程三(promise)
jQuery 原本写一个小动画我们可能是这样的 $('.animateEle').animate({ opacity:'.5' }, 4000,function(){ $('.animateEle2' ...
- 理解js异步编程
Promise 背景 javascript语言的一大特点就是单线程,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码,也就是说,同一个时间只能做一件事. 怎么做到异步编程?回调函数.直到no ...
- JS魔法堂:深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- js异步编程
前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的, ...
- 深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- 深入理解node.js异步编程:基础篇
###[本文是基础内容,大神请绕道,才疏学浅,难免纰漏,请各位轻喷] ##1. 概述 目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平 ...
随机推荐
- eclipse下Android工程名称的修改方法
eclipse下Android工程名称的修改方法 对于已经建立的工程,如果发现原来的工程名不合适,此时若想彻底更改工程名,需要三个步骤: 1.更改工程名 选中工程名,右键-->Refactor- ...
- 优化梯度计算的改进的HS光流算法
前言 在经典HS光流算法中,图像中两点间的灰度变化被假定为线性的,但实际上灰度变化是非线性的.本文详细分析了灰度估计不准确造成的偏差并提出了一种改进HS光流算法,这种算法可以得到较好的计算结果,并能明 ...
- LeetCode: Binary Tree Postorder Traversal [145]
[题目] Given a binary tree, return the postorder traversal of its nodes' values. For example: Given bi ...
- c# 编程修改 wince 系统时间
[StructLayout(LayoutKind.Sequential)] public struct SYSTEMTIME { public ushort wYear; public ushort ...
- three supported reliability levels: * End-to-end * Store on failure * Best effort
https://github.com/cloudera/flume/blob/master/flume-docs/src/docs/UserGuide/Introduction === Reliabi ...
- unity导出android项目
1. 2 . 3 选择Google Android Project(若不选则直接导出Apk) Export,Android项目即可导出成功.
- VVDocument+Appledoc生成文档
在写代码的时候写上适当的注释是一种良好的习惯,方便自己或者别人阅读的方便. **VVDocument**:(Github地址:[VVDocument](https://github.com/onevc ...
- wifi方式调试android程序
1. 通过wifi, 利用adb来连接手机. 在pc的cmd中输入命令: adb connect 192.168.1.100 其中adb就是手机的ip. 如果连接成功, 就可以进入android的sh ...
- git删除某次提交(某个commit)的方法【转】
本文转载自:https://www.36nu.com/post/275 git删除某次提交(某个commit)的方法 疯狂的兔子 发表于 4个月前 阅读 536 收藏 0 推荐 0 评论 0 推荐收藏 ...
- bzoj4485: [Jsoi2015]圈地
思维僵化选手在线被虐 其实应该是不难的,题目明显分成两个集合,要求是不同集合的点不能联通 先假设全选了,然后二分图最小割,相邻两个点直接连墙的费用就可以了 #include<cstdio> ...