Promise
Promise 对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功返回值或失败信息指定处理方法。 这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。
我们来看一下官方定义,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); });
|
上面的例子,会在Ajax请求成功后调用resolve 回调函数来处理结果,如果请求失败则调用reject 回调函数来处理错误。Promise对象内部包含三种状态,分别为pending,fulfilled和rejected。这三种状态可以类比于我们平常在ajax数据请求过程的pending,success,error。一开始请求发出后,状态是Pending,表示正在等待处理完毕,这个状态是中间状态而且是单向不可逆的。成功获得值后状态就变为fulfilled,然后将成功获取到的值存储起来,后续可以通过调用then 方法传入的回调函数来进一步处理。而如果失败了的话,状态变为rejected,错误可以选择抛出(throw)或者调用reject 方法来处理。
请求的几个状态:
- pending( 中间状态)—> fulfilled , rejected
- fulfilled(最终态)—> 返回value 不可变
- rejected(最终态) —> 返回reason 不可变
如图所示:
promises
一个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) => { |
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; |
通过阅读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
|
|
2.Promise.race(iterable): 返回一个新的promise 对象,其回调函数迭代遍历每个值,分别处理。同样都是传入一组promise对象进行处理,同Promise.all不同的是,只要其中有一个promise的状态变为fulfilled 或rejected ,就会调用后续的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
|
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}; } } } }
|
而生成器(Generator)可以看做一个特殊的迭代器,你可以不用纠结迭代器的定义形式,使用更加友好地方式实现代码逻辑。 先来看一段简单的代码:
1 2 3 4 5 6 7 8 9 10 11
|
function* getNum() { yield 1; yield 2; yield 3; }
|
生成器函数的定义需要使用function* 的形式,这也是它和普通函数定义的区别。yield 是一个类似return 的关键字,当代码执行到这里的时候,会暂停当前函数的执行,并保存当前的堆栈信息,返回yield 后面跟着表达式的值,这个值就是上面代码所看到的value 所对应的值。而done 这个属性表示是否还有更多的元素。当done 为true 的时候,就表明这个迭代过程结束了。需要注意的是这个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(); |
我们可以发现,当第一次调用gen.next() 后,程序执行到第一个yield 语句就中断了,而在getFirstName 里显式地将上一个yield 语句的返回值改为hello ,触发了第二yield 语句的执行。以此类推,最终就打印出我们想要的结果了。
spawn函数
我们可以考虑把上面的代码改写一下,在这里将Promise和Generator结合起来,将异步操作用Promise 对象封装好,然后,resolve 出去,而创建一个spawn 函数,这个函数的作用是自动触发generator 的next 方法。来看一下代码:
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' } } }
|
|