关于
微信公众号: 前端呼啦圈 (Love-FED)
我的博客: 劳卜的博客 http://www.cnblogs.com/luozhihao
知乎专栏: 前端呼啦圈 https://zhuanlan.zhihu.com/font-end
前言
在实际编码中, 我们经常会遇到 Javascript 代码异步执行的场景, 比如 ajax 的调用, 定时器的使用等, 在这样的场景下也经常会出现这样那样匪夷所思的 bug 或者糟糕的代码片段, 那么处理好你的 Javascript 异步代码成为了异步编程至关重要的前提. 下面我们从问题出发, 一步步完善你的异步代码.
异步问题
1. 回调地狱
首先, 我们来看下异步编程中最常见的一种问题, 便是回调地狱. 它的出现是由于异步代码执行时间的不确定性及代码间的依赖关系引发的, 比如:
- // 一个动画结束后, 执行下一个动画, 下一个动画结束后再执行下一个动画
- $('#box').animate({width: '100px'}, 1000, function(){
- $('#box').animate({height: '100px'}, 1000, function(){
- $('#box').animate({left: 100}, 1000);
- });
- });
由于我们不知道第一个动画什么时候开始或者什么时候结束, 所以我们把第二个动画的执行内容放到了第一个动画的结束事件里, 把第三个动画放到了第二个动画的结束事件里, 这时候如果有很多这样的动画, 那么就会形成回调地狱.
2. 捕获异常
除了回调地狱, 如果我们需要在异步代码中捕获异常也比较麻烦, 可能需要手动配置捕获方法:
- try {
- throw new Error('fail');
- } catch (e) {
- console.log(e);
- }
这样的代码书写明显不是我们想要的, 不仅不利于维护, 而且也在一定程度上违背了良好的 Javascript 编码规范.
解决方案
那么我们如何优雅的写好我们的异步代码呢? 我主要列了以下 5 种常见方案:
1. callback
callback 顾名思义便是回调, 但并不是将回调内容放在异步方法里, 而是放到外部的回调函数中, 比如问题 1 的代码我们通过 callback 可以变成这样:
- $('#box').animate({width: '100px'}, 1000, autoHeight);
- function autoHeight() {
- $('#box').animate({height: '100px'}, 1000, autoLeft);
- }
- function autoLeft() {
- $('#box').animate({left: 100}, 1000);
- }
如此我们看似异步的代码变成了同步的写法, 避免了层层嵌套的写法, 看上去也流畅了很多. 同时使用 callback 也是异步编程最基础和核心的一种解决思路.
2. Promise
基于 callback,Promise 目前也被广泛运用, 其是异步编程的一种解决方案, 比传统的回调函数解决方案更合理和强大. 相信了解 ES6 的同学肯定不会陌生.
比如我们现在有这样一个场景, 我们需要异步加载一张图片, 在图片加载成功后做一些操作, 这里我不想用回调函数或者将逻辑写在图片的成功事件里, 那么用 Promise 我们可以这样写:
- let p = new Promise((resolve, reject) => {
- let img = new Image(); // 创建图片对象
- // 图片加载成功事件
- img.onload = function() {
- resolve(img); // 输出图片对象
- };
- // 图片加载失败事件
- img.onerror = function() {
- reject(new Error('load error')); // 输出错误
- };
- img.src = 'xxx'; // 图片路径
- });
- // Promise then 回调
- p
- .then(result => {
- $('#box').append(result); // 成功后我们把图片放到页面上
- })
- .catch(error => {
- console.log(error); // 打印错误
- })
通过 Promise 我们把图片构建加载的逻辑和成功或失败后的处理逻辑拆分了开来, 将回调函数的嵌套, 改成链式调用, 同时使用 Promise 的 catch 事件回调后异常捕获也变得十分方便.
当然如果要等待多个异步请求完成执行某些操作, 可以使用 Promise.all 方法, 如:
- let p = Promise.all([p1, p2, p3]); // 其中 p1,p2,p3 都是 Promise 实例
- p.then(result => console.log(result));
当然 Promise 也有其相应的缺点, 比如下一个 then 回调只能获取上一个 then 返回的数据, 不能跨层获取, 同时大量的 then 回调也会使代码不容易维护.
3. Generator
与 Promise 一样, Generator 函数也是 ES6 提供的一种异步编程解决方案, 其会返回一个遍历器对象, 异步任务需要暂停的地方我们可以使用 yield 语句, 比如:
- function* getData() {
- let result = yield fetch("xxx"); // 调用 ajax,yield 命令后面只能是 Thunk 函数或 Promise 对象
- console.log(result);
- }
- // 执行
- let g = getData();
- let result = g.next(); // { value: [object Promise], done: false }
- result.value.then(data => {
- return data.json();
- }).then(data => {
- g.next(data);
- });
Generator 中遇到 yield 的地方会进行暂停, 所以我们需要手动调用 next 方法往下, next 返回值的 value 属性便是我们需要的数据, 这里是 fetch 方法返回的 Promise 对象, 所以我们需要使用 then 回调, 最后再调用 g.next(data) 结束并输出数据.
Generator 函数的缺点在于, 我们每一次执行 yield 语句都需要手动进行 next, 不是很方便.
4. co
为了解决上方 Generator 函数需手动执行 next 方法的问题, TJ Holowaychuk 大神编写了一个 co 函数库, 能够使 Generator 函数可以自动执行, 比如原来我们需要这样:
- let files = function* (){
- var f1 = yield readFile('/xxx/xxx'); // 读取 file1 文件
- var f2 = yield readFile('/xxx/xxx'); // 读取 file2 文件
- console.log(f1.toString());
- console.log(f2.toString());
- };
- files.next(); // 执行 yield
- files.next(); // 执行 yield
使用 co 后:
- var co = require('co');
- co(files);
co 函数返回一个 Promise 对象, 因此可以用 then 方法添加回调函数.
- co(files).then(() => {
- console.log('执行完成');
- });
最后我们可以看到我们没有手动执行 next 方法, 也会打印出所读取的文件.
co 模块虽然很好的帮助了我们解决了 Generator 函数必须靠执行器的问题, 但是使用起来我们都需要额外引入一个模块, 那么有没有更加方便的方式来解决呢? 继续往下看.
5. async and await
除了以上 4 中方式可以解决 Javascript 异步编程的问题外, ES7 还提供了更加方便的 async 函数和 await 命令, 了解一下?
其实 async 是 Generator 函数的语法糖, 不同点在于其内置了执行器, 也就是说 async 函数自带执行器. 看一下下面的例子:
- let p1 = new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve(1);
- }, 1000);
- });
- let p2 = new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve(2);
- }, 1000);
- });
- async function waitFn() {
- let a = await p1; // await 命令后面可以是 Promise 对象和原始类型的值, 如果使原始类型最终也会返回为 Promise 对象
- let b = await p2;
- return a + b
- }
- // async 函数的返回值是 Promise 对象, 可以用 then 方法指定下一步的操作
- waitFn().then(result => {
- console.log(result); // 2s 后输出 3
- });
async 函数内部 return 语句返回的值, 会成为 then 方法回调函数的参数. 因此这就像极了利用 co 包裹起来的 Generator 函数, 只是把 * 替换成了 async, 把 yield 替换成了 await.
可以说 async and await 是 ES7 中最重要的一个特性, 虽然其也存在一些弊端, 但是相比较而言用其处理异步代码来说还是比较得心应手的.
结语
本文简单介绍了处理好 Javascript 异步代码的五种常见方式, 每一种方式都有其使用和存在的条件和必要性, 有兴趣的同学可以对其进行单独的拓展和探究, 只有了解并掌握每一种方式各自的优点并加以运用, 才能享受异步编程带来的快感.
来源: https://juejin.im/post/5adf2298f265da0ba266a2d1