callback
在 JavaScript 中函数是第一公民,可以做为参数传入函数中执行。所以我们可以把需要异步执行的代码放到回调函数中,然后在异步回调再执行这些代码。
Example
如下定义 delay
函数, callback
参数的类型是一个函数, 该函数会在1秒钟后执行。
function delay(callback) {
console.log('foo');
setTimeout(callback, 1000);
}
delay(console.log.bind(console, 'bar'));
// 控制台立即打印 foo
// 1秒钟后控制台输出 bar
回调函数处理单个异步操作看起来问题不大,但想像这样几个场景。
- 场景一:假设 delayA, delayB, delayC 都是异步操作, 传入的
callback
参数在完成操作后执行. 操作 done 依赖 delayC 操作的结果, delayC 操作又依赖 delayB, delayB 又依赖 delayA,我们怎么解决?:
function delayA(callback) {
setTimeout(() => {
callback(1);
}, 1000);
}
function delayB(fn, somthingFromA) {
setTimeout(() => {
callback(somthingFromA + 1);
}, 2000);
}
function delayC(fn, somthingFromB) {
setTimeout(() => {
callback(somthingFromB + 1);
}, 3000);
}
function done(result) {
console.log(result);
}
delayA((feedBackByA) => {
delayB((feedBackByB) => {
delayC((feedBackByC) => {
done(feedBackByC);
}, feedBackByB);
}, feedBackByA);
});
// 6 秒后控制台输出 3
- 场景二:操作 done 依赖 delayA, delayB, delayC 操作的结果, 但 delayA, delayB, delayC 之间没有依赖关系,他们定义如下:
function delayA(callback) {
setTimeout(() => {
callback(1);
}, 1000);
}
function delayB(fn) {
setTimeout(() => {
callback(2);
}, 2000);
}
function delayC(fn) {
setTimeout(() => {
callback(3);
}, 3000);
}
function done(somthingFromA, somthingFromB, somthingFromC) {
console.log(somthingFromA, somthingFromB, somthingFromC);
}
当然我们可以用场景一同样的解决方案,但多个不相关异步操作同步执行延迟了 done 操作的执行时机。
delayA((feedBackByA) => {
delayB((feedBackByB) => {
delayC((feedBackByC) => {
done(feedBackByA, feedBackByB, feedBackByC);
});
});
});
// 6 秒后控制台输出 1, 2, 3
因此需要定义一个辅助函数,这个辅助函数使得多个异步操作同时开始执行,并在最晚回调的异步操作结束时执行回调函数:
/*
* fns: 异步操作函数的数组
* callback: 所有异步操作都完成时的回调
*/
var helper = (fns, callback) => {
// 定义一个 checkList,标识每个异步操作是否完成
const checkList = new Array(fns.length).fill(false);
// 保存每个异步操作返回的参数供 callback 使用
const parameters = new Array(fns.length);
// 判断是否所有的异步操作都已完成
function allCheck() {
return checkList.reduce((prev, val) => {
return prev && val;
}, true);
}
fns.forEach((fn, index) => {
fns((feedBack) => {
parameters[index] = feedBack;
checkList[index] = true;
if (allCheck()) {
callback.apply(null, parameters);
}
});
});
};
helper([delayA, delayB, delayC], done);
// 3 秒后控制台输出 1, 2, 3
再想象场景一和场景二的需求结合起来,代码将会变得嵌套过深不易阅读。综上,过度使用回调函数就会产生传说中的回调地狱,随着异步操作的依赖关系变得复杂,代码变得丑陋而不可维护。
Promise
在 ES6 中引入了 Promise,经常被用来处理异步,我们来看看它是怎么简化我们处理异步的过程的。
对 Promise 不了解的同学可以先看看 developer.mozilla.org/en-US/docs/…
首先我们看看把之前的一个由异步函数转化为一个 Promise:
funcion promisify(fn) {
return (args) =>
return new Promise((resolve, reject) => {
// fn 是一个异步操作,第一参数是回调函数,第二个参数是执行的参数
fn(resolve, args);
});
};
}
那么原来一个使用 callback 作为参数的异步操作如下:
function delay(callback, content) {
setTimeout(() => callback(content), 1000);
}
就可以被 Promise 化:
const delayPromise = promisify(delay);
delayPromise('foo').then((content) => {
console.log(content);
});
// 1 秒后控制台输出 foo
看起来虽然和回调函数没有什么区别,但我们再看看上面的场景一,使用 Promise 我们的解决方案会发生什么变化。
promisify(delayA)()
.then((feedbackFromA) => {
return promisify(delayB)(feedbackFromA);
})
.then((feedbackFromB) => {
return promisify(delayC)(feedbackFromB);
})
.then((feedbackFromC) => {
done(feedbackFromC);
});
// 6 秒后控制台输出 3
再看场景二:
Promise.all([
promisify(delayA)(),
promisify(delayB)(),
promisify(delayC)()
]).then(([feedbackFromA, feedbackFromB, feedbackFromC]) => {
done(feedbackFromA, feedbackFromB, feedbackFromC);
});
// 3 秒后控制台打印 1,2,3
一旦所有异步操作被封装为 Promise 对象,那么显然不会再有回调地狱的问题了。
Generator
Generator
也是 ES6 中引入的概念。对于 Generator
不了解的同学请先查看文档 developer.mozilla.org/en-US/docs/…
首先我们看看一个 Generator
是如何工作的:
function* main() {
const a = yield 1; // a 为第二次调用 next 时传入的参数
const b = yield 2 + a; // b 为第三次调用 next 时传入的参数
yield 3 + b;
return 4;
}
const gen = main();
gen.next(); // {value: 1, done: false}
gen.next(1); // {value: 3, done: false}
gen.next(5); // {value: 8, done: false}
gen.next(); // {value: 4, done: true}
Generator
又是怎么解决异步的呢?想象我们有一个辅助函数 helper
函数, 可以让下面的 generator
自动执行完毕, 那么以后写异步代码就可以和写同步代码一样方便了。
helper(function* main(args) {
const feedBackFromA = yield promisify(delayA)(args);
const feedBackFromB = yield promisify(delayB)(feedBackFromA);
const feedBackFromC = yield promisify(delayC)(feedBackFromB);
done(feedBackFromC);
})(realArgs);
这个 helper
函数应满足一下几个条件:
- 自动执行
generator
直到generator
的状态为完成 yield
关键字后面的Promise
resolve 的值被返回作为下次调用 next 的参数- 返回 Promise,使用
generator
实例的返回值作为 resolve 的值
因此,定义 helper 如下:
function helper(genFn) {
return (...args) => new Promise(resolve, reject) => {
let gen = genFn(args);
function next(prev) => {
const {
value,
done
} = gen.next(prev);
if(done) {
return resolve(next.value);
} else {
return value.then(next);
}
}
next();
});
}
那么
helper(main)();
// 6 秒后控制台打印 3
那么场景二的问题可以可以利用这个 helper 这么解决:
helper(function* main() {
const [feedBackFromA, feedBackFromB, feedBackFromC] = yield Promise.all([
promisify(delayA)();
promisify(delayB)();
promisify(delayC)();
]);
d(feedBackFromA, feedBackFromB, feedBackFromC);
})();
// 3 秒后控制台打印 1,2,3
感兴趣的人可以自己加工这个 helper
,使得它支持下面这样的语法:
helper(function* main() {
const [feedBackFromA, feedBackFromB, feedBackFromC] = yield [
promisify(delayA)();
promisify(delayB)();
promisify(delayC)();
];
d(feedBackFromA, feedBackFromB, feedBackFromC);
})();
这里推荐一个好用的三方库 co
, 它提供了一个很强大的 helper
实现, 他的 yield
后面可以接受 Array,Promise,Function 等众多类型。
Async Function
ES7 中加入了 async function 的原生支持。
所以写法和上面用 generator 的解决方案很相似,但 await 关键词后面只接受 Promise 对象。
async function 的解决方案:
场景一:
async function main() {
const feedBackFromA = await promisify(delayA)();
const feedBackFromB = await promisify(delayB)(feedBackFromA);
const feedBackFromC = await promisify(delayC)(feedBackFromB);
d(feedBackFromC);
}
场景二:
async function main() {
const feedBacks = await Promise.all([
promisify(delayA)(/* a的参数 */);
promisify(delayB)(/* b的参数 */);
promisify(delayC)(/* c的参数 */);
]);
d(feedBacks);
}
错误处理
callback
在各自的回调中处理错误。
a(() => {
if (err) {
// handle the error of a
}
b(() => {
if (err) {
// handle the error of b
}
...
});
});
Promise
使用 catch 方法获取在整个 Promise 过程的错误统一处理,也可以在 then 方法的第二个参数中各自处理。
promisify(delayA)()
.then(promisify(delayB), (err) => {
// handle the error of a
})
.catch((err) => {
// handle all errors
});
Generator 和 Async Function
写法已经接近同步,可直接使用 try catch 来处理错误。
async function main() {
try {
const feedBackFromA = await promisify(delayA)(/* a的参数 */);
const feedBackFromB = await promisify(delayB)(aRes);
const feedBackFromC = await promisify(delayC)(feedBackFromB);
d(feedBackFromC);
} catch (err) {
// handle all errors
}
}
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。