Async Javascript
& callback hell
想像一下
有隻程式會去讀取系統裡的 TODO LIST
然後會根據裡面的 userId 查詢用戶的 email
最後發送提醒的電子郵件給使用者
/**
* @param {string} response
* @param {function(object)} callback
*/
function dataFormat(response, callback) {
// very very complex
callback(JSON.parse(response));
}
有一隻 異步的程式
負責處理 response
轉換成我們需要的格式
/**
* @param {string} email
* @param {function(isSuccess)} callback
*/
function sendRemind(email, callback){
callback(true)
}
email 提醒使用者
你會收到寄信的結果 (成功失敗)
接著看完整的範例
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
事件監聽
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
等待回應
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
資料轉換
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
取 User Data
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
等待回應
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
資料轉換
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
寄信
$btn.addEventListener('click', function() {
const xhr = new XMLHttpRequest();
xhr.open('GET', TODO_URL);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('send email ok');
}
})
});
}
xhr.send();
});
}
xhr.send();
});
這個情況就叫 callback hell
還有人特地用它註冊了一個網站 calbackhell.com
當你的異步 (async) 呼叫愈多程式碼會愈難閱讀
要怎麼擺平它 ?
擺平它 (flatten)
var cbChain = [];
var thenObj = {
/**
* @param {function} cb
*/
then: function (cb) {
cbChain.push(cb);
return thenObj;
}
}
先看看下面這個結構
當 thenObj.then 被呼叫時
會把 callback 放進 cbChain[ ]
然後回傳另一個 thenObj
function ajaxGet(url) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function () {
cbChain.shift()(xhr.response);
}
xhr.send();
return thenObj;
}
回頭改寫一下 XMLHttpRequest
當成功取得資料時
把 response 帶給 callback chain 中的第一個 callback
$btn.addEventListener('click', function() {
ajasGet(TODO_URL)
.then(console.log)
// string
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": false
// }
});
簡化一下原本的範例,先來取得第一筆資料
function thenDataFormat(response) {
const rs = cbChain.shift()(JSON.parse(response));
let nextCb = cbChain.shift();
if (nextCb) nextCb(rs);
return thenObj;
}
然後修改我們的 data formatter
當它被呼叫時會從 callback chain 取一個出來呼叫
為了不打斷 "then" chain,記得回傳 thenObj
function thenDataFormat(response) {
const rs = cbChain.shift()(JSON.parse(response));
let nextCb = cbChain.shift();
if (nextCb) nextCb(rs);
return thenObj;
}
然後修改我們的 data formatter
再來檢查 callback chain
把轉換結果傳給下一個人
$btn.addEventListener('click', function() {
ajasGet(TODO_URL)
.then(thenDataFormat)
.then(console.log)
// Object
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": false
// }
});
串起來
經過複雜的格式轉換後,response 被轉成物件了
function thenSendEmail(email) {
// 模擬寄送延遲
setTimeout(() => {
const isSuccess = true;
const rs = cbChain.shift()(isSuccess);
let nextCb = cbChain.shift();
if (nextCb) nextCb(rs);
}, 1000);
return thenObj;
}
原本的 sendEmail 變成這樣
$btn.addEventListener('click', function () {
ajaxGet(TODO_URL)
.then(thenDataFormat)
.then(function (data) {
return USER_URL + data.userId;
})
.then(ajaxGet)
.then(thenDataFormat)
.then(function (data) {
console.log('email is: ' + data.email);
return data.email
})
.then(thenSendEmail)
.then(function (isSuccess) {
const msg = isSuccess ? 'success' : 'fail';
console.log('send email ' + msg);
})
});
最終我們的程式碼變成這樣
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
const xhr = new XMLHttpRequest();
xhr.open('GET', USER_URL + data.userId);
xhr.onload = function() {
dataFormat(xhr.response, function(data) {
sendRemind(data.email, function(isSuccess) {
if (isSuccess) {
console.log('ok');
}
})
});
}
xhr.send();
});
}
ajaxGet(TODO_URL)
.then(thenDataFormat)
.then(function (data) {
return USER_URL + data.userId;
})
.then(ajaxGet)
.then(thenDataFormat)
.then(function (data) {
console.log('email is: ' + data.email);
return data.email
})
.then(thenSendEmail)
.then(function (isSuccess) {
const msg = isSuccess ? 'success' : 'fail';
console.log('send email ' + msg);
})
Before
After
把巢狀結構改成像是一步步執行的結構稱為 Flatten
在 中這種方式解決 callback hell 是最常見的
ES5
var jqxhr = $.ajax( "example.php" )
.done(function() {
alert( "success" );
})
.fail(function() {
alert( "error" );
})
.always(function() {
alert( "complete" );
});
這個是 Jquery 中的 XMLHttpRequest
在新版的 ECMAScript 我們有其它種作法
ES6 (ES 2015)
-
Promise
-
Generator
ES8 (ES 2017)
-
Async、Await
Promise 是一個容器
裡面保存著未來才會結束的事件 (async)
new Promise( /* executor */ function(resolve, reject) { ... } );
用 new 建立 Promise 物件
executor 會收到兩個參數函式 reslove、 reject
成功時呼叫 reslove
失敗呼叫 reject
Promise 有三種狀態
- pending 未決定
- fulfilled 履行、完成
- rejected 拒絕
通常是異步操作的結果改變 Promise 的狀態
一旦狀態改變(承諾)就不會再變,這也是 Promise 的名稱由來
Promise.prototype.catch(onRejected)
Promise.prototype.then(onFulfilled[, onRejected])
catch 處理發生錯誤時的回呼函式
then 第一個參數對應 resolved 狀態,第二個參數對應 rejected
由於then 以及 catch 方法都回傳 promise,它們可以被串接。
Promise.all([p1, p2, p3 ...])
把多個 promise 包裝成新的 promise
const p = Promise.all([p1, p2, p3]);
全部都 fulfilled,p 才 fulfilled
一個 rejected, p 就 rejected
Promise.race([p1, p2, p3 ...])
race 一樣是多個 promise 包裝成新的 promise
const p = Promise.all([p1, p2, p3]);
跟 all 不同的是,只要有一個 promise 的狀態被改變 p 就會被改變
接著回頭來修改我們的 TODO remind
function ajaxGet(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
resolve(xhr.response);
}
xhr.onerror = () => {
reject(
xhr.responseText ||
new Error('some error happen')
);
}
xhr.send();
});
}
/**
* @param {string} response
*/
function dataFormat(response) {
return new Promise((resolve, reject) => {
try {
resolve(JSON.parse(response));
} catch (error) {
reject(error);
}
});
}
/**
* @param {string} email
*/
function sendRemind(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
}
串起來
$btn.addEventListener('click', function () {
ajaxGet(TODO_URL)
.then(dataFormat)
.then(data => USER_URL + data.userId)
.then(ajaxGet)
.then(dataFormat)
.then(data => data.email)
.then(sendRemind)
.then(function () {
console.log('send email success');
})
.catch(error => {
console.log('some error happen', error);
});
});
Generator 是一個可以迭代的 function
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
函數用 * modifier 就變成 Generator
當你呼叫這個函數時會得到 Generator 物件
每次呼叫 next() 時遇到 yield 就會暫停
function* gen(){
var gotSomething = yield 1
console.log(gotSomething);
}
var g = gen();
g.next(); // {done: false, value: 1}
g.next('hello generator'); // {done: true, value: undefined};
// "hello generator"
next() 可以帶參數,它會被當作前一個 yield expression 的回傳值。
Generator.prototype.throw()
在函式外拋出錯誤讓函式內捕獲
var gen = function* () {
try {
yield;
} catch (e) {
console.log('inner-cache', e);
}
};
var g = gen();
g.next(); // {value: undefined, done: false}
try {
g.throw('a');
g.throw('b');
} catch (e) {
console.log('outer-cache', e);
}
// inner-cache a
// outer-cache' b
Generator.prototype.return()
結束 Iterator 並返回指定值
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
yield* expression
function* genA(){
yield 'A1'
yield* genB();
}
function* genB(){
yield 'B1'
}
var g = genA();
[...g]; // ["A1, B1"]
在 Generator 中呼叫另一個 Generator
接著回頭來修改我們的 TODO remind
function co(gen) {
var g = gen();
function next(err, data) {
var res;
if(err) {
return g.throw(err);
} else {
res = g.next(data);
}
if(!res.done) {
res.value(next)
}
}
next();
}
co() 會幫我們控制 generator 的進行
它負責檢查是否結束
傳遞錯誤、把上一次的值帶改下一個 yield
function ajaxGet(url) {
return function(notify) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
notify(null, xhr.response);
}
xhr.onerror = () => {
notify(
xhr.responseText ||
new Error('some error'), null);
}
xhr.send();
}
}
/**
* @param {string} response
*/
function dataFormat(response) {
return function(notify) {
try {
const data = JSON.parse(response);
notify(null, data);
} catch (error) {
notify(error, null);
}
}
}
/**
* @param {string} email
*/
function sendRemind(email) {
return function(notify) {
setTimeout(() => {
console.log('send email...' + email);
notify(null, true);
}, 1000);
}
}
串起來
$btn.addEventListener('click', function() {
function* remindTODO() {
try {
let rs;
rs = yield ajaxGet(TODO_URL);
rs = yield dataFormat(rs);
rs = yield (data => (notify) => notify(null, USER_URL + data.userId))(rs);
rs = yield ajaxGet(rs);
rs = yield dataFormat(rs);
rs = yield (data => (notify) => notify(null, data.email))(rs);
rs = yield sendRemind(rs);
} catch (e) {
console.error('ohoh!', e);
}
}
co(remindTODO);
});
Asnyc / Await
算是 Generator 的 Syntactic sugar,但它們使異步操更方便
function* gen(){
const rs1 = yield 1;
const rs2 = yield 2;
console.log(rs1, rs2);
}
const g = gen();
g.next(g.next(g.next().value).value)
// 1, 2
async function gen(){
const rs1 = await 1;
const rs2 = await 2;
console.log(rs1, rs2);
}
gen(); // 1, 2
Generator
Async /Await
比較後發現,只是把 * 換成 async
yield 換成 await
相比 Generator, Async / Await 多了
- 內建執行器,我們不用像前面的範例寫 co() 來幫助執行
- 回傳值總是一個 Promise,代表你可以用 .then() 串接呼叫
await sendEmail function(){
const user = await getUserData(id);
const result = await sendEmail(user.email);
return result;
}
sendEmail.then(result => console.log(result));
async function foo() {
return 'hello world';
}
foo().then(v => console.log(v))
// "hello world"
async function foo() {
await Promise.reject('壞掉了');
}
foo()
.then(v => console.log(v))
.catch(e => console.log(e))
// "壞掉了"
其它範例
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
// after 1 sec
// 1000
await with thenable object
接著回頭來修改我們的 TODO remind
// 裡面的 function 直接使用 Promise 部份介紹的
$btn.addEventListener('click', function () {
async function remindTODO() {
const response = await ajaxGet(TODO_URL);
const todoContent = await dataFormat(response);
const responseForUser = await ajaxGet(USER_URL + todoContent.id);
const userData = await dataFormat(responseForUser);
const result = await sendRemind(userData.email);
return result;
}
remindTODO()
.then(rs => {
console.log('success');
})
.catch(e => {
console.error(e);
})
});
END
Quiz
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
Q1. 請寫出下面程式執行後 log 的結果
function delay(callback) {
setTimeout(() => {
callback();
}, 1000);
}
delay(function () {
console.log('start');
delay(function () {
console.log(2);
delay(function () {
console.log(3);
delay(function () {
console.log(4);
delay(function () {
console.log(5);
delay(function () {
console.log('end');
});
});
});
});
});
});
Q2. 請把下面的程式用 Promise 或 Async/Await 改寫
function foo() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('foo resolve');
resolve();
}
, 1000);
});
}
function bar() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('bar resolve');
resolve();
}
, 1000);
});
}
(async ()=>{
await foo();
await bar();
})()
Q3. foo 與 bar 是可以獨立運行互不干擾的程式,現在 bar 必須等待 foo 執行完才會執行,請修改成並行
async javascript
By mangogan
async javascript
- 664