JavaScript异步编程
- Published on
- Authors
- Name
- 0neSe7en
- @0ne_Se7en
任何可以用 JavaScript 写成的应用最终都会用 JavaScript 写。
https://blog.codinghorror.com/the-principle-of-least-power/
为什么要用异步编程
同步的代码, 在很多情况下, CPU其实是在等待中度过的, 比如等待一个网络请求,等待数据库。比如在Python中,发起HTTP请求会阻塞住,知道返回结果才继续进行。
import requests
res = requests.get(‘https://youtube.com’)
# 在国内,可能要卡住4、5秒,才能运行下一步
print(res)
这里就有大量的CPU时间浪费掉了,因为我什么也没干,就是干等着请求数据了。所以,这个时间可以利用起来,我先执行下面的代码,等到这个网络IO完成了,我在反过头来执行callback,这就是所谓的异步了。
也就是为了充分利用CPU。对于有UI的程序,也是避免阻塞UI。
JavaScript的异步
一般认为JavaScript是可以利用事件模型处理异步触发任务的单线程语言。当要处理很多事件,同时要求数据的状态能够从 一个事件传递到下一个事件,那么就会写出如下代码:
step1(result1 => {
step2(result2 => {
step3(result3 => {
})
})
})
JavaScript中的每个异步函数都构建在其他某个或某些异步函数之上,凡是异步函数,从上到下(一直到原声代码)都是异步的。
举一个具体的需求: 登录教室(比实际的要更复杂一些) 来依次介绍各种异步的写法:
- Step1: 用户输入用户名密码,点击登录时,请求Web服务器校验用户名密码是否合法,函数名:
validUserPassword
,返回:{roomId: String}
- Step2: 如果合法,则根据返回的
roomId
请求教室信息,函数名:getRoomInfo(roomId)
,返回值:{roomId: String, userlist: ['user1', 'user2', 'user'3], roomType: enum(PLAYBACK, LIVE)}
。如果不合法,则抛出错误,提示用户。 - Step3: 如果请求成功,则根据
userlist
拉去每个用户的详细信息getUserInfo(userId)
。如果请求教室信息失败,则抛出错误,提示用户。 - Step4: 如果所有用户信息拉取成功,则根据
roomType
加载不同的教室。对于回放教室,调用loadPlayback(roomId)
。对于直播教室调用loadLivestatus(roomId)
。
其中每一步成功了才能进入下一步,失败了需要返回相应的错误信息。
每一个函数都是异步函数。最后一个参数都是回调函数。回调函数的第一个参数是err
,第二个参数是response
。函数名后面是Async
的,是返回Promise
的异步函数。
最好直接在 https://jsfiddle.net/0neSe7en/oxaouqz5/ 这里自己运行代码试试看。
1. Callback
function viaCallback(callback) {
validUserPassword('user', 'pass', (err, res) => {
if (err) {
return callback(new Error('用户名密码错误'));
}
insertText(res);
return getRoomInfo(res.roomId, (err, roomInfo) => {
if (err) {
return callback(new Error('获取教室信息失败'));
}
insertText(roomInfo);
let doneCount = 0;
let userInfoResults = [];
for (let i = 0; i < roomInfo.userlist.length; i++) {
const userId = roomInfo.userlist[i];
getUserInfo(userId, (err, userInfo) => {
if (err) {
return callback(new Error('获取用户信息失败'));
}
userInfoResults[i] = userInfo;
doneCount ++;
if (doneCount === roomInfo.userlist.length) {
insertText(userInfoResults);
if (roomInfo.roomType === 'PLAYBACK') {
loadPlayback(res.roomId, (err, res) => {
if (err) {
return callback(new Error('加载回放信息失败'));
} else {
insertText(res) ;
}
})
} else if (roomInfo.roomType === 'LIVE') {
loadLivestatus(res.roomId, (err, res) => {
if (err) {
return callback(new Error('加载直播信息失败'));
} else {
insertText(res) ;
}
})
}
}
})
}
})
})
}
可以看到,在不把逻辑抽出到函数中,就会一层层的嵌套。而处理一个数组的异步调用时,就会变得格外麻烦。即使优化之后,代码的可读性还是比较差。
function viaCallbackBetter(callback) {
function loadRoom(roomInfo) {
if (roomInfo.roomType === 'PLAYBACK') {
loadPlayback(roomInfo.roomId, callback)
} else if (roomInfo.roomType === 'LIVE') {
loadLivestatus(roomInfo.roomId, callback)
}
}
function handleRoomInfo(err, roomInfo) {
if (err) {
return callback(new Error('获取教室信息失败'));
}
insertText(roomInfo);
let doneCount = 0;
let userInfoResults = [];
for (let i = 0; i < roomInfo.userlist.length; i++) {
const userId = roomInfo.userlist[i];
getUserInfo(userId, (err, userInfo) => {
if (err) {
return callback(new Error('获取用户信息失败'));
}
userInfoResults[i] = userInfo;
doneCount++;
if (doneCount === roomInfo.userlist.length) {
insertText(userInfoResults);
loadRoom(roomInfo);
}
})
}
}
function handleLogin(err, res) {
if (err) {
return callback(new Error('用户名密码错误'));
}
insertText(res);
return getRoomInfo(res.roomId, handleRoomInfo)
}
validUserPassword('user', 'pass', handleLogin);
}
2. Async.js
这个现在不常用了,先略过,之后再补充。
3. Promise
function viaPromise() {
return validUserPasswordAsync('user', 'pass').then(res => {
insertText(res);
return getRoomInfoAsync(res.roomId).then(res => {
return res;
}).catch(() => Promise.reject('获取教室信息失败'))
}, () => {
return Promise.reject('用户名密码错误');
}).then(roomInfo => {
insertText(roomInfo);
return Promise.all(roomInfo.userlist.map(uid => getUserInfoAsync(uid))).then(userInfoResults => {
return { userInfoResults, roomInfo };
}).catch(() => Promise.reject('获取用户信息失败'))
}).then(({userInfoResults, roomInfo}) => {
insertText(userInfoResults);
if (roomInfo.roomType === 'PLAYBACK') {
return loadPlaybackAsync(roomInfo.roomId).catch(() => Promise.reject('加载回放失败'));
} else if (roomInfo.roomType === 'LIVE') {
return loadLivestatusAsync(roomInfo.roomId).catch(() => Promise.reject('加载直播失败'));
}
return Promise.reject('加载教室失败')
})
}
优化之后:
function viaPromiseBetter() {
function handleLogin(res) {
insertText(res);
return getRoomInfoAsync(res.roomId).catch(() => Promise.reject('获取教室信息失败'))
}
function handleRoomInfo(roomInfo) {
insertText(roomInfo);
return Promise.all(roomInfo.userlist.map(uid => getUserInfoAsync(uid)))
.then(userInfoResults => ({ userInfoResults, roomInfo }))
.catch(() => Promise.reject('获取用户信息失败'))
}
function loadRoom({userInfoResults, roomInfo}) {
insertText(userInfoResults);
if (roomInfo.roomType === 'PLAYBACK') {
return loadPlaybackAsync(roomInfo.roomId).catch(() => Promise.reject('加载回放失败'));
} else if (roomInfo.roomType === 'LIVE') {
return loadLivestatusAsync(roomInfo.roomId).catch(() => Promise.reject('加载直播失败'));
}
return Promise.reject('加载教室失败')
}
return validUserPasswordAsync('user', 'pass')
.then(handleLogin, () => Promise.reject('用户名密码错误'))
.then(handleRoomInfo)
.then(loadRoom);
}
4. async, await
async function viaAsync() {
let roomId, roomInfo;
try {
const res = await validUserPasswordAsync('user', 'pass');
roomId = res.roomId;
insertText(res);
} catch (e) {
throw new Error('用户名密码错误');
}
try {
roomInfo = await getRoomInfoAsync(roomId);
insertText(roomInfo);
} catch (e) {
throw new Error('获取教室信息失败');
}
try {
const userInfoResults = await Promise.all(roomInfo.userlist.map(uid => getUserInfoAsync(uid)));
insertText(userInfoResults);
} catch (e) {
throw new Error('获取用户信息失败');
}
if (roomInfo.roomType === 'PLAYBACK') {
try {
return await loadPlaybackAsync(roomInfo.roomId)
} catch (e) {
throw new Error('加载回放失败');
}
} else if (roomInfo.roomType === 'LIVE') {
try {
return await loadLivestatusAsync(roomInfo.roomId)
} catch (e) {
throw new Error('加载直播失败');
}
} else {
throw new Error('加载教室失败');
}
}
5. worker
这个之后补充
为什么要用Promise
以前的用法:
$.get('/getSomething', { success: onSuccess, failure: onFailure, always: onAlways});
// 采用Promise。注意:这是jQuery最早实现的Promise,和规范的接口不一样。但是Promise是由于jQuery1.5带火的。
const promise = $.get('/getSomething');
promise.done(onSuccess);
promise.fail('onFailure);
promise.always(onAlways);
乍一看这种变化没什么。为什么一定要在触发Ajax调用之后再附加回调呢?!!封装!!。如果Ajax调用需要实现很多效果(触发动画、插入HTML、锁定/解锁用户输入等等),那么仅由负责发出请求的那部分应用代码处理所有这些效果,就显得很蠢很拙劣。
只传递Promise 对象就会优雅得多。传递Promise 对象就相当于声明:“你感兴趣的某某事就要发生了。想知道什么时候完事吗?给这个Promise 对象一个回调就行啦!”Promise 对象也和EventEmitter 对象一样,允许向同一个事件绑定任意多的处理器(堆积技术)。对于多个Ajax 调用分享某个功能小片段(譬如“正加载”动画)的情况,堆积技术也会使降低代码重复度容易很多。
不过使用Promise 对象的最大优势仍然在于,它可以轻松从现有Promise 对象派生出新的Promise 对象。我们可以要求代表着并行任务的两个Promise 对象合并成一个Promise 对象,由后者负责通知前面那些任务都已完成。也可以要求代表着任务系列中首任务的Promise 对象派生出一个能代表任务系列中末任务的Promise 对象,这样后者就能知道这一系列任务是否均已完成。
Promise的坑
我自己是在2015年初准备学习Node.js的,那个时侯对 JavaScript 的异步、回调函数,都不是很理解。在使用Promise的过程中,各种常见的坑基本都踩过了。然后看到了这篇文章,基本上解释了我遇到的所有的问题... ,作为 Promise
常见的坑。
作者是PouchDB( https://github.com/pouchdb/pouchdb )的创始人和维护者,译者是FEX团队成员( http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/ )。文章写于2015年5月份,我也在差不多的时间,大量使用Promise。
参考:
- JavaScript Promise迷你书:http://liubin.org/promises-book/#introduction
- Nodeschool关于Promise的交互式教程:https://nodeschool.io/zh-cn/#promise-it-wont-hurt
- 使用Promise的问题:https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html