这篇文章是之前我发在我公众号上的,现在也迁移一份放到博客网站上(

背景

在编写 js 代码时不可避免的需要使用到许多异步操作, 使用传统的回掉方式非常容易陷入回调地狱中, 于是聪明的人类发明了许多神奇的技术来解决这个问题来使得异步代码可以更优雅的写出

在本文中, 异步操作均以 setTimeout 来代替, setTimeout 不会阻塞 js 代码的继续执行

传统回调模式

在大部分情况下, 使用传统回调模式可以解决大部分需求, 而且写起来和看起来也不算难受, 例如下面这段代码示例:

// 假设 getData 是根据 param 来获取不同数据的
// 可以类比到如 ajax 的 get/post 等操作
function getData(param, callback)
{
    // 某个异步的实现是回调模式
    setTimeout(() => {
        let data = 'Hello ' + param;
        callback(data);
    }, 1000);
}

// 传统回调方式
getData('there', (d) => {
    console.log(d);
});
console.log('main');

我们调用 getData 时给它传递了一个回调函数, 因此在操作完成后, 获取到的结果会被正确的传递给 callback 以便进行进一步操作, 下面是上面代码的输出:

main
Hello there

乍一看似乎没有什么问题, 但是容易陷入 "回调地狱"
想象一下, 我们在某个场景下, 一共需要进行 10 次异步操作, 且第 i 次操作依赖于第 i - 1 次操作的结果 (或者需要等待第 i - 1 次操作完成后才能进行), 如: 互相依赖的网络请求, 按顺序的动画操作, 等待所有依赖的数据加载完成后再继续执行代码等都属于这样的情景, 为了实现这样的需求, 使用传统回调模式时就必须要写出下面这样的代码:

getData('a', (d) => {
    getData(d + 'b', (d) => {
        getData(d + 'c', (d) => {
            getData(d + 'd', (d) => {
                console.log(d);
            });
        });
    });
});

/*
4秒后输出:
Hello Hello Hello Hello abcd
*/

三角形代码还是很美的
在少量的依赖的情境下处理起来也还好, 但当依赖项很多时, 这样的代码就不仅看起来非常霸气, 写起来也令人心烦了

Promise

为了避免调用链变得很长很难写难读, 大牛们提出了 Promise 的解决方案

Promise 是什么

Wikipedia(https://en.wikipedia.org/wiki/Futures_and_promises):
In computer science, future, promise, delay, and deferred refer to constructs used for synchronizing program execution in some concurrent programming languages.

Tamce(渣翻于 2018-04-12):
Future, Promise, Delay, Deferred 是为了在异步编程模式中为了同步程序执行而提供的的方法

我们这里只讨论 Promise
简单来说, Promise 模型描述了 是一个提供了比较方便的接口来对异步操作进行控制的对象或者方式, 并不是只有 javascript 才有 Promise, Promise 指的是一种异步编程模式

基本概念

可以在 Promise/A+ 看到 Promise 的详细定义;这里暂时不过多阐述,只给出几个名词的解释。

Promise/A+ :

“promise” is an object or function with a then method whose behavior conforms to this specification.
“thenable” is an object or function that defines a then method.
“value” is any legal JavaScript value (including undefined, a thenable, or a promise).
“exception” is a value that is thrown using the throw statement.
“reason” is a value that indicates why a promise was rejected.

简而言之, 一个 Promise 对象是一个具有 then 方法的对象或者函数,其基本规则大致可以用下面的话来简单总结:

  1. then 方法,方法接受两个参数,分别是 onfulfilledonrejected 回调(可为空)
  2. 一个 Promise 要么处于 Pending 状态,要么处于 Fulfilled 状态或 Rejected 状态(并称 Settled
  3. 当异步操作成功完成时,Promise 保证结果被传入 onfulfilled 回调,当失败时,理由被传入 onrejected 回调
  4. then 方法返回的还是一个 Promise 对象

使用 Promise

浏览器中提供了 Promise 对象,利用这个,我们可以将上述的 getData 改造为 Promise 版本的。

// Promise 版的 getData
function getData(param)
{
    return new Promise((resolve, reject) => {
        // 在内部,可以执行各种异步操作或者耗时的操作
        // 调用 resolve(value) 将 Promise 状态变为 Fulfilled
        // 调用 reject(reason) 或者抛出异常将 Promise 状态变为 Rejected
        setTimeout(() => {
            resolve('Hello ' + param);
        }, 1000);
    });
}

// 使用 then
getData('there').then(data => console.log(data));

虽然看起来好像没多大差别(只是把回调函数换了一个位置放而已)
不过,使用 Promise 的最大好处在于用这种模式可以实现链式调用(在链式调用时,下一个 then 收到的参数是上一个的返回值),如下面的例子:

getData('a').then(d => {
    return getData(d + 'b');
}).then(d => {
    return getData(d + 'c');
}).then(d => {
    return getData(d + 'd');
}).then(d => {
    console.log(d);
});
// 会在4秒后输出
// Hello Hello Hello Hello abcd

可以看到之前传统回调的三角形代码已经被链式调用展平了,另外, 利用数组的 reducePromise,上面的代码我们还可以改写成这样:

[getData('a'), 'b', 'c', 'd'].reduce((a, b) => {
    return a.then(d => {
        return getData(d + b);
    });
}).then(result => console.log(result));

// 4秒后输出
// Hello Hello Hello Hello abcd

使用 Promise 和数组可以组合出很多方便的写法,另外 Promise 对象上还提供了 allrace 等常用方法

  • all 方法接受一个数组作为参数并返回一个 Promise,方法会并行运行数组内的异步操作,当数组内所有 Promise 均进入 Fulfilled 状态的时,该 PromiseFulfill 并将各结果以数组的形式返回。
  • race 方法参数和 all 类似,但只要数组内有一个 Promise 完成时就停止,结果就是这个最先完成的结果。

例如:

Promise.all([getData('a'), getData('b')]).then(d => console.log(d));
// ["Hello a", "Hello b"]
// 执行时间 1s 左右

Promise.race([getData('a'), getData('b')]).then(d => console.log(d));
// Hello a
// 执行时间 1s 左右(由于本例使用的时 setTimeout,而 a 在 b 的前面,所以 a 比 b 先进入队列执行,所以 a 一定比 b 快,所以返回的结果一定是 Hello a,在其他执行时间不确定的异步操作时就不会总是第一个的结果了)

更多例子

下面是我在某些项目中的做法(已经添加注释),希望可以给大家一些参考(同时也希望从大佬获得一些意见)
(其实感觉页面中一般涉及到异步的部分大部分也都基本是 ajax 了)

// auth 函数检查本地是否有用户数据,如果没有,那么调用接口判断是否登陆
// 如果已登陆,则设置本地的数据,否则跳转登陆页面
// auth 函数返回一个 Promise,value 为用户数据
function auth() {
  // 暂时没用 localStorage
  if (undefined != window.user)
    // 直接返回一个处于 Fulfilled 状态的 Promise
    return Promise.resolve(window.user);
  // 用 jQuery 的 ajax 异步请求接口
  return $.get('/api/me').catch(e => {
    window.location = '/login';
    return e;
  }).then(data => {
    window.user = data;
    return data;
  });
}

// ========== 其他页面或者任何地方 =============
// 代码中的一个例子
auth().then(user => {
  // 可以在这里安全的使用用户数据 user 了
  $.get('/activities/mine').then(d => {
    // 用 Vue 做数据绑定(也有其他的纯粹提供数据绑定的轻量库,但我没用233)
    window.vue.records = d;
  });
});

说在后面

其实本来一开始想写关于这方面的内容的时候是接触了好几个解决方案如 Promise/Futureco 包装器,Function Generatorasync/await,其中几个也简单看了一下实现原理,于是就很想写一个这样的文章;本来是想写实现原理的,也就是像是动手实现一个 Promise 这种,但是写着写着就变科普了,就这样吧,async/await 是真的爽(逃

希望对这方面不熟的朋友门能从我的文章中获得一些什么,大神们轻喷(小声),也欢迎大家在评论区交流~

标签: javascript, async, promise, web

添加新评论