Javascript异步回调:从手搓promise到function */yield与async/await
Author:[email protected] Date:
虽然我对js的鄙视一直都是无以复加,但是奈何前端环境不得不依赖javascript。哪些nodejs的大神们四处布道nodejs统治一切:单线程非阻塞,高IO操作。但是,java也可以做好吧,而且GO做的更干练!假设你的应用程序要做两件事情,分别是A和B。你发起请求A,等待响应,出错。发起请求B,等待响应,出错。Go语言的阻塞模型可以非常容易地处理这些异常,而换到了Node里,要处理异常就要跳到另一个函数里去,事情就会变得复杂。Node的非阻塞模型没有了多线程,但却多出了“回调地狱”问题。
所以在此谈下JS的异步回调:promise yield async/await
对本篇的基础知识,安利下:
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
Promise
promise基本特性:
Promise 是一个对象,使用的时候new 即可。new promise时, 需要传递一个executor()执行器——这个函数在new Promise 时立即执行;
Promise 的 executor 函数有两个参数,一个是 resolve,一个是 reject。resolve 让 Promise 由 pending 等待态变成 fulfilled 成功态,reject 让 Promise 由 pending 等待态变成 rejected 失败态。resolve 和 reject 是 Promise 提供的。
Promise 的 3 个状态:pending 等待态、fulfilled 成功态、rejected 失败态。promise 的默认状态是 pending;
promise 只能从pending到rejected, 或者从pending到fulfilled,状态一旦确认,就不会再改变;
promise 必须有一个then方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」。then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。
如果调用 then 时,promise 已经成功,则执行onFulfilled,参数是promise的value
如果调用 then 时,promise 已经失败,那么执行onRejected, 参数是promise的reason
如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调onRejected
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用:在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject函数的作用:在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
promise 案例:
function timeout(value) { return new Promise(((resolve) => { setTimeout(() => { value++; resolve(value); }, 500); })); } timeout(0).then((res) => { console.log('res'); console.log(res); }); async function test(value) { console.log('value'); const c = await timeout(value); console.log(c); } test(0);
Promise是ES6之后原生的对象,我们只需要实例化Promise对象就可以直接使用。
Promise属性及方法
Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;
当你的数据不是promise实例,或者你不知道他是不是promise,而你又想把他当做promise实例来使用的时候,比如:
Promise.resolve(data ? data : getData()).then(...)
用来创建微任务,更新视图的时候非常有用
Promise.resolve().then(() => refreshView());
Promise.all([p1,p1,p3,]).then(results=>{}) ,all接收一个数组参数,里面的值最终都算返回Promise对象——它们都执行完后才会进到then里面,results就是[p1Res,p2Res,p3Res]
有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据
Promise.race(iterable),传入的promises数组中一个promose resolve 或者reject,就马上返回其promise
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」
Promise.any(),只要其中的一个 promise 完成,就返回那个已经有完成值的 promise 。如果可迭代对象中没有一个 promise 完成(即所有的 promises 都失败/拒绝),就返回一个拒绝的 promise。目前没有几个浏览器支持。
promise相关笔试题
promise顺序
Promise.resolve().then(() => { console.log('a'); }).then((res) => { console.log('b') }).then((res) => { console.log('c') }).then((res) => { console.log('d') }) Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(4); })
这个很好理解
再来一题
Promise.resolve().then(() => { console.log(0); return Promise.resolve(1); }).then((res) => { console.log(res) }) Promise.resolve().then(() => { console.log(3); }).then(() => { console.log(4); }).then(() => { console.log(5); }).then(() => { console.log(6); }).then(() =>{ console.log(7); })
当你在一个Promise的解决函数(resolve)中传递另一个Promise时,外部Promise的状态将会被挂起,直到内部Promise被解决。一旦内部Promise解决了,外部Promise也会随之解决,并且它的.then()方法会被放入微任务队列中。下面这道题就很好做了
new Promise(((resolve) => { console.log(1) new Promise((resolve,reject)=>{ console.log(2) resolve() }).then(()=>{ console.log(3) }).then(()=>{ console.log(4) }) resolve() })).then(()=>{ console.log(5) }).then(()=>{ console.log(6) })
首先同步地执行 console.log(1)。
然后 resolve() 立即调用,但是微任务队列中的内嵌 Promise 的 then 回调会被放到微任务队列中。
内嵌 Promise 的构造函数执行同步代码 console.log(2),然后 resolve() 调用,这会将 console.log(3) 放入微任务队列。
同步代码执行完毕后,微任务队列的回调开始执行。首先是内嵌 Promise 的 then 回调中 console.log(3),然后是外层 Promise 的 then 回调中 console.log(5)。
还是推荐阅读下:《弄懂javascript的执行机制:事件轮询|微任务和宏任务|定时器》
基础题:宏任务/微任务/异常捕获
console.log('a') function f () { console.log('b') return new Promise((resolve, reject) => { console.log('c') setTimeout(() => { reject(1); resolve(2); console.log('d') }, 0); }); } console.log('e') let p = f(); p.then((res) => { console.log('resolve'); console.log(res); },(res) => { console.log('reject') console.log(res); }).catch(res => { console.log('catch') console.log(res); });
promise内的代码块是同步执行的。reject resolve 是内异步执行的(微任务)。如果reject resolve 包含在异步函数里面,如setTimeout,则先执行同步模块,在执行异步,最终执行 then内的 reject resolve 函数,关于执行顺序的,可参考《弄懂javascript的执行机制:事件轮询|微任务和宏任务》
reject resolve不管以何种形势,只会执行其中的一个。比如reject已经执行了。resolve不再触发。
then 内有reject 函数,则执行reject函数。否则,在catch内捕获
注意:一个promise,只有第一个reject操作失败结果,非Promise链中reject不会影响后面.then()的执行,并且如果reject和catch两种方式同时使用的话,已经reject处理了,catch不再捕获。
promise进阶题
比如:
function doSomething() { return Promise.resolve(1); }; function doSomethingElse() { return Promise.resolve(2); }; // 执行1 doSomething() .then(() => { return doSomethingElse() }) .then(val => console.log('a', val)) // 执行2 doSomething() .then(() => { doSomethingElse() }) .then(val => console.log('b', val)) // 执行3 doSomething() .then(doSomethingElse()) .then(val => console.log('c', val)) // 执行4 doSomething() .then(doSomethingElse) .then(val => console.log('d', val))
又比如
new Promise(resolve => { let resolvedPromise = Promise.resolve() resolve(resolvedPromise) }).then(() => { console.log('resolvePromise resolved') }) Promise.resolve() .then(() => { console.log('promise1') }) .then(() => { console.log('promise2') }) .then(() => { console.log('promise3') })
这个有点难,具体推荐阅读:对不起,你之前学的 promise 可能是错的!(从 ecma 标准看 promise) https://juejin.cn/post/7331996679548747811
Promise.all执行顺序
Promise.all(),怎么按顺序执行?
Promise.all()是并行的,等最慢的执行完后完成,在按照发起请求的先后,结果合并到数组里。
Promise.all 里的任务列表[asyncTask(1),asyncTask(2),asyncTask(3)],是按照顺序发起的。
它们是异步的,互相之间并不阻塞,每个任务完成时机是不确定的,尽管如此,
所有任务结束之后,它们的结果仍然是按顺序地映射到resultList里,这样就能和Promise.all里的任务列表[asyncTask(1),asyncTask(2),asyncTask(3)]一一对应起来。
demo
const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1000); }, 1000); }); const p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve(100); }, 100); }); const p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(500); }, 500); }); Promise.all([p1, p3, p2]).then((results) => { console.log(results); }); Promise.race([p1, p3, p2]).then((results) => { console.log(results); });
如果这个promise队列里出现了reject,那么Promise.all()返回的结果会被一个reject而报销(其他正常返回也没用了)
比如第一个 p1,是reject,就会报错。
const p1 = new Promise((resolve, reject) => { setTimeout(() => { reject(1000); }, 1000); }); const p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve(100); }, 100); }); const p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(500); }, 500); }); Promise.all([p1, p3, p2]).then((results) => { console.log(results); });
所以,第一个需要catch
const p1 = new Promise((resolve, reject) => { setTimeout(() => { reject(1000); }, 1000); }).catch((res) => { console.log(res); });
对我们日常工作中的使用,比如多个请求合并。
import axios from 'axios'; const requests = ['requestData1', 'requestData1']; const promises = requests.map(req => axios.get('url', req).catch((resp) => { // TODO })); Promise.all(promises).then((results) => { results.forEach((res) => { // TODO }); });
Promise.all vs Promise.race
race根据传入的多个Promise实例,只要有一个实例resolve或者reject,就只返回该结果,其他实例不再执行。
const p1 = new Promise((resolve, reject) => { setTimeout(() => { console.log('p1'); resolve(1000); }, 1000); }); const p2 = new Promise((resolve, reject) => { setTimeout(() => { console.log('p2'); resolve(100); }, 100); }); const p3 = new Promise((resolve, reject) => { setTimeout(() => { console.log('p3'); resolve(500); }, 500); }); Promise.race([p1, p3, p2]).then((results) => { console.log(results); });
只会执行最先执行的 p2 resolve
Promise.race vs promise.any
Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。
Promise.all()中的Promise序列会全部执行通过才认为是成功,否则认为是失败;
Promise.race()中的Promise序列中第一个执行完毕的是通过,则认为成功,如果第一个执行完毕的Promise是拒绝,则认为失败;
Promise.any()中的Promise序列只要有一个执行通过,则认为成功,如果全部拒绝,则认为失败;
js原生手工实现promise函数
先看图
function PromiseA(callback) { /** * @property {('pending'|'fulfilled'|'rejected')} states - The state of the promise */ this.status = 'pending';// Promise的状态,初始为'pending' let doneList = []; // 存储then方法中传入的成功回调函数 let failList = [];// 存储then方法中传入的失败回调函数 this.then = function(done, fail) { // then方法,用于添加回调函数 switch (this.status) {// 根据当前Promise的状态,决定如何处理回调函数 case 'pending': // 如果Promise状态为'pending',将回调函数添加到列表中 doneList.push(done); failList.push(fail); break; case 'fulfilled':// 如果Promise状态为'fulfilled',立即执行成功回调函数 done(); return this; case 'rejected': // 如果Promise状态为'rejected',立即执行失败回调函数 fail && fail(); return this; } }; function resolve(result) { // resolve方法,用于将Promise状态改为'fulfilled'并执行成功回调函数 this.status = 'fulfilled'; setTimeout(function() { // 异步执行成功回调函数,避免阻塞主线程 for (let i = 0; i < doneList.length; i++) { let temp = doneList[i](result); if (temp instanceof PromiseA) { // 如果回调函数返回一个Promise,则将该Promise的状态与当前Promise关联 for (i++; i < doneList.length; i++) {// 遍历剩余的成功回调函数和失败回调函数,将它们添加到temp Promise中 temp.then(doneList[i], failList[i]); } } else {// 如果回调函数没有返回Promise,则将结果赋值给result result = temp; } } }, 0); } function reject(error) { // reject方法,用于将Promise状态改为'rejected'并执行失败回调函数 this.status = 'rejected'; setTimeout(function() {// 异步执行失败回调函数,避免阻塞主线程 for (let i = 0; i < failList.length; i++) { let temp = failList[i](error); if (temp instanceof PromiseA) { // 如果回调函数返回一个Promise,则将该Promise的状态与当前Promise关联 temp.then(doneList[i], failList[i]);// 遍历剩余的成功回调函数和失败回调函数,将它们添加到temp Promise中 } else { error = temp; // 如果回调函数没有返回Promise,则将结果赋值给error // 将第一个成功回调函数和失败回调函数从列表中移除 doneList.shift(); failList.shift(); resolve(error); // 将error作为参数调用resolve方法,继续执行Promise链 } } }, 0); } callback(resolve, reject);// 执行callback函数,传入resolve和reject方法 }
再捕获一下异常,封装下:
function PromiseA(callback) { this.status = 'pending'; let doneList = []; let failList = []; this.then = function (done, fail) { switch (this.status) { case 'pending': doneList.push(done); failList.push(fail); break; case 'fulfilled': done && done(); return this; case 'rejected': fail && fail(); return this; } return this; }; const resolve = (result) => { this.status = 'fulfilled'; setTimeout(() => { let value = result; for (let i = 0; i < doneList.length; i++) { let temp = doneList[i](value); if (temp instanceof PromiseA) { temp.then((res) => { value = res; continueChain(i + 1, value, 'fulfilled'); }, reject); break; } else { value = temp; } } }, 0); }; const reject = (error) => { this.status = 'rejected'; setTimeout(() => { let value = error; let temp = failList[0] && failList[0](value); continueChain(1, temp, 'rejected'); }, 0); }; const continueChain = (index, value, status) => { let list = status === 'fulfilled' ? doneList : failList; for (let i = index; i < list.length; i++) { let temp = list[i](value); if (temp instanceof PromiseA) { temp.then((res) => { value = res; continueChain(i + 1, value, status); }, reject); break; } else { value = temp; } } }; try { callback(resolve, reject); } catch (e) { reject(e); } }
promise它只是减少了嵌套,并不能完全消除嵌套;另外,采用Promise的代码看起来依然是异步的。
推荐阅读《Promise简单实现(正常思路版)》《Promises/A+规范》,这里不再赘述。
Promise特别需要注意的是,他的异常处理。
.catch()的作用是捕获Promise的错误,与then()的rejected回调作用几乎一致。但是由于Promise的抛错具有冒泡性质,能够不断传递,这样就能够在下一个catch()中统一处理这些错误。同时catch()也能够捕获then()中抛出的错误,所以建议不要使用then()的rejected回调,而是统一使用catch()来处理错误。推荐阅读:《看这一篇就够了!浅谈ES6的Promise对象》
yield
其实这部分可以忽略,跳到Async/await部分了
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数是一个普通函数,但是有两个特征。
function关键字与函数名之间有一个星号
函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
function* helloWorldGenerator() { yield 'hello';//该函数的状态 yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); console.log(hw.next()); //{ value: 'hello', done: false } //Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。 console.log(hw.next()); //{ value: 'world', done: false } //第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。 console.log(hw.next()); //{ value: 'ending', done: true } console.log(hw.next()); //{ value: undefined, done: true } //第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。 for(let v of helloWorldGenerator()){ console.log(v); // hello world }
yield概念,这里提出来看下:
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下
遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
yield表达式与return语句既有相似之处,也有区别。
相似之处:都能返回紧跟在语句后面的那个表达式的值。
区别在于:每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。
我是不太喜欢yield这个模式的,自认为是一个狗血模式!若果感兴趣,推荐阅读:《从Iterator到Generator与:手搓generator来理解Async/Await风靡前端》
Async/await
async / await是ES7的重要特性之一,也是目前社区里公认的优秀异步解决方案
C#:C#是async/await最早的采用者之一,从.NET 4.5和C# 5.0开始支持这种语法,大大简化了异步编程模型。
Python:Python 3.5引入了async/await关键词,用于简化协程的写法,使得非阻塞IO编程更加直观。
Dart:Dart语言支持async/await,这在开发Flutter应用程序时,用于简化异步和等待操作,尤其是在UI编程和网络请求处理中。
Rust:Rust在2019年引入了异步函数和.await语法,增强了该语言处理异步操作的能力。虽然其方式与async/await有些不同,但是目的和使用的核心概念是类似的。
Kotlin:虽然Kotlin本身不使用async和await这样的关键词,但是通过协程的概念和库提供了相似的功能。Kotlin的协程是其对异步编程的解决方案,与async/await提供的功能非常相似。
Swift:Swift在5.5版本中引入了对异步函数的支持,包括async关键字和await表达式。这使得Swift开发者可以更方便地编写异步代码,尤其是在iOS和macOS的应用开发中。
Async/await建立于Promise之上——async函数的返回值为promise对象。
async用来申明里面包裹的内容可以进行同步的方式执行,await则是进行执行顺序控制,每次执行一个await,程序都会暂停等待await返回值,然后再执行之后的await。
await后面调用的函数需要返回一个promise,另外这个函数是一个普通的函数即可,而不是generator。函数体内的return值,将会作为这个Promise对象resolve时的参数。
await只能用在async函数之中,用在普通函数中会报错。
await命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
推荐阅读《Javascript中的async await》
Async/await之错误处理Error handling
首先推荐读下《ES6 Async/Await 完爆Promise的6个原因》,虽然这个标题就狗血,Promise也并非什么终极解决方案,还有待改善。但是其中的内容页没有必要重写。节选一部分如下:
Async/await使得处理同步+异步错误成为了现实。我们同样使用try/catch结构,但是在promises的情况下,try/catch难以处理在JSON.parse过程中的问题,原因是这个错误发生在Promise内部。想要处理这种情况下的错误,我们只能再嵌套一层try/catch,就像这样:
const makeRequest = () => { try { getJSON() .then(result => { // this parse may fail const data = JSON.parse(result) console.log(data) }) // uncomment this block to handle asynchronous errors // .catch((err) => { // console.log(err) // }) } catch (err) { console.log(err) } }
但是,如果用async/await处理,一切变得简单,解析中的错误也能轻而易举的解决:
const makeRequest = async () => { try { // this parse may fail const data = JSON.parse(await getJSON()) console.log(data) } catch (err) { console.log(err) } }
Async/await项目项目应用
fetch
async function getData () { //await the response of the fetch call let response = await fetch('https://api.github.com/users'); //proceed once the first promise is resolved. let data = await response.json(); //proceed only when the second promise is resolved return data; } //call getData function getData().then(data => console.log(data));//log the data
axios
class App extends React.Component{ constructor(){ super(); this.state = { serverResponse: '' } } componentDidMount(){ this.getData(); } async getData(){ const res = await axios.get('url-to-get-the-data'); const { data } = await res; this.setState({serverResponse: data}) const res = await axios.post('url-to-post-the-data', {username,password}); } render(){ return( {this.state.serverResponse} ); }}
对于异步函数的嗑叨
其中settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;
promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;
async函数表示函数里面可能会有异步方法,
await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
综合考题
请给出下面代码的输出顺序
async function test1(){ console.log('t1'); await test2(); console.log('t2') } async function test2(){ console.log('t3') } console.log('t4'); test1(); new Promise((resolve)=>{ console.log('t5') resolve() }).then(()=>{ console.log('t6') }) console.log('t7')
参考文章:
对Promise.all执行顺序的深入理解 https://zhuanlan.zhihu.com/p/93889764
Promise 源码和分析 https://segmentfault.com/a/1190000023586499
转载本站文章《Javascript异步回调:从手搓promise到function */yield与async/await》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2017_0118_7944.html