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