异步解决方案看他就够了(promise、async)(1.1万字)

刚接触js的时候,对于es6的promise、async、await简直怕的要死,甚至有段时间很是惧怕promise这个词,随着后面慢慢的接触,以为这个东西并不是那么难理解,主要仍是须要弄懂js的一些基础知识。那么接下来,跟上个人思路,一块儿完全弄懂promise、async、await。前端

关于这个系列一共三个比较重要的知识点:node

一、关于什么同步、异步,其中涉及了一些堆栈和消息队列、事件轮询的知识;

二、关于异步编程的几个解决方案,主要是回调函数和promise;

三、关于异步编程的终极解决方案Generator函数以及他的语法糖async、await。
复制代码

若是要弄懂promise,就必须弄懂什么是异步、什么是同步,这篇文章主要是讲一下什么是同步、什么是异步。ios

js是怎么来的?

任何新语言的出现确定是与他当时的需求有关系的,js全称是Javascript,诞生于1995年(跟我同岁)。最初他的诞生就是为了表单提交的时候作提示用的,在js问世以前,全部的表单都必须提交到服务端才能校验必填项。es6

好比你想申请一个qq号,各类信息填了一大堆,提交完才知道,你手机号少输入了一位从新输入,那确定砸电脑的心都有了,这个时候,js出生了,由于是跟用户作实时交互的,因此最先叫livescript,当时为了蹭蹭Java的热度,上户口的时候就改为了Javascript,一不当心长大了能够跟Java分庭抗礼了。ajax

js为何是单线程

js从诞生之初就是单线程,那为何是单线程呢?为了让咱们这些菜鸡更容易入门?固然不是。编程

js主要的用途就是操做DOM,以及与用户的交互,这就决定了他只能是单线程, 好比你这个线程建立了一个DOM,那个线程给删除了,这时候浏览器应该以哪一个为准, 因此这个应该永远不会变,你前端发展的能造火箭了,js确定也是单线程的。axios

1、什么是同步和异步

一、什么是同步呢?

你能够理解为同一个时间,你只能干一件事。今天下班早,你想给女友打个电话,女友可能跟其余小伙伴一块儿吃饭呢, 因为手机静音,因此听不到,你就一直打,一直打,啥都没干,把时间都浪费了,这就叫同步。由于js是单线程的嘛,因此js从小就是同步的。数组

来一段代码:
function second() {
    console.log('second')
}
function first(){
    console.log('first')
    second()
    console.log('Last')
}
first()

这个很简单,执行打印结果:
first、second、last
复制代码

那么js执行这段代码,到底发生了什么呢?这里面又有一个‘调用栈’的概念promise

二、调用栈

是否是一听到什么堆栈就惧怕,别慌,没那么复杂,你能够理解为一个厕所,你们去上厕所,可是!不是先进先出,而是后进先出。用调用栈的概念,解释一下上面代码的执行顺序:浏览器

当执行此代码时,将建立一个全局执行上下文并将其推到调用堆栈的顶部;// 这个不过重要,下面是重点
first()函数先上,如今他在顶部;
而后打印‘first’,而后执行完了,这个时候这个console.log会自动弹走,就是这个console.log虽然是后进来的,可是他先走了;
如今first函数仍然在顶部,他下面还有second函数,因此不会弹走;
执行second()函数,这时候second函数在顶部;
打印‘second’,而后执行完了,弹走这个console.log,这时候second在顶部;
这个时候second函数的事儿都干完了,他也弹走了,这时候first函数在顶部;
浏览器会问,first你还有事吗,first说我还有一个,执行打印‘last’
复制代码

三、什么是异步呢?

电话没打通,你就给女友发了个短信,洗澡去了,你回家了告诉我,(等我洗完了)再打给你,这就是异步。后来为了提升效率,把浏览器的多个内核都用起来,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。

因此这并无影响js单线程的本质,js仍是每次只能干一件事,只不过把洗澡提早了而已。

来段代码:
const getList = () => {
    setTimeout(() => {
        console.log('我执行了!');
    }, 2000);
};
console.log('Hello World');
getList();
console.log('哈哈哈');

执行顺序是:
Hello World、哈哈哈、我执行了!(两秒之后执行最后一个)
复制代码

这段代码执行,又发生了什么呢?这个地方又有一个‘消息队列’的概念,不慌!

四、消息队列

刚才咱们说了,同步的时候,浏览器会维护一个‘执行栈’,除了执行栈,在开启多线程的时候,浏览器还会维护一个消息列表,除了主线程,其他的都是副线程,这些副线程合起来就叫消息列表。

咱们用消息列表的概念分析一下上面的代码:

按照执行顺序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比定时器先执行呢?这个里面又涉及了一个‘事件轮询’的概念。

五、初识事件轮询

上面咱们说了,浏览器为了提高效率,为js开启了一个不太同样的多线程,由于js不能同时执行嘛,那副线程(注意是副线程里面哈)里面谁执行,这个选择的过程,就能够理解为事件轮询。咱们先用事件轮询的顺序分析一下上面的代码,再来上概念:

promise函数确定首先执行,他是主线程嘛,打印‘我是promise’;
而后继续走主线程,打印‘我是主线程’;
而后主线程走完了,开始走消息列表;
(宏任务和微任务一会再讲)
这个时候会先执行promise.then,由于他是微任务,里面的‘我是then!’
消息列表里面在上面的是定时器,可是定时器是宏任务,优先级比较低,因此会日后排;
复制代码

六、什么是宏任务?微任务?

**宏任务(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)执行完毕后,开始下一次宏任务。
复制代码

七、那么这个二、三、四、五、6执行的过程就是事件轮询。

在这儿感谢掘金大神的文章,为了表示尊重,挂上地址!

juejin.cn/post/684490…

2、回调函数

上面我们说了,宏任务与微任务都是异步的,其中包括ajax请求、计时器等等,咱们初步的了解一下promise,知道他是解决异步的一种方式,那么咱们经常使用的一共有哪几种方法呢?第一种就是回调函数。

先上代码:
function f2() {
    console.log('2222')
}
function f1(callback){
    console.log('111')
  setTimeout(function () {
    callback(); 
  }, 5000);
  console.log('3333')
}
f1(f2);

先看下打印值是:
111
3333
五秒后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以后再接着读取文件二、3

var 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:

3、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(err)// then里面第二个参数就能拿到捕捉到的失败结果
})

打印结果:

1111
2000(一秒之后)
复制代码

一、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的值
})

因此打印顺序应该是:

1111
2000
3000
复制代码

刚才咱们看到了then接受两个参数,一个是成功的回调、一个是失败的回调,看起来好像也不是那么优雅,promise里除了then还提供了catch方法:

二、catch捕捉操做

这个catch就是专门捕捉错误的回调的,仍是先看例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2000) // 失败之后这个reject会把失败的结果捕捉到
    }, 1000)
    console.log(1111)
})
promise.catch(res => {
    console.log(res) // catch里面就能拿到捕捉到的失败结果
})

打印结果:

1111
2000(一秒之后)
复制代码

除了then和catch以外,promise还有两个语法,all和race,咱们也简单用一下:

三、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方法。很是完美,很是优雅。

四、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函数的使用。

4、Generator 函数

一、什么是 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 函数有四个特色:

一、function后面有个小*,这个地方有两种写法,没啥太大区别;

function* doSomething(){}
function *doSomething(){}
复制代码

二、函数里面会有一个yield,把函数截成不一样的状态;

一个yield能够截成两个状态,也就须要两个next()触发;
复制代码

三、Generator函数本身不会执行,而是会返回一个遍历器对象;

四、遍历器对象会经过.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?

一、第一个next(1)传进去的1,没有起任何意义,打印的{value: "hhh", done: false};
二、第二个next(2)传进去的2,因此x会打印2,第二个next(2)打印2+3;
三、第三个next()传进去的是空,那么y打印的就是未定义,undefined*3那确定就是NaN;
四、第四个next()传进去的是空,可是return的是x,刚才说了x是2,那打印的是2*2
复制代码

5、async、await

一、什么是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有三个特色:

一、函数前面会加一个async修饰符,来证实这个函数是一个异步函数;

二、await 是个运算符,用于组成表达式,它会阻塞后面的代码

三、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
复制代码

分析一下上面的栗子能够获得这两个特色:

一、async返回的是一个promise对象,函数内部 return 返回的值,会成为 then 方法回调函数的参数;

二、await若是等到的不是promise对象,就获得一个表达式的运算结果。
复制代码

二、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基础的文章,长得好看的哥哥姐姐们点个关注呗。