異步解決方案看他就夠了(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基礎的文章,長得好看的哥哥姐姐們點個關注唄。

相關文章
相關標籤/搜索