回调地狱
一个段子
以前有个段子讲一个小偷,潜入某神秘机构,偷出代码最后一页,打开一看:
});
});
});
});
});
});
});
});
});
});
});
什么?这只是段子不是现实?那看看现实版快滴打车的源代码:
因为Javascript的异步特性,每个开发者都无法避免会碰到一些callback hell,同时在代码的迭代过程当中因为这样一些callback hell导致代码越来越不可维护。尤其是当回调过程中去参杂一些同步逻辑判断,那都是迭代过程中的代码杀手。
一个例子
产品:我们需要从服务器取数据,然后再xxx
开发:搞定
function myFunc() {
getServerData(function () {
// Do something
});
}
产品:我们要插个小功能,取另一分数据,然后再xxx
开发:ok
function myFunc() {
getServerData(function (d1) {
getServerData2(function (d2) {
// Do something
});
});
}
产品:需要在取第二份数据前加个小判断,部分用户不需要取第二份数据。
开发:改起来会有点麻烦。
产品:不就加个条件判断么?怎么会麻烦。
开发:...
function myFunc() {
getServerData(function (d1) {
var doSomething = function () {
// Do something
}
if (condition) {
getServerData2(function (d2) {
doSomething();
});
} else {
doSomething();
}
});
}
产品:再帮我加一个很小的功能。
开发:...
解套平坑
解决方案
其实JavaScript 一直在避免回调地狱的问题做出努力,比如async.js
,then.js
包括ES6下的Promise
,co generator
等等。这里不去细讲,想进一步了解这些解决方案的差异的话可以看尤雨溪大神在直呼上的回答:
nodejs异步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何优缺点?最爱哪个?哪个简单?
Async Functions
这里要讲的是一种更平滑更接近同步体验的一种方案Async Functions
。
async/await
语法最早是在C#5.0
中引入,引入后引起了一致好评,因此使用异步编程最多的JavaScript
迫不及待的向ES2016(ES7)提交了草案,但因为某些原因,呼声很高的Async Functions
并没能赶上ES2016的deadline,估计最晚会在ES2017中加入到正式规范,但是并不妨碍我们在Babel
的帮助下在ES5的环境下使用它。
先看看在使用Async Functions
的情况下,上面产品需求的代码开发将会怎么实现:
async function myFunc () {
let d1, d2;
d1 = await getServerData();
if (condition) {
d2 = await getServerData2();
}
// Do something
}
加入了神奇的async
和await
关键字后,上面的异步回调完全以同步的方式展现,也不用去担心产品需要再在某个回调中插入流程了而且导致代码结构大面积改动了。
Babel实现方式
babel
编译Async Functions
需要transform-async-to-generator
插件,参考官方文档安装。
基于ES6
写上测试代码src/index.js
:
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.random() * 10 >> 0);
}, time * 1000);
});
}
async function main () {
console.log('Random is: ');
let d = await sleep(2);
console.log(d);
return d;
}
let rst = main();
babel配置文件.babelrc
如下:
{
"plugins": ["transform-async-to-generator"]
}
执行命令编译:
babel src -d dist
编译后主要代码如下:
let main = (() => {
var _ref = _asyncToGenerator(function* () {
console.log('Random is: ');
let d = yield sleep(2);
console.log(d);
return d;
});
return function main() {
return _ref.apply(this, arguments);
};
})();
编译后的代码和co
很相似,可以理解为基于ES6的Promise
和Generator
的语法糖。
基于ES5
因此要进一步运行在浏览器环境下我们还需要使用ES2015 presets
和transform-runtime
插件。
babel配置文件.babelrc
如下:
{
"presets": ["es2015"],
"plugins": ["transform-async-to-generator", "transform-runtime"]
}
编译后关键代码如下:
var main = function () {
var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee() {
var d;
return _regenerator2.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log('Random is: ');
_context.next = 3;
return sleep(2);
case 3:
d = _context.sent;
console.log(d);
return _context.abrupt('return', d);
case 6:
case 'end':
return _context.stop();
}
}
}, _callee, this);
}));
return function main() {
return _ref.apply(this, arguments);
};
}();
可以看到编译后代码是由状态机实现,并无任何ES5下不兼容代码。
使用与实践
错误捕捉
在使用Promise
时,我们有resolve
和reject
,如下:
function doSth() {
promise().then(d => console.log(d)).cache(e => console.error(e));
}
在Async Functions
中写法如下:
async doSth() {
try {
let d = await promise();
console.log(d);
} catch (e) {
console.error(e);
}
}
在async
是使用throw
相当于Promise
中的reject
:
async function hello() {
await sleep(5);
throw Error('err');
}
let promise = hello();
promise.then(d => console.log(d)).catch(e => console.log(e));
// 输出err
返回值
在Async
函数中, 返回值永远是Promise
async function hello() {
await sleep(5);
return 'world';
}
let promise = hello();
promise.then(d => console.log(d));
// 输出world
循环中使用async
因为同步非阻塞的表现,所以在循环中使用async
将会比以前的代码更易读明了。
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
result = await reader.read();
}
return total;
}
匿名函数中使用
同样在匿名函数中可以一样去使用async
关键字,如下:
const promises = urls.map(async url => {
const p = await fetch(url);
return p;
});
await
连续使用问题
代码一:
async function foo() {
await sleep(3);
await sleep(3);
return 'done';
}
运行完需要6秒。
代码二:
async function bar() {
const s1 = sleep(3);
const s2 = sleep(3);
await s1;
await s2;
return 'done';
}
代码二运行完却只要3秒,因为sleep是在同一时间运行的。
结束语:async/await
无疑是现阶段最好的异步回调同步化的解决方案,不过因为暂时没有纳入ES2016规范,而且主流浏览器的支持的不足,所以我们只能通过使用babel
尝鲜。但是我们也可以借此看到未来JavaScript
在回调问题上的主流解决方案。