为了解决异步嵌套,写出更优雅,更易维护的代码,先后出现了ES6 提供的 Promise 方法,然后又是Generator/yield组合和 ES7 提供的 Async/Await 语法糖可以更好解决多层回调问题。
有了promise,为啥还要有Async/Await ?
虽然有了promise,链式调用更清晰了,但是需求复杂起来,就会发现代码充斥着.then,代码看起来也很吃力,这就出现了Async/Await解决了这个问题。还有Generator/yield接下来会一一介绍。
一,Promise
Promise表示一个异步状态的最终状态————>成功或者失败,成功通过resolve返回成功的值,失败通过reject返回错误信息。
Promise一旦创建就会立即执行
Promise有三种状态:
(1)pending:请求中...
(2)fulfilled:完成状态 (从pending 到 fulfilled,resolve()接受成功返回的值,返回出去)
(3)rejected:拒绝状态(pending 到 rejected,reject()接受错误信息并返回出去,当有错误最好用reject(new Error("出错了")),更规范,也可以reject("出错了")直接字符串返回)
基本用法:
1 let p = new Promise((resolve,reject) => { 2 //............ 3 //成功状态 4 if(条件成立){ 5 resolve('success') 6 }else{ 7 reject(new Error("出错了")) 8 } 9 10 }); 11 p.then(res=> { 12 console.log(res);//成功 13 }).catch(err=>{ 14 console.log(err);//失败 15 });
Promise特点就是,状态一经确定为fulfilled或者rejected,就不会再变,状态就凝固了。
为什么说状态一经变化,就凝固了呢?看一下这个例子:
1 let p1 = new Promise((resolve,reject) => { 2 //............ 3 //成功状态 fulfilled状态 4 resolve('success'); 5 console.log(1); 6 //成功状态了,就不会改变了,所以下面这个状态不会变成拒绝状态 7 reject(new Error("出错了")) 8 }); 9 10 p1.then(res=> { 11 console.log(res);//成功 12 }).catch(err=>{ 13 console.log(err);//失败 14 }); 15 //打印结果:1 success
其实resolve下面的代码可以执行,但是resolve()先执行的,所以此时promise的状态是fulfilled状态,因为状态一经确定就不会变了,所以,reject()在下面没有触发,是因为状态是fulfilled状态了。
then方法:
promise 原型上存在一个then方法——》Promise.prototype.then(),它的作用是为 Promise 实例添加状态改变时的回调函数。then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例),因此可以采用链式写法
then有三种参数形式:
①.then(fun1,fun2) //fun1是成功了的回调,fun2是拒绝了(rejected)的回调
②.then(fun1) //fun1 是成功了的回调
③.then(null/undefined,fun2) //这个只接收拒绝了(rejected)回调,和catch 一样,都能用于指定发生错误时的回调函数。这种不太常用,更推荐使用catch
下面来看一下catch:
catch方法:
promise 原型上另一个方法——》Promise.prototype.catch(),catch()方法返回的还是一个 Promise 对象,后面还可以调用.then()方法
一般建议把catch放在链式的末尾,因为catch用于捕获它前面的异步中的错误,对于.catch()后面的链式调用(.catch().then())出现了错误就与catch()无关了,就捕获不到了
1 let p = new Promise((resolve,reject) => { 2 3 }); 4 p.then(res=> { 5 console.log(res);//成功 6 }).catch(err=>{ 7 console.log(err);//失败 8 });
借用阮一峰阮老师的例子:
1 const promise = new Promise(function(resolve, reject) { 2 throw new Error('test'); 3 }); 4 promise.catch(function(error) { 5 console.log(error); 6 });
Error :test
等价写法如下:
1 // 写法一 2 const promise = new Promise(function(resolve, reject) { 3 try { 4 throw new Error('test'); 5 } catch(e) { 6 reject(e); 7 } 8 }); 9 promise.catch(function(error) { 10 console.log(error); 11 }); 12 //结果:Error test 13 // 写法二 14 const promise = new Promise(function(resolve, reject) { 15 reject(new Error('test')); 16 }); 17 promise.catch(function(error) { 18 console.log(error); 19 }); 20 //结果:Error test
比较上面两种写法,可以发现reject()
方法的作用,等同于抛出错误。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。
1 const promise = new Promise(function (resolve, reject) { 2 setTimeout(function () { throw new Error('test') }, 0); 3 resolve('ok'); 4 }); 5 promise.then(function (value) { 6 console.log(value) 7 }).catch(err=>{ 8 console.log("错误啦") 9 }); 10 // ok 11 // Uncaught Error: test
上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
1 const promise = new Promise(function (resolve, reject) { 2 throw new Error('test'); 3 resolve('ok'); 4 }); 5 promise.then(function (value) { console.log(value) }).catch(err=>{ 6 console.log("错误") 7 });
这样就会正常捕获。
注意:catch()方法之中,还能再抛出错误,抛出去的错误,如果后面还有catch(),会被这个catch()捕获。
Promise.resolve()
将现有参数转换成Promise对象的快捷方式
Promise.resolve(1).then(res=>{ console.log(res)//1 })
Promise.resolve()
获取一个拒绝状态的Promise对象的快捷方式
1 Promise.reject(1).catch(err=>{ 2 console.log(err)//1 3 })
Promise.all([p1,p2,p3....])
p1,p2,p3都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve方法,将参数转为 Promise 实例,再进一步处理
参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。当所有的promise的状态都是成功状态才返回数组,只要其中有一个的对象是reject的,就返回reject的状态值。
1 let p1 = Promise.resolve(123); 2 let p2 = Promise.resolve('hello'); 3 let p3 = Promise.resolve('success'); 4 let p4 = Promise.reject('error'); 5 6 Promise.all([p1,p2,p4]).then(result => { 7 console.log(result); 8 }).catch(err=> { 9 console.log(err); 10 });
error
Promise.race([p1,p2,p3....])
p1,p2,p3都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve方法,将参数转为 Promise 实例,再进一步处理
Promise.allsettled([p1,p2,p3....])
只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,才会结束。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
1 let p1 = Promise.resolve(123); 2 let p4 = Promise.reject('error'); 3 4 5 Promise.allSettled([p1,p4]).then(result => { 6 console.log(result); 7 }).catch(err=> { 8 console.log(err); 9 });
promise的优缺点:
优点:
1.Promise 分离了异步数据获取和业务逻辑,有利于代码复用。
2.可以采用链式写法
3.一旦 Promise 的值确定为fulfilled 或者 rejected 后,不可改变。
缺点:
代码冗余,语义不清。
二,Async/await
async function xxx (){}
async 声明的函数的返回本质是promise
1 (async function f(){ 2 return "哈哈" 3 })()
如果return 出来的不是promise ,默认会通过Promise.resolve("哈哈"),转换成promise对象
等同于:
1 (async function f(){ 2 return Promise.resolve("哈哈") 3 })()
对待async 返回值像对待promise一样,结果用.then()链式获取
await 必须和async配合使用,await 必须放在async函数内部使用,等待一个异步方法执行完成返回结果,无需用then接收,直接返回结果出来。
async / await特点:
①async 后面的函数是异步的,返回一个promise对象,await必须放在内部使用
②await 在等待后面的promise返回结果后,再继续执行下面的代码
③await 后面最好是一个promise异步对象,敲黑板!!!是promise对象(settimeout这也是异步但是不可以),通过await去等待,当然是非异步的其他值也可以,只是直接执行罢了,但是用await也没就啥意义啦
1 async function b(){ 2 return 1 3 } 4 b();
可以看出async 后面的函数返回一个promise对象
例子:
1 function p(wait){ 2 return new Promise((resolve,reject)=>{ 3 setTimeout(()=>{ 4 resolve(1); 5 },wait) 6 }) 7 } 8 async function b(){ 9 console.log(222); 10 let res=await p(1000); 11 console.log(res); 12 console.log(333); 13 } 14 b();
先输出222,await暂停,然后等待p(1000)异步执行完毕,返回结果,在继续执行下面的代码,输出p(1000)的结果1,然后输出333
对比一下 promise 和async/await使用:
promise 解决了内嵌回调的问题
例子:
1 function waitFn(wait) { 2 return new Promise((resolve,reject) => { 3 setTimeout(() => { 4 resolve(wait); 5 },wait); 6 }); 7 } 8 9 waitFn(1000).then(res1 => { 10 console.log("res1:"+res1); 11 return waitFn(res1+1000); 12 }).then(res2 => { 13 console.log("res2:"+res2); 14 return waitFn(res2+1000); 15 }).then(res3 => { 16 console.log("res3:"+res3); 17 })
等待1s输出res1:1000,等待2s输出res2:2000,等待3s输出res3:3000
用async/await 怎么写:
1 function waitFn(wait) { 2 return new Promise((resolve,reject) => { 3 setTimeout(() => { 4 resolve(wait); 5 },wait); 6 }); 7 } 8 9 async function pro(){ 10 let res1=await waitFn(1000); 11 console.log("res1:"+res1); 12 let res2=await waitFn(1000+res1); 13 console.log("res2:"+res2); 14 let res3=await waitFn(1000+res2); 15 console.log("res3:"+res3); 16 } 17 pro()
结果同样:等待1s输出res1:1000,等待2s输出res2:2000,等待3s输出res3:3000
是不是更简洁,更清晰了。
三,Generator/yield
generator 函数function*()定义一个生成器函数,函数体内部使用yield表达式,这个生成器函数会返回一个generator对象。
引用阮老师的一个例子:
1 function* helloWorldGenerator() {
2 yield 'hello';
3 yield 'world';
4 return 'ending';
5 }
6
7 var hw = helloWorldGenerator();
8 hw.next();//{value: "hello", done: false}
9 hw.next();//{value: "world", done: false}
10 hw.next();//{value: "ending", done: true}
11 hw.next();//{value: undefined, done: true}
生成器函数不像普通函数,调用就会立即执行代码并返回结果,而是会返回一个生成器迭代对象(Iterator),这个迭代器对象有个next()方法,调用next()方法使得内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到yield表达式或者return语句就会停止,next()执行会返回一个对象,这个对象是当前yield表达式的值,这个对象包含两个属性,value和done,value表示本次yield表达式的值,done属性为布尔类型,表示生成器之后是否还有yield语句,即生成器是否已经执行完毕,如果done为true,表示遍历结束啦。done为false,就是下面还有yield语句,还没执行完毕,就会从本次yield继续执行,执行到下一个yield,next()同样返回一个对象包含{value:xx,done:true/false},value表示本次yield表达式的值,done属性为布尔类型,就这样重复遍历着,直到done为true,表示遍历结束啦。
yield表达式是暂停执行的标记,而next方法可以恢复执行。
第一次next()不用传参数,传参数也不会用到,第一次next()意思是启动的意思,当调用了第二次next()可以传递参数,参数就是上一次yield左边的变量的值。如果next()不传递参数,上一次yeild左边变量的值就是undefined。
引用阮老师的例子:
1 function* foo(x) { 2 var y = 2 * (yield (x + 1)); 3 var z = yield (y / 3); 4 return (x + y + z); 5 } 6 7 var a = foo(5); 8 a.next() // Object{value:6, done:false} 9 a.next() // Object{value:NaN, done:false} 10 a.next() // Object{value:NaN, done:true} 11 12 var b = foo(5); 13 b.next() // { value:6, done:false } 14 b.next(12) // { value:8, done:false } 15 b.next(13) // { value:42, done:true }
yield和return异同:
①yield和return都可以返回值
②yield可以有暂停的作用,直到调用next(),内部的指针移到了该yield的表达式,才会继续执行。return不具备记忆功能。
③一个函数里面只有一个return 语句,而yield可以有多个。
④正常函数只能有一个返回值,只能执行一次return语句。Generator 函数可以返回一系列的值,因为可以有任意多个yield。
generator 函数function* (){}里面可以不放yield,但是不会执行,需要调用.next()方法才会执行
eg:
var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { var length = a.length; for (var i = 0; i < length; i++) { var item = a[i]; if (typeof item !== 'number') { yield* flat(item); } else { yield item; } } }; for (var f of flat(arr)) { console.log(f); } // 1, 2, 3, 4, 5, 6
for....of和Symbol.iterator
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不再需要调用next
方法。
1 function* foo() { 2 yield "a"; 3 yield "b"; 4 yield "c"; 5 yield "d"; 6 yield "e"; 7 return "f"; 8 } 9 10 for (let v of foo()) { 11 console.log(v); 12 } 13 // a b c d e
js原生对象本来不具备Iterator接口,不能用for..of遍历。
例子:
1 let obj={"a":1,"b":2,"c":3}; 2 console.log(obj) 3 console.log(obj[Symbol.iterator]) 4 for(let item of obj){ 5 console.log(item) 6 }
任意一个对象的Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
看下面这个例子:
1 let obj={"a":1,"b":2,"c":3}; 2 obj[Symbol.iterator] = function* () { 3 yield obj["a"]; 4 yield obj["b"]; 5 yield obj["c"]; 6 }; 7 console.log(obj); 8 console.log(obj[Symbol.iterator]); 9 for(let item of obj){ 10 console.log(item) 11 }
看上面这个例子就具备了Iterator接口,就可以用for...of遍历。
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator
属性,执行后返回自身。
1 function* gen(){ 2 // some code 3 } 4 5 var g = gen(); 6 7 g[Symbol.iterator]() === g
//true
promise , generator ,async/await的使用例子:
老式的回调地狱:
1 step1(function (value1) { 2 step2(value1, function(value2) { 3 step3(value2, function(value3) { 4 step4(value3, function(value4) { 5 // Do something with value4 6 }); 7 }); 8 }); 9 });
用promise改写:
1 Promise.resolve(step1) 2 .then(step2) 3 .then(step3) 4 .then(step4) 5 .then(value4) { 6 // Do something with value4 7 }) 8 .catch(error=>{ 9 10 })
用async/await改写:
1 async function task(value1) { 2 var value2 = await step1(value1); 3 var value3 = await step2(value2); 4 var value4 = await step3(value3); 5 var value5 = await step4(value4); 6 console.log(value5) 7 }
用generator 函数改写:
1 function step(value){ 2 return value+1; 3 } 4 function* task(value) { 5 try { 6 var value2 = yield step(value); 7 var value3 = yield step(value2); 8 var value4 = yield step(value3); 9 var value5 = yield step(value4); 10 // Do something with value4 11 console.log(value5); 12 } catch (e) { 13 // Handle any error from step1 through step4 14 } 15 } 16 go(task(1)); 17 function go(task) { 18 var taskObj = task.next(task.value); 19 console.log(taskObj); 20 // 如果Generator函数未结束,就继续调用 21 if (!taskObj.done) { 22 task.value = taskObj.value; 23 go(task); 24 } 25 }