前言
2020 秋天,我將用 30 天的時間,來嘗試回答和網路前端開發相關的 30 個問題。30 天無法一網打盡浩瀚的前端知識,有些問題可能對有些讀者來說相對簡單,不過期待這趟旅程,能幫助自己、也幫助讀者打開不同的知識大門。有興趣的話,跟著我一起探索吧!
在昨天討論 iterator 和 generator 的文章當中有提到
async/await 這個語法糖,背後的運作就是透過 generator 和 promise 共同建立起來的
所以今天就來看看 async/await 是如何運作的吧!
在原本的 Promise 當中,我們可以用 then
的方式,來確保前面的動作完成,並把資訊往後傳遞。
譬如今天我們要用透過 axios 從 randomuser 拿到一筆 female user 資料,並進行處理
let users = []
let getUser = axios.get('https://randomuser.me/api/?gender=female')
.then(
res => {
users.push(res.data.results[0])
// .. do something
}
)
如果突然想到要多抓一位 male 的資料,可以繼續用 then
把大家 (Promise) 串在一起,確保依序完成並且傳遞資料,像是
let users = []
let getUser = axios.get('https://randomuser.me/api/?gender=female')
.then(res => {
users.push(res.data.results[0])
return axios.get('https://randomuser.me/api/?gender=male')
})
.then(res => {
users.push(res.data.results[0])
// .. do something with users
})
但是,如果有更多的 Promise 工作需要被完成,這樣不就 then
接到天荒地老。那我可以分開取值嗎?像是:
let user1 = axios.get('https://randomuser.me/api/?gender=female')
.then(res => {
return res.data.results[0]
})
let user2 = axios.get('https://randomuser.me/api/?gender=male')
.then(res => {
return res.data.results[0]
})
// .. do something with user1 & user2
實際上不行,這樣 user1 和 user2 拿到的其實是 Promise 物件,而不是我們預期的 user data。這樣看起來只能繼續在 then
裡面處理資料了。
這時候有人想到,不如我們把所有 task 放進一個 array,然後建立一個 function ,讓它負責用 then
把 array 裡面需要處理的 task 串起來,應該會方便許多喔!
首先,建立 array of tasks
let element = 'https://randomuser.me/api/'
let tasks = [
(element) => {
return axios
.get(element + '?seed=abc')
.then(res => {
return res.data.results[0].name.first
})
},
(element) => {
return axios
.get(element + '?seed=edf')
.then(res => {
return res.data.results[0].name.first
})
},
(element) => {
return axios
.get(element + '?seed=ghi')
.then(res => {
return res.data.results[0].name.first
})
},
]
然後,建立一個 function,可以遍歷 array 當中的所有 task,並依序用 then
把所有 task 串再一起,並不斷地將資料傳遞下去
function chainPromise(element, tasks) {
var returnValue = ''
var p = tasks[0](element)
for(let i = 1; i < tasks.length; i++) {
p = p.then(function(value) {
returnValue += (' ' + value)
return tasks[i](element)
})
}
return p.catch(function(e) {
console.log(e)
}).then(function(value) {
returnValue += (' ' + value)
return returnValue
})
}
這樣就可以避免寫 then
寫到天荒地老的狀況。但是這樣看起來還是有點麻煩,於是,async/await 就這樣誕生了。
要能夠滿足任務,就需要確保幾件事情
這時候我們就想到了 generator 和 promise!
Generator 可以一步步的迭代所有需要處理的 task,並能夠進行進程控制,像是傳入參數。Promise 則可以確保非同步的任務完成。
所以兩個加起來,其實就可以完成上面提到的三個目標!
所以一步一步來的話,首先是建立 generator
function* gen() {
const result1 = yield task1()
const result2 = yield task2(result1)
const result3 = yield task3(result2)
return result3
}
這裡有三個 task 分別為 task1, task2, task3。當每次呼叫執行下一個 task 的時候,會把上一個 task 的結果傳進去。
接著,我們需要一個 runner (generator runner) 來操作這個 generator,也就是透過它來管理 generator 的進程,像是下面這樣:
function runner(gen) {
const it = gen()
function run(arg) {
const result = it.next(arg)
if (result.done) {
return result.value
} else {
return Promise.reslove(result.value).then(run)
}
}
return run()
}
把 generator (gen) 傳入之後,先呼叫 next
並傳入參數(如果有的話),這時 run
function 內會接收回傳值 result,如果 result.done 為 true,就會結束並回傳 result.value;如果 result.done 為 false,那麼就會繼續執行 run
function。
原理就先暫時講到這裡。讓我們來直接看一下 async/await 實際的程式碼為何。
這裡有個使用 async/await 所建立的 function
async function foo () {
let result1 = await bar1()
let result2 = await bar2()
return 'ALL DONE'
}
如果轉成 ES5 之前的寫法,就會是
// ES5
let foo = (() => {
var ref = _asyncToGenerator(function* () {
let result1 = yield bar1();
let result2 = yield bar2();
return 'ALL DONE'
});
return function foo() {
return ref.apply(this, arguments);
};
})();
function _asyncToGenerator(fn) {
return function () {
var gen = fn.apply(this, arguments);
return new Promise(
function (resolve, reject) {
function step(key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
}
catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
return Promise.resolve(value)
.then(function (value) {
return step("next", value);
}, function (err) {
return step("throw", err);
});
}
}
return step("next");
});
};
}
這裡的 runner function 就是 _asyncToGenerator,而 run
function 就是 step(key, arg) ,跟前面比較不一樣的是,run
function 被包在一個 Promise 裡面。
return step("next")
這邊被呼叫,並帶入叫 “next” 的 keyvar info = gen[key](arg)
其實就是我們之前看到的 it.next(arg) 。而取出的 value,就是從 yield 傳入的 bar1()return step("next", value)
{done: true}
,也就是說,會執行 if (info.done) { resolve(value)}
這段程式碼,離開 Promise。此時的 value,就是最後傳過來的 "ALL DONE"。在解決問題的過程當中,會發現工具也不斷的在進化,但其實要解決問題的本質是沒有變的,若能夠了解工具底層的運作,就能夠更接近問題的本質,以及工具發明者的想法喔!
TD
Be curious as astronomer, think as physicist, hack as engineer, fight as baseball player"Life is like riding a bicycle. To keep your balance, you must keep moving."