异步编程模式在前端开发过程中,显得越来越重要。从最开始的XHR到封装后的Ajax都在试图解决异步编程过程中的问题。随着ES6新标准的出来,处理异步数据流的解决方案又有了新的变化。Promise就是这其中的一个。我们都知道,在传统的ajax请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生很难看的多层回调,俗称”回调地狱”(callback hell)。另一方面,往往错误处理的代码和正常的业务代码耦合在一起,造成代码会极其难看。为了让编程更美好,我们就需要引入promise来降低异步编程的复杂性。

Promise

Promise 对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功返回值或失败信息指定处理方法。 这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。 ——MDN

我们来看一下官方定义,Promise实际上就是一个特殊的Javascript对象,反映了”异步操作的最终值”。”Promise”直译过来有预期的意思,因此,它也代表了某种承诺,即无论你异步操作成功与否,这个对象最终都会返回一个值给你。
先写一个简单的demo来直观感受一下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

const promise = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});
 
promise.then((value) => {
console.log(value);
},(err) => {
console.log(err);
});
//也可以采取下面这种写法
promise.then(value => console.log(value)).catch(err => console.log(err));

上面的例子,会在Ajax请求成功后调用resolve回调函数来处理结果,如果请求失败则调用reject回调函数来处理错误。Promise对象内部包含三种状态,分别为pending,fulfilled和rejected。这三种状态可以类比于我们平常在ajax数据请求过程的pending,success,error。一开始请求发出后,状态是Pending,表示正在等待处理完毕,这个状态是中间状态而且是单向不可逆的。成功获得值后状态就变为fulfilled,然后将成功获取到的值存储起来,后续可以通过调用then方法传入的回调函数来进一步处理。而如果失败了的话,状态变为rejected,错误可以选择抛出(throw)或者调用reject方法来处理。

请求的几个状态:

  1. pending( 中间状态)—> fulfilled , rejected
  2. fulfilled(最终态)—> 返回value 不可变
  3. rejected(最终态) —> 返回reason 不可变

如图所示:

promises

promise

一个promise内部可以返回另一个promise,这样就可以进行层级调用。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

const getAllUsers = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});
 
const getUserProfile = function(username) {
return new Promise((resolve, reject) => {
$.ajax('https://github.com/users' + username, (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
};
 
getAllUsers.then((users) => {
//获取第一个用户的信息
return getUserProfile(users[0]);
}).then((profile) => {
console.log(profile)
}).catch(err => console.log(err));

Promise实现原理

目前,有多种Promise的实现方式,我选择了https://github.com/then/promise的源码进行阅读。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function Promise(fn) {
var state = null; //用以保存处理状态,true为fulfilled状态,false为rejected状态
var value = null; //用以保存处理结果值
var deferreds = [];
var self = this;
this.then = function(onFulfilled, onRejected) {
return new self.constructor(
function(resolve, reject) {...}
);
}; //返回一个延迟处理函数,调用这个方法,就能触发用户传入的处理函数,分别对应处理promise的fulfilled状态和rejected状态
 
function handle(deferred) {...} //延迟队列处理
 
function resolve(newValue) {...} //更新value值,并把state更新为true,代表结果正常
 
function reject(newValue) {...} //更新vlaue值,并把state更新为false,代表结果错误,这个value值就是错误原因方便后面调用处理
 
function finale() {...} //清空异步队列
 
doResolve(fn, resolve, reject); //调用resolve和reject两个回调函数处理结果
}

通过阅读promise的源码,我们可以很清楚地看到,在构建一个promise对象的时候,是利用函数式编程的特性,如惰性求值和部分求值等来进行将异步处理的。而处理多线程并发的机制就是利用setTimeout(fn,0)这个技巧。

构造Promise

Promise构造函数的初始函数需要有两个参数,resolve和reject,分别对应fulfilled和rejected两个状态的处理。


1
2
3
4
5
6
7
8

var promise = new Promise((resolve, reject) => {
try {
var value = doSomething();
resolve(value);
} catch(err) {
reject(err);
}
});

Promise的常用方法

1.Promise.all(iterator):

​ 返回一个新的promise对象,其中所有promise的对象成功触发的时候,该对象才会触发成功,若有任何一个发成错误,就会触发改对象的失败方法。成功触发的返回值是所有promise对象返回值组成的数组。直接看例子吧:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//设置三个任务
const tasks = {
task1() {
return new Promise(...); //return 1
},
 
task2() {
return new Promise(...); // return 2
},
 
task3() {
return new Promise(...); // return 3
}
};
 
//列表中的所有任务会并发执行,当所有任务执行状态都为fulfilled后,执行then方法
Promise.all([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//最终结果为:[1,2,3]

2.Promise.race(iterable): 返回一个新的promise对象,其回调函数迭代遍历每个值,分别处理。同样都是传入一组promise对象进行处理,同Promise.all不同的是,只要其中有一个promise的状态变为fulfilledrejected,就会调用后续的操作。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//设置三个任务
const tasks = {
task1() {
return new Promise(...); //return 1
},
 
task2() {
return new Promise(...); // return 2
},
 
task3() {
return new Promise(...); // return 3
}
};
 
//列表中的所有任务会并发执行,只要有一个promise对象出现结果,就会执行then方法
Promise.race([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//假设任务1最开始返回结果,则控制台打印结果为`1`

3.Promise.reject(reason): 返回一个新的promise对象,用reason值直接将状态变为rejected


1
2
3
4
5

const promise2 = new Promise((resolve, reject) => {
reject('Failed');
});
 
const promise2 = Promise.reject('Failed');

上面两种写法是等价的。

4.Promise.resolve(value): 返回一个新的promise对象,这个promise对象是被resolved的。

与reject类似,下面这两种写法也是等价的。


1
2
3
4
5

const promise2 = new Promise((resolve, reject) => {
resolve('Success');
});
 
const promise2 = Promise.resolve('Success');

5.then 利用这个方法访问值或者错误原因。其回调函数就是用来处理异步处理返回值的。

6.catch 利用这个方法捕获错误,并处理。

Generator & Iterator 迭代器和生成器

虽然Promise解决了回调地狱(callback hell)的问题,但是仍然需要在使用的时候考虑到非同步的情况,而有没有什么办法能让异步处理的代码写起来更简单呢?在介绍解决方案之前,我们先来介绍一下ES6中有的迭代器和生成器。
迭代器(Iterator),顾名思义,它的作用就是用来迭代遍历集合对象。
在ES6语法中迭代器是一个有next方法的对象,可以利用Symbol.iterator的标志返回一个迭代器。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

const getNum = {
[Symbol.iterator]() {
let arr = [1,2,3];
let i = 0;
return {
next() {
return i < arr.length ? {value: arr[i++]} : {done: true};
}
}
}
}
 
//利用for...of语法遍历迭代器
for(const num of getNum) {
console.log(num);
}

而生成器(Generator)可以看做一个特殊的迭代器,你可以不用纠结迭代器的定义形式,使用更加友好地方式实现代码逻辑。
先来看一段简单的代码:


1
2
3
4
5
6
7
8
9
10
11

function* getNum() {
yield 1;
yield 2;
yield 3;
}
//调用生成器,生成一个可迭代的对象
const gen = getNum();
 
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: true}

生成器函数的定义需要使用function*的形式,这也是它和普通函数定义的区别。yield是一个类似return的关键字,当代码执行到这里的时候,会暂停当前函数的执行,并保存当前的堆栈信息,返回yield后面跟着表达式的值,这个值就是上面代码所看到的value所对应的值。而done这个属性表示是否还有更多的元素。当donetrue的时候,就表明这个迭代过程结束了。需要注意的是这个next方法其实传入参数,这个参数表示上一个yield语句的返回值,如果你给next方法传入了参数,就会将上一次yield语句的值设置为对应值。

利用generator的异步处理

先来看一下下面这段代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function getFirstName() {
setTimeout(() => {
gen.next('hello');
},2000);
}
 
function getLastName() {
setTimeout(() => {
gen.next('world');
},1000);
}
 
function* say() {
let firstName = yield getFirstName();
let lastName = yield getLastName();
console.log(firstName + lastName);
}
 
var gen = say();
 
gen.next(); // {value: undefined, done: false}
//helloworld

我们可以发现,当第一次调用gen.next()后,程序执行到第一个yield语句就中断了,而在getFirstName里显式地将上一个yield语句的返回值改为hello,触发了第二yield语句的执行。以此类推,最终就打印出我们想要的结果了。

spawn函数

我们可以考虑把上面的代码改写一下,在这里将Promise和Generator结合起来,将异步操作用Promise对象封装好,然后,resolve出去,而创建一个spawn函数,这个函数的作用是自动触发generatornext方法。来看一下代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

function getFirstName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello');
}, 2000);
});
}
 
function getLastName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('world');
}, 1000);
});
}
 
function* say() {
let firstName = yield getFirstName();
let lastName = yield getLastName();
console.log(firstName + lastName);
}
 
function spawn(generator) {
return new Promise((resolve, reject) => {
var onResult = (lastPromiseResult) => {
var {value, done} = generator.next(lastPromiseResult);
if(!done) {
value.then(onResult, reject);
}else {
resolve(value);
}
}
onResult();
});
}
 
spawn(say()).then((value) => {console.log(value)});

到这里,这个解决方案就很接近接下来要介绍的async/await的实现方式了。

Async/Await

这两个关键字其实是一起使用的,async函数其实就相当于funciton *的作用,而await就相当与yield的作用。而在async/await机制中,自动包含了我们上述封装出来的spawn自动执行函数。
利用这两个新的关键字,可以让代码更加简洁和明了:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

function getFirstName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('hello');
resolve('hello');
}, 2000);
});
}
 
function getLastName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('world');
resolve('world');
}, 1000);
});
}
 
async function say() {
let firstName = await getFirstName();
let secondName = await getLastName();
return firstName + lastName;
}
 
console.log(say());

执行结果为,先等待2秒打印hello,再等待1秒打印world,最后打印’helloworld’,与预期的执行顺序是一致的。

上面的代码你需要注意的是,你必须显式声明await,否则你会得到一个promise对象而不是你想要获得的值。

比起Generator函数,async/await的语义更好,代码写起来更加自然。将异步处理的逻辑放在语法层面去处理,写的代码也更加符合人的自然思考方式。

错误处理

对于async/await这种方法来说,错误处理也比较符合我们平常编写同步代码时候处理的逻辑,直接使用try..catch就可以了。


1
2
3
4
5
6
7
8
9
10
11
12
13
14

function getUsers() {
return $.ajax('https://github.com/users');
}
 
async function getFirstUser() {
try {
let users = await getUsers();
return users[0].name;
} catch (err) {
return {
name: 'default user'
}
}
}

写在最后

目前,Service Workers、 Fetch、 StreamsLoader 等全部基于 Promise。可以预见,在未来的Javascript异步编程中,Promise及其衍生出来的技术必将大放异彩。那么,你准备好了吗?

原文地址:http://scq000.github.io/2016/11/05/%E5%89%8D%E7%AB%AF%E7%9A%84%E5%BC%82%E6%AD%A5%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E4%B9%8BPromise%E5%92%8CAwait-Async/

前端的异步解决方案之Promise和Await-Async的更多相关文章

  1. 【异步编程】Part1:await&async语法糖让异步编程如鱼得水

    前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...

  2. Promise,Generator,Await/Async

    上节中忘记讲:Iterator接口和Generator函数的关系了,Symbol.iterator方法的最简单的实现就是通过Generator函数: let myIterable = { [Symbo ...

  3. 异步解决方案----Promise与Await

    前言 异步编程模式在前端开发过程中,显得越来越重要.从最开始的XHR到封装后的Ajax都在试图解决异步编程过程中的问题.随着ES6新标准的到来,处理异步数据流又有了新的方案.我们都知道,在传统的aja ...

  4. ES6学习笔记(十二)异步解决方案Promise

    1.Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大.它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了P ...

  5. javascript异步编程的前世今生,从onclick到await/async

    javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...

  6. 5分种让你了解javascript异步编程的前世今生,从onclick到await/async

      javascript与异步编程 为了避免资源管理等复杂性的问题,javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是 ...

  7. Promise、async、await 异步解决方案

    参考: https://www.cnblogs.com/CandyManPing/p/9384104.html  或  https://www.jianshu.com/p/fe0159f8beb4(推 ...

  8. JS异步编程 (2) - Promise、Generator、async/await

    JS异步编程 (2) - Promise.Generator.async/await 上篇文章我们讲了下JS异步编程的相关知识,比如什么是异步,为什么要使用异步编程以及在浏览器中JS如何实现异步的.最 ...

  9. js async await 终极异步解决方案

    既然有了promise 为什么还要有async await ? 当然是promise 也不是完美的异步解决方案,而 async await 的写法看起来更加简单且容易理解. 回顾 Promise Pr ...

随机推荐

  1. Activiti:MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 3 无效。

    在win下开发,有时编译或运行项目会报3字节的UTF-8序列的字节3无效. 解决该问题的办法 1.将xml头文件改为GBK编码方式 ,我这里不OK <?xml version="1.0 ...

  2. 蓝桥杯 基础练习 BASIC-19 完美的代价

    基础练习 完美的代价   时间限制:1.0s   内存限制:512.0MB 问题描述 回文串,是一种特殊的字符串,它从左往右读和从右往左读是一样的.小龙龙认为回文串才是完美的.现在给你一个串,它不一定 ...

  3. 分析诊断工具之一:MYSQL性能查看(多指标)

    网上有很多的文章教怎么配置MySQL服务器,但考虑到服务器硬件配置的不同,具体应用的差别,那些文章的做法只能作为初步设置参考,我们需要根据自己的情况进行配置优化,好的做法是MySQL服务器稳定运行了一 ...

  4. DCloud-HBulder:杂项

    ylbtech-DCloud-HBulder:杂项 1.返回顶部   2.返回顶部   3.返回顶部   4.返回顶部   5.返回顶部     6.返回顶部   7.返回顶部   8.返回顶部   ...

  5. maven中maven dependencies中依赖出现了项目

    maven 中maven dependencies中依赖出现了项目,把依赖的项目关掉,项目消失,但是还是无法打包 ,出现的错误如图.说明:依赖的项目为project-dao  打包的项目为projec ...

  6. 【OK210试用体验】进阶篇(2)视频图像采集之MJPG-streamer编译(arm移植)

    上一篇([OK210试用体验]进阶篇(1)视频图像采集之MJPG-streamer编译(Ubuntu系统下))进行了MJPG-streamer在Ubuntu下的编译及测试,这一篇针对OK210,进行a ...

  7. dialog插件demo

    基本操作 默认窗体 new Dialog('这是一个默认对话框').show(); 非模态对话框 new Dialog('非模态对话框,可以打开多个!',{modal:false}).show(); ...

  8. SQL基础(2)

    SQL TOP (1)TOP子句 OP 子句用于规定要返回的记录的数目. 对于拥有数千条记录的大型表来说,TOP 子句是非常有用的. 注释:并非所有的数据库系统都支持 TOP 子句. (2)SQL S ...

  9. 隔行变色---bai

    <!DOCTYPE html> <html> <style> .mousein { background-color:blue; cursor: pointer; ...

  10. 问题:oracle case when;结果:Oracle CASE WHEN 用法介绍

    Oracle CASE WHEN 用法介绍 1. CASE WHEN 表达式有两种形式 --简单Case函数 CASE sex WHEN '1' THEN '男' WHEN '2' THEN '女' ...