剛接觸js的時候,對於es6的promise、async、await簡直怕的要死,甚至有段時間很是懼怕promise這個詞,隨着後面慢慢的接觸,以爲這個東西並不是那麼難理解,主要仍是須要弄懂js的一些基礎知識。那麼接下來,跟上個人思路,一塊兒完全弄懂promise、async、await。前端
關於這個系列一共三個比較重要的知識點:node
一、關於什麼同步、異步,其中涉及了一些堆棧和消息隊列、事件輪詢的知識;
二、關於異步編程的幾個解決方案,主要是回調函數和promise;
三、關於異步編程的終極解決方案Generator函數以及他的語法糖async、await。
複製代碼
若是要弄懂promise,就必須弄懂什麼是異步、什麼是同步,這篇文章主要是講一下什麼是同步、什麼是異步。ios
任何新語言的出現確定是與他當時的需求有關係的,js全稱是Javascript,誕生於1995年(跟我同歲)。最初他的誕生就是爲了表單提交的時候作提示用的,在js問世以前,全部的表單都必須提交到服務端才能校驗必填項。es6
好比你想申請一個qq號,各類信息填了一大堆,提交完才知道,你手機號少輸入了一位從新輸入,那確定砸電腦的心都有了,這個時候,js出生了,由於是跟用戶作實時交互的,因此最先叫livescript,當時爲了蹭蹭Java的熱度,上戶口的時候就改爲了Javascript,一不當心長大了能夠跟Java分庭抗禮了。ajax
js從誕生之初就是單線程,那爲何是單線程呢?爲了讓咱們這些菜雞更容易入門?固然不是。編程
js主要的用途就是操做DOM,以及與用戶的交互,這就決定了他只能是單線程, 好比你這個線程建立了一個DOM,那個線程給刪除了,這時候瀏覽器應該以哪一個爲準, 因此這個應該永遠不會變,你前端發展的能造火箭了,js確定也是單線程的。axios
你能夠理解爲同一個時間,你只能幹一件事。今天下班早,你想給女友打個電話,女友可能跟其餘小夥伴一塊兒吃飯呢, 因爲手機靜音,因此聽不到,你就一直打,一直打,啥都沒幹,把時間都浪費了,這就叫同步。由於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)執行完畢後,開始下一次宏任務。
複製代碼
在這兒感謝掘金大神的文章,爲了表示尊重,掛上地址!
上面我們說了,宏任務與微任務都是異步的,其中包括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:
首先咱們從字面意思上理解一下什麼是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(一秒之後)
複製代碼
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就是專門捕捉錯誤的回調的,仍是先看例子:
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,咱們也簡單用一下:
如今咱們有這麼一個需求,一共有三個接口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('全都執行完了!')
})
})
})
複製代碼
一層套一層的,好像不是那麼優雅
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方法。很是完美,很是優雅。
如今又有一個需求,一樣是接口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應該怎麼寫?
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,用promise解決異步編程,若是多個調用,就會看起來不那麼舒服。
es6除了提供了promise還爲咱們提供了更增強大的async和await,async、await是Generator函數的語法糖,若是想要徹底掌握async、await的用法,必需要掌握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}
複製代碼
一、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
複製代碼
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
複製代碼
async、await是Generator函數的語法糖,原理是經過Generator函數加自動執行器來實現的,這就使得async、await跟普通函數同樣了,不用再一直next執行了。
他吸取了Generator函數的優勢,能夠經過await來把函數分狀態執行,可是又不用一直next,能夠自動執行。
仍是上例子:
function f() {
return new Promise(resolve =>{
resolve('hhh')
})
}
async function doSomething1(){
let x = await f()
}
doSomething1()
打印結果:
hhh
複製代碼
看了上面的例子,能夠看出async有三個特色:
一、函數前面會加一個async修飾符,來證實這個函數是一個異步函數;
二、await 是個運算符,用於組成表達式,它會阻塞後面的代碼
三、await 若是等到的是 Promise 對象,則獲得其 resolve值。
複製代碼
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對象,就獲得一個表達式的運算結果。
複製代碼
如今有一個封裝好的,獲取數據的方法,咱們分別用promise、Generator、async來實現發請求,作一下比較:
function getList() {
return new Promise((resolve, reject) =>{
$axios('/pt/getList').then(res => {
resolve(res)
}, err => {
reject(err)
})
})
}
複製代碼
function initTable() {
getList().then(res => {
console.log(res)
}).catch(err => {
this.$message(err) // element的語法
})
}
而後直接調用就能夠
這麼作看起來很是的簡潔,可是若是多個請求調用
就會是.then,.then看起來很是不舒服
複製代碼
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 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基礎的文章,長得好看的哥哥姐姐們點個關注唄。