引言
接觸過Ajax請求的會遇到過異步調用的問題,爲了保證調用順序的正確性,一般我們會在回調函數中調用,也有用到一些新的解決方案如Promise
相關的技術。
在異步編程中,還有一種常用的解決方案,它就是Generator
生成器函數。顧名思義,它是一個生成器,它也是一個狀態機,內部擁有值及相關的狀態,生成器返回一個迭代器Iterator
對象,我們可以通過這個迭代器,手動地遍歷相關的值、狀態,保證正確的執行順序。
Iterator接口
遍歷器(Iterator
)就是這樣一種機制。它是一種接口,爲各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator
接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator的作用
- 爲各種數據結構,提供一個統一的、簡便的訪問接口
- 使得數據結構的成員能夠按某種次序排列
- ES6 創造了一種新的遍歷命令
for...of
循環,Iterator
接口主要供for...of
消費。
Iterator實現
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
原生具備Iterator接口的數據結構
- Array
- Map
- Set
- String
- TypedArray
- 函數的 arguments 對象
- NodeList 對象
查看一下Map
下面的所掛載的Iterator
。
let map = new Map();
console.log(map.__proto__);
輸出結果:
clear:ƒ clear()
constructor:ƒ Map()
delete:ƒ delete()
entries:ƒ entries()
forEach:ƒ forEach()
get:ƒ ()
has:ƒ has()
keys:ƒ keys()
set:ƒ ()
size:(...)
values:ƒ values()
Symbol(Symbol.iterator):ƒ entries()
Symbol(Symbol.toStringTag):"Map"
get size:ƒ size()
__proto__:Object
如何爲Object部署一個Iterator接口
function iteratorObject(obj){
let keys = Object.keys(obj);
let index = -1;
return {
next(){
index++;
return index<keys.length?{
value:obj[keys[index]],
key:keys[index],
done:false
}:{
value:undefined,
key:undefined,
done:true
}
}
}
}
let obj = {a:1,b:2,c:3};
let iter = iteratorObject(obj);
console.log(iter.next());
// {value: 1, key: "a", done: false}
console.log(iter.next());
// {value: 2, key: "b", done: false}
console.log(iter.next());
// {value: 3, key: "c", done: false}
console.log(iter.next());
// {value: undefined, key: undefined, done: true}
通過上面的方法可以簡單的爲Object
部署了一個Iterator
接口。
Generator函數
Generator是ES6的新特性,通過yield
關鍵字,可以讓函數的執行流掛起,那麼便爲改變執行流程提供了可能。
Generator語法
dome:
function * greneratorDome(){
yield "Hello";
yield "World";
return "ending";
}
let grenDome = greneratorDome();
console.log(grenDome)
上面的代碼中定義了一個Generator
函數,獲取到了函數返回的對象。下面是其輸出結果。
原型鏈:
greneratorDome {<suspended>}
__proto__:Generator
__proto__:Generator
constructor:GeneratorFunction {prototype: Generator, constructor: ƒ, Symbol(Symbol.toStringTag): "GeneratorFunction"}
next:ƒ next()
return:ƒ return()
throw:ƒ throw()
Symbol(Symbol.toStringTag):"Generator"
__proto__:Object
[[GeneratorStatus]]:"suspended"
[[GeneratorFunction]]:ƒ* greneratorDome()
[[GeneratorReceiver]]:Window
[[GeneratorLocation]]:test.html:43
[[Scopes]]:Scopes[3]
通過上面的輸出結果可以看的出來,沿着原型鏈向上查找就存在一個next
方法,這個方法與Iterator
接口返回的結果是大同小異的。
繼續延續dome代碼,並使用next
方法向下執行。
function * greneratorDome(){
yield "Hello";
yield "World";
return "Ending";
}
let grenDome = greneratorDome();
console.log(grenDome.next());
// {value: "Hello", done: false}
console.log(grenDome.next());
// {value: "World", done: false}
console.log(grenDome.next());
// {value: "Ending", done: true}
console.log(grenDome.next());
// {value: undefined, done: true}
在最開始的地方有提到過Generator
函數,最後返回的是一個Iterator
對象,這也就不難理解了。
異步的Generator
dome
function a (){
setTimeout(() => {
alert("我是後彈出");
},1000)
}
function b (){
alsert("我是先彈出");
}
function * grenDome (){
yield a();
yield b();
}
let gren = grenDome();
gren.next();
gren.next();
// 輸出結果
// 我是先彈出
// 我是後彈出
結合Promise
function a (){
return new Promise((resolve,reject) => {
setTimeOut(() => {
console.log(1)
resolve("a");
})
})
}
function b (){
return new Promise((resolve,reject) => {
console.log(2)
resolve("b");
})
}
function * grenDome (){
yield a();
yield b();
return new Promise((resolve,reject) => {
resolve("grenDome內部")
})
}
let gren = grenDome();
// console.log(gren.next())
// {value: Promise, done: false}
// console.log(gren.next())
// {value: Promise, done: false}
// console.log(gren.next())
// {value: Promise, done: true}
// console.log(gren.next())
// {value: undefined, done: true}
gren.next().value.then((res) => {
console.log(res);
// a函數
})
gren.next().value.then((res) => {
console.log(res);
// b函數
})
gren.next().value.then((res) => {
console.log(res);
// grenDome內部
})
// 輸出結果
// a
// b
// grenDome內部
在上面的代碼中有一點是需要注意的,在grenDome
函數裏面最後return
出去了一個Promise
,但是在輸出的時候雖然done
屬性已經爲true
但是value
裏面仍然會存有一個promise
對象,實際上done
表示的是對應yield
關鍵字的函數已經遍歷完成了。
Async/Await
Async/await
是Javascript
編寫異步程序的新方法。以往的異步方法無外乎回調函數和Promise
。但是Async/await
建立於Promise
之上,換句話來說使用了Generator
函數做了語法糖。
async
函數就是隧道盡頭的亮光,很多人認爲它是異步操作的終極解決方案。
什麼是Async/Await
async
顧名思義是“異步”的意思,async
用於聲明一個函數是異步的。而await
從字面意思上是“等待”的意思,就是用於等待異步完成。並且await
只能在async
函數中使用。
Async/Await語法
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
};
asyncPrint('hello world',2000);
// 在2000ms之後輸出`hello world`
返回Promse對象
通常async
、await
都是跟隨Promise
一起使用的。爲什麼這麼說呢?因爲async
返回的都是一個Promise
對象同時async
適用於任何類型的函數上。這樣await
得到的就是一個Promise
對象,如果不是Promise
對象的話那async
返回的是什麼就是什麼。
async function f() {
return 'hello world';
}
f().then(v => console.log(v));
// hello world
async
函數返回一個Promise
對象,可以使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。
function a(){
return new Promise((resolve,reject) => {
console.log("a函數")
resolve("a函數")
})
}
function b (){
return new Promise((resolve,reject) => {
console.log("b函數")
resolve("b函數")
})
}
async function dome (){
let A = await a();
let B = await b();
return Promise.resolve([A,B]);
}
dome().then((res) => {
console.log(res);
});
執行機制
前面已經說過await
是等待的意思,之後等前面的代碼執行完成之後纔會繼續向下執行。
function a(){
return new Promise((resolve,reject) => {
resolve("a");
console.log("a:不行")
})
}
function b (){
return new Promise((resolve,reject) => {
resolve("b");
console.log("b:不行");
})
}
async function dome (){
await a();
await b();
console.log("雖然我在後面,但是我想要先執行可以麼?")
}
dome();
// 輸出結果
// a:不行
// b:不行
// 雖然我在後面,但是我想要先執行可以麼?
另外一個列子
function timeout1(ms) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("timeout1")
resolve();
},ms);
});
};
function timeout2(ms) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("timeout2");
resolve();
},ms);
});
};
async function asyncPrint() {
await timeout1(1000);
await timeout2(2000);
};
asyncPrint().then((res) => {
console.log(res);
}).catch((err) => {
console.log(err)
})
// 1s 後輸出timeout1
// 3s 後輸出timeout2
// undefined
async、await錯誤處理
JavaScript異步請求肯定會有請求失敗的情況,上面也說到了async返回的是一個Promise對象。既然是返回一個Promise對象的話那處理當異步請求發生錯誤的時候我們就要處理reject的狀態了。
在Promise中當請求reject的時候我們可以使用catch。爲了保持代碼的健壯性使用async、await的時候我們使用try catch來處理錯誤。
async function catchErr() {
try {
const errRes = await new Promise((resolve, reject) => {
console.log(a)
});
} catch(err) {
console.log(err);
}
}
catchErr();
// ReferenceError: a is not defined
總結
Iterator接口
遍歷器對象除了具有next
方法,還可以具有return
方法和throw
方法。如果你自己寫遍歷器對象生成函數,那麼next
方法是必須部署的,return
方法和throw
方法是否部署是可選的。
Es6
提供很多API
都是基於Iterator
接口,比如解構,for...of循環,拓展運算等。
Generator函數
調用Generator
函數,返回一個遍歷器對象,代表Generator
函數的內部指針。以後每次調用遍歷器對象的next
方法,就會返回一個有着value
和done
兩個屬性的對象。 value
屬性表示當前的內部狀態的值,是yield
語句後面那個表達式的值;done
屬性是一個布爾值,表示是否遍歷結束
Async/Await
Async/await
是近些年來JavaScript
最具革命性的新特性之一。他讓讀者意識到使用Promise
存在的一些問題,並提供了自身來代替Promise
的方案。他使得異步代碼變的不再明顯,我們好不容易已經學會並習慣了使用回調函數或者then
來處理異步。