一节课彻底弄懂promise、async、await
刚接触js的时候,对于es6的promise、async、await简直怕的要死,甚至有段时间非常害怕promise这个词,随着后面慢慢的接触,觉得这个东西并非那么难了解,主要还是需要弄懂js的少量基础知识。那么接下来,跟上我的思路,一起彻底弄懂promise、async、await。
关于这个系列一共三个比较重要的知识点:
1、关于什么同步、异步,其中涉及了少量堆栈和消息队列、事件轮询的知识;2、关于异步编程的几个处理方案,主要是回调函数和promise;3、关于异步编程的终极处理方案Generator函数以及他的语法糖async、await。假如要弄懂promise,就必需弄懂什么是异步、什么是同步,这篇文章主要是讲一下什么是同步、什么是异步。
js是怎样来的?
任何新语言的出现一定是与他当时的需求有关系的,js全称是Javascript,诞生于1995年(跟我同岁)。最初他的诞生就是为了表单提交的时候做提醒用的,在js问世之前,所有的表单都必需提交到服务端才能校验必填项。
比方你想申请一个qq号,各种信息填了一大堆,提交完才知道,你手机号少输入了一位重新输入,那一定砸电脑的心都有了,这个时候,js出生了,由于是跟客户做实时交互的,所以最早叫livescript,当时为了蹭蹭Java的热度,上户口的时候就改成了Javascript,一不小心长大了可以跟Java平起平坐了。
js为什么是单线程
js从诞生之初就是单线程,那为什么是单线程呢?为了让我们这些菜鸡更容易入门?当然不是。
js主要的用途就是操作DOM,以及与客户的交互,这就决定了他只能是单线程,
比方你这个线程创立了一个DOM,那个线程给删除了,这时候浏览器应该以哪个为准,
所以这个应该永远不会变,你前台发展的能造火箭了,js一定也是单线程的。
一、什么是同步和异步
1、什么是同步呢?
你可以了解为同一个时间,你只能干一件事。今天下班早,你想给女朋友打个电话,女朋友可能跟其余小伙伴一起吃饭呢,
因为手机静音,所以听不到,你就一直打,一直打,啥都没干,把时间都白费了,这就叫同步。由于js是单线程的嘛,所以js从小就是同步的。
来一段代码:function second() { console.log('second')}function first(){ console.log('first') second() console.log('Last')}first()这个很简单,执行打印结果:first、second、last那么js执行这段代码,究竟发生了什么呢?这里面又有一个‘调用栈’的概念
2、调用栈
是不是一听到什么堆栈就害怕,别慌,没那么复杂,你可以了解为一个厕所,大家去上厕所,但是!不是先进先出,而是后进先出。用调用栈的概念,解释一下上面代码的执行顺序:
当执行此代码时,将创立一个全局执行上下文并将其推到调用堆栈的顶部;// 这个不太重要,下面是重点first()函数先上,现在他在顶部;而后打印‘first’,而后执行完了,这个时候这个console.log会自动弹走,就是这个console.log尽管是后进来的,但是他先走了;现在first函数依然在顶部,他下面还有second函数,所以不会弹走;执行second()函数,这时候second函数在顶部;打印‘second’,而后执行完了,弹走这个console.log,这时候second在顶部;这个时候second函数的事儿都干完了,他也弹走了,这时候first函数在顶部;浏览器会问,first你还有事吗,first说我还有一个,执行打印‘last’3、什么是异步呢?
电话没打通,你就给女朋友发了个短信,洗澡去了,你回家了告诉我,(等我洗完了)再打给你,这就是异步。后来为了提高效率,把浏览器的多个内核都用起来,HTML5提出Web Worker标准,允许JavaScript脚本创立多个线程,但是子线程完全受主线程控制,且不得操作DOM。
所以这并没有影响js单线程的本质,js还是每次只能干一件事,只不过把洗澡提前了而已。
来段代码:const getList = () => { setTimeout(() => { console.log('我执行了!'); }, 2000);};console.log('Hello World');getList();console.log('哈哈哈');执行顺序是:Hello World、哈哈哈、我执行了!(两秒以后执行最后一个)这段代码执行,又发生了什么呢?这个地方又有一个‘消息队列’的概念,不慌!
4、消息队列
刚才我们说了,同步的时候,浏览器会维护一个‘执行栈’,除了执行栈,在开启多线程的时候,浏览器还会维护一个消息列表,除了主线程,其他的都是副线程,这些副线程合起来就叫消息列表。
我们用消息列表的概念分析一下上面的代码:
按照执行顺序console.log('Hello World')先执行,浏览器一看,中央军(主线程)!你先过;而后是getlist函数执行,浏览器看到setTimeout,你是八L(副线程)!你先靠边等着;而后是console.log('哈哈哈')执行,中央军(主线程)!你也过;而后浏览器问,还有中央军吗?没了,八L开始过!添加难度:
setTimeout(function() { console.log('我是定时器!');})new Promise(function(resolve) { console.log('我是promise!'); resolve();}).then(function() { console.log('我是then!');})console.log('我是主线程!');执行顺序:我是promise!我是主线程!我是then!我是定时器!为什么promise.then比定时器先执行呢?这个里面又涉及了一个‘事件轮询’的概念。
5、初识事件轮询
上面我们说了,浏览器为了提升效率,为js开启了一个不太一样的多线程,由于js不能同时执行嘛,那副线程(注意是副线程里面哈)里面谁执行,这个选择的过程,即可以了解为事件轮询。我们先用事件轮询的顺序分析一下上面的代码,再来上概念:
promise函数一定首先执行,他是主线程嘛,打印‘我是promise’;而后继续走主线程,打印‘我是主线程’;而后主线程走完了,开始走消息列表;(宏任务和微任务一会再讲)这个时候会先执行promise.then,由于他是微任务,里面的‘我是then!’消息列表里面在上面的是定时器,但是定时器是宏任务,优先级比较低,所以会往后排;6、什么是宏任务?微任务?
**宏任务(Macrotasks):**js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等。**微任务(Microtasks):**promise、process.nextTick(node环境)、Object.observe, MutationObserver等。微任务比宏任务要牛逼一点浏览器执行的顺序:(1)执行主代码块,这个主代码块也是宏任务(2)若遇到Promise,把then之后的内容放进微任务队列(3)遇到setTimeout,把他放到宏任务里面(4)一次宏任务执行完成,检查微任务队列有无任务 (5)有的话执行所有微任务 (6)执行完毕后,开始下一次宏任务。7、那么这个2、3、4、5、6执行的过程就是事件轮询。
在这儿感谢掘金大神的文章,为了表示尊重,挂上地址!
https://juejin.im/post/5e1d0de3e51d455887067f90
上一篇呢,主要是聊了聊同步、异步,他们各自引申出来的‘执行栈’、‘消息队列’,以及‘宏任务’、‘微任务’,假如大家对这几个概念不太理解,可以去这个链接:
https://www.jianshu.com/p/61e7844e68d8
二、回调函数
上面咱们说了,宏任务与微任务都是异步的,其中包括ajax请求、计时器等等,我们初步的理解一下promise,知道他是处理异步的一种方式,那么我们常用的一共有哪几种方法呢?第一种就是回调函数。
先上代码:function f2() { console.log('2222')}function f1(callback){ console.log('111') setTimeout(function () { callback(); }, 5000); console.log('3333')}f1(f2);先看下打印值是:1113333五秒后2222相当于主线程执行完了,会通过回调函数去调用f2函数,这个没什么毛病。但是看下下面的例子:
现在我们读取一个文件,fileReader就是一个异步请求// 这个异步请求就是通过回调函数的方式获取的var reader = new FileReader()var file = input.files[0]reader.readAsText(file, 'utf-8',function(err, data){ if(err){ console.log(err) } else { console.log(data) }})现在看起来也很不错,但是假如文件上传出错了,我们还要在回调里面做判断,要是我们读取完这个文件接着要读取多个文件呢?是不是应该这么写:
读取完文件1之后再接着读取文件2、3var reader = new FileReader()var file = input.files[0]reader.readAsText(file1, 'utf-8',function(err1, data1){ if(err1){ console.log(err1) } else { console.log(data1) } reader.readAsText(file2, 'utf-8',function(err2, data2){ if(err2){ console.log(err2) } else { console.log(data2) } reader.readAsText(file3, 'utf-8',function(err3, data3){ if(err3){ console.log(err3) } else { console.log(data3) } }) })})这么写可以实现需求,但是这个代码的可读性就比较差,看起来就不那么优雅,也就是我们常说的‘回调地狱’。那么怎样破解这种嵌套式的回调呢?ES6为我们提供了promise:
三、promise
首先我们从字面意思上了解一下什么是promise?promise可以翻译成承诺、保证,这个地方你可以了解为:
女朋友让我干了一件事,尽管还没干完,但是我保证这件事会有一个结果给你,成功(fulfiled)或者者失败(rejected),还有一个等待状态(pending)。
还是先上例子let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve(2000) // 成功以后这个resolve会把成功的结果捕捉到 // reject(2000) // 失败以后这个reject会把失败的结果捕捉到 }, 1000) console.log(1111)})promise.then(res => { console.log(res) // then里面第一个参数就能拿到捕捉到的成功结果}, err =>{ console.log(res)// then里面第二个参数就能拿到捕捉到的失败结果})打印结果:11112000(一秒以后)1、then链式操作
Promise对象的then方法返回一个新的Promise对象,因而可以通过链式调用then方法。
then方法接收两个函数作为参数,第一个参数是Promise执行成功时的回调,第二个参数是Promise执行失败时的回调,这个上面的例子说的很明白了,第二个参数捕捉的就是失败的回调。
两个函数只会有一个被调用,这句话怎样了解呢?
女朋友让你去做西红柿鸡蛋汤,你要么就去做,要么就不做,叫外卖,一定没有第三种选择
。
函数的返回值将被用作创立then返回的Promise对象。这句话应该怎样了解呢?还是上例子:
let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve(2000) }, 1000) console.log(1111)})promise.then(res => { console.log(res) // 这个地方会打印捕捉到的2000 return res + 1000 // 这个函数的返回值,返回的就是这个promise对象捕捉到的成功的值}).then(res => { console.log(res) //这个地方打印的就是上一个promise对象return的值})所以打印顺序应该是:111120003000刚才我们看到了then接受两个参数,一个是成功的回调、一个是失败的回调,看起来如同也不是那么优雅,promise里除了then还提供了catch方法:
2、catch捕捉操作
这个catch就是专门捕捉错误的回调的,还是先看例子:
let promise = new Promise((resolve, reject) => { setTimeout(() => { reject(2000) // 失败以后这个reject会把失败的结果捕捉到 }, 1000) console.log(1111)})promise.catch(res => { console.log(res) // catch里面就能拿到捕捉到的失败结果})打印结果:11112000(一秒以后)除了then和catch之外,promise还有两个语法,all和race,我们也简单用一下:
3、all
现在我们有这么一个需求,一共有三个接口A、B、C,必需三个接口都成功以后,才能发起第四个请求,怎样实现呢?
链式调用
let getInfoA = new Promise((resolve, reject) => { console.log('小A开始执行了') resolve()}).then(res => { let getInfoB = new Promise((resolve, reject) => { console.log('小B开始执行了') resolve() }).then(res => { let getInfoC = new Promise((resolve, reject) => { console.log('小C开始执行了') resolve() }).then(res => { console.log('全都执行完了!') }) })})一层套一层的,如同不是那么优雅
all
let getInfoA = new Promise((resolve, reject) => { console.log('小A开始执行了') resolve()})let getInfoB = new Promise((resolve, reject) => { console.log('小B开始执行了') resolve()})let getInfoC = new Promise((resolve, reject) => { console.log('小C开始执行了') resolve()})Promise.all([getInfoA, getInfoB, getInfoC]).then(res => { console.log('全都执行完了!')})接收一个Promise对象组成的数组作为参数,当这个数组所有的Promise对象状态都变成resolved或者者rejected的时候,它才会去调用then方法。非常完美,非常优雅。
4、race
现在又有一个需求,同样是接口A、B、C,只需有一个响应了,我即可以调接口D,那么怎样实现呢?
传统方式
let getInfoA = new Promise((resolve, reject) => { console.log('小A开始执行了') setTimeout((err => { resolve('小A最快') }),1000)}).then(res =>{ console.log(res)})let getInfoB = new Promise((resolve, reject) => { console.log('小B开始执行了') setTimeout((err => { resolve('小B最快') }),1001)}).then(res =>{ console.log(res)})let getInfoC = new Promise((resolve, reject) => { console.log('小C开始执行了') setTimeout((err => { resolve('小C最快') }),1002)}).then(res =>{ console.log(res)})打印结果小A开始执行了小B开始执行了小C开始执行了小A最快这个方法得写三遍,如同也不是那么优雅,一起来看下race应该怎样写?
race
let getInfoA = new Promise((resolve, reject) => { console.log('小A开始执行了') setTimeout((err => { resolve('小A最快') }),1000)})let getInfoB = new Promise((resolve, reject) => { console.log('小B开始执行了') setTimeout((err => { resolve('小B最快') }),1001)})let getInfoC = new Promise((resolve, reject) => { console.log('小C开始执行了') setTimeout((err => { resolve('小C最快') }),1002)})Promise.race([getInfoA, getInfoB, getInfoC]).then(res => { console.log(res)})打印结果小A开始执行了小B开始执行了小C开始执行了小A最快与Promise.all类似的是,Promise.race都是以一个Promise对象组成的数组作为参数,不同的是,只需当数组中的其中一个Promsie状态变成resolved或者者rejected时,即可以调用.then方法了。
promise是ES6用来处理异步的一个方法,现在用的已经比较广泛了,像我们经常用的axios,他就是用promise封装的,用起来非常方便。
之前聊了异步编程的回调函数和promise,用promise处理异步编程,假如多个调用,就会看起来不那么舒服。
es6除了提供了promise还为我们提供了更增强大的async和await,async、await是Generator函数的语法糖,假如想要完全掌握async、await的用法,必需要掌握Generator函数的使用。
四、Generator 函数
1、什么是 Generator 函数?
来自阮一峰老师文档上的解释:Generator函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
你可以这么了解,这个函数自己执行不了,得让别人帮忙执行,踢一脚(next()),走一步。
基本的用法:
function* doSomething() { yield '吃饭' return '睡觉'}let newDoSomething = doSomething() // 自己执行不了,需要指向一个状态机console.log(newDoSomething.next()) // {value: "吃饭", done: false}console.log(newDoSomething.next()) // {value: "睡觉", done: true}从上面的例子可以看出来,Generator 函数有四个特点:
1、function后面有个小*,这个地方有两种写法,没啥太大区别;
function* doSomething(){}function *doSomething(){}2、函数里面会有一个yield,把函数截成不同的状态;
一个yield可以截成两个状态,也就需要两个next()触发;3、Generator函数自己不会执行,而是会返回一个遍历器对象;
4、遍历器对象会通过.next()方法依次调用各个状态。
消息传递
Generator函数除了能控制函数分状态的执行,还有一个非常重要的作用就是消息传递,还是上例子:
function *doSomething() { let x = yield 'hhh' console.log(x) return (x * 2)}let newDoSomething = doSomething()console.log(newDoSomething.next(1)) console.log(newDoSomething.next(2)) 打印结果:{value: "hhh", done: false}2{value: 4, done: true}具体分析一下为什么会打印这个: (重点)
//{value: "hhh", done: false}第一个next()是Generator函数的启动器这个时候打印的是yield后面的值重点的一句,yield后面的值并不会赋值给x//2暂停执行的时候,yield表达式处可以接收下一个启动它的next(...)传进来的值你可以了解为:这个时候第二个next传入的参数会把第一个yield的值替换掉 //{value: 4, done: true}这个时候,x被赋值2,所以打印2*2注意几个问题:
第一个next()是用来启动Generator函数的,传值会被忽略,没用
yield的用法和return比较像,你可以当做return来用,假如yield后没值,return undefined
最后一个next()函数,得到的是函数return的值,假如没有,也是undefined
彻底了解了上面的概念,再来看下下面的栗子:
function *doSomething() { let x = yield 'hhh' let y = yield (x + 3) let z = yield (y * 3) return (x * 2)}let newDoSomething = doSomething()console.log(newDoSomething.next(1)) // {value: "hhh", done: false}console.log(newDoSomething.next(2)) // {value: 5, done: false}console.log(newDoSomething.next(100)) // {value: 300, done: false}console.log(newDoSomething.next(1000)) // {value: 4, done: true}还是用上面的思路分析一下:
第一个next(1),传进去的值没用,打印的是yield后的值第二个next(2),这个时候的x已经被赋值为2,所以打印2+3第三个next(100),这个时候的y已经被赋值为100,所以打印100*3第四个next(1000),这个时候y已经被赋值为1000,,但是打印的是x*2,所以打印的4 再来看个特殊的情况:(特殊的才是容易掉坑的)
function *doSomething() { let x = yield 'hhh' console.log(x) let y = yield (x + 3) console.log(y) let z = yield (y * 3) return (x * 2)}let newDoSomething = doSomething()console.log(newDoSomething.next(1))console.log(newDoSomething.next(2))console.log(newDoSomething.next())console.log(newDoSomething.next())看下打印结果:
{value: "hhh", done: false}2{value: 5, done: false}undefined{value: NaN, done: false}{value: 4, done: true}分析下为什么打印undefined?
1、第一个next(1)传进去的1,没有起任何意义,打印的{value: "hhh", done: false};2、第二个next(2)传进去的2,所以x会打印2,第二个next(2)打印2+3;3、第三个next()传进去的是空,那么y打印的就是未定义,undefined*3那一定就是NaN;4、第四个next()传进去的是空,但是return的是x,刚才说了x是2,那打印的是2*2五、async、await
1、什么是async、await?
async、await是Generator函数的语法糖,原理是通过Generator函数加自动执行器来实现的,这就使得async、await跟普通函数一样了,不用再一直next执行了。
他吸收了Generator函数的优点,可以通过await来把函数分状态执行,但是又不用一直next,可以自动执行。
还是上例子:
栗子1
function f() { return new Promise(resolve =>{ resolve('hhh') })}async function doSomething1(){ let x = await f()}doSomething1()打印结果:hhh看了上面的例子,可以看出async有三个特点:
1、函数前面会加一个async修饰符,来证实这个函数是一个异步函数;2、await 是个运算符,用于组成表达式,它会阻塞后面的代码3、await 假如等到的是 Promise 对象,则得到其 resolve值。栗子2
async function doSomething1(){ let x = await 'hhh' return x}console.log(doSomething1())doSomething1().then(res => { console.log(res)})打印结果:Promise {<pending>}hhh分析一下上面的栗子可以得到这两个特点:
1、async返回的是一个promise对象,函数内部 return 返回的值,会成为 then 方法回调函数的参数;2、await假如等到的不是promise对象,就得到一个表达式的运算结果。2、async、await项目中的使用
现在有一个封装好的,获取数据的方法,我们分别用promise、Generator、async来实现发请求,做一下比较:
function getList() { return new Promise((resolve, reject) =>{ $axios('/pt/getList').then(res => { resolve(res) }, err => { reject(err) }) })}promise
function initTable() { getList().then(res => { console.log(res) }).catch(err => { this.$message(err) // element的语法 })}而后直接调用即可以这么做看起来非常的简洁,但是假如多个请求调用就会是.then,.then看起来非常不舒服Generator函数
function *initTable(args) { const getList = yield getlist(args) return getList}function getList() { const g = initTable(this.searchParams) const gg = g.next().value gg.then(res =>{ this.total = res.data.count if (res.data.list) { this.tableList = res.data.list this.tableList.forEach(e => { e.receiveAmt = format(e.receiveAmt) }) } else { this.tableList = [] } })}这个看起来就比较伤,写起来非常麻烦async await
async initTable() { // table列表查 const getData = await getList(this.searchParams) return getData},getList() { this.initTable().then(res =>{ this.tableList = res.data.list })}这样写如同也很简单,而且非常方便主要是假如调用多个接口,可以直接多个await以上是我个人对promise、async、await的一点见地,有不对的欢迎各位大佬留言或者者加我微信交流。
个人的微信公众号:小Jerry有话说,平常会发少量技术文章和读书笔记,欢迎交流。
后面会持续升级少量js基础的文章,长得好看的哥哥姐姐们可以点个关注。
gzh.jpg
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 一节课彻底弄懂promise、async、await