2012年5月5日 星期六

想寫好 JavaScript,遞迴技能要練好

Standard
V8 JavaScript Engine 的運作,主要是以事件驅動(Event-driven)來執行所有的程式碼,如果你有開發過 GTK+ 程式的經驗,使用上應該不陌生。不過這對一般人其實相當不好理解,因為事件驅動會讓你的程式看起來像是多工在運作(如使用執行緒),所以有些人對於 JavaScript 的理解是,有些東西可以丟背景執行。

要了解事件驅動,可以先從 JavaScript 的行為說起。如果你正在使用 Node.js,你就會發現 Node.js 通常對同樣功能提供了兩個 API,分為『同步(Synchronize)』和『非同步(Asynchronize)』兩種類型。例如下面這刪除特定檔案的例子:

同步(Synchronize) API:
fs.unlinkSync('/home/fred/badfile');
...

非同步(Asynchronize) API:
fs.unlink('/home/fred/badfile', function(err) {
    if (err) {
        console.log('Failed to delete file.');
        return;
    }

    console.log('Successfully delete a file.');
});
...

簡單觀察同步與非同步的執行結果,可以知道前者(同步)會停住並阻塞在此 API,等檔案刪除成功後才繼續下一行程式;而後者(非同步)有如將刪除檔案的工作丟到背景執行,並繼續往下一行程式執行,完全不會像同步 API 一樣停住。等刪除檔案的工作完成後,便觸發事件去呼叫 Callback function。而這樣非同步的機制就全靠事件驅動(Event-driven)來達成,雖看起來像是把程式丟到背景跑,實際上是在 JavaScript Engine 上註冊了一個事件,等到工作完成以後,事件被觸發時才接續執行 callback function。

非同步的做法通常會用在需要花大量時間的 API 上,如檔案系統、資料庫系統的操作等,以避免因為單一 API 造成整個程式鎖死或卡住不會動的問題,尤以前端和需要即時反應的工作上,特別需要。

至於這樣的事件驅動是怎麼達成的?簡單舉例,像多工作業系統一樣,如果你以微觀的視角來看,電腦根本仍只是單工在執行程式,只是每支程式因為分配到的執行時間相當小,對人來說,一晃眼就已經輪流執行了在作業系統『排程器』上的每一支程式,所以看似同時多工。而在 JavaScript Engine 中,其實也是類似的情形,但不太一樣的是,它是以『事件(Event)』而非『時間』為單位,也就是等到當下事件所對應的程式執行結束後,才會切換到下一個被觸發的事件。

就因為 JavaScript Engine 有這樣的特性,所以我們可以肯定的知道有一種問題存在:『如果某個事件處理太久或陷入無窮迴圈,會讓整個 JavaScript Engine 動彈不得』,當在這問題發生時,我們的 JavaScript 程式就會像當機一樣,卡死在那不會動。

所以,在了解 JavaScript Engine 的運作原理後,就知道在開發 JavaScript 程式時,有一些要點必須注意:
  1. 盡可能『完全不使用』同步(Synchronize)類型的 API
  2. 避免使用『大量次數』或『執行期長』的『迴圈(Loop)』和『遞迴(Recursive)』方法
  3. 盡可能讓自己的程式,執行時不阻塞,以免影響到其他事件

不過寫程式難免會碰到處理大量資料,無法避免要花大量時間去處理,所以需要一些手段,利用 JavaScript 的事件引擎來分散消化這些運算。通常的做法有:
  1. 切割資料,採遞回方式分批處理
  2. 將迴圈改成使用遞迴方法
  3. 使用 setTimeout() 或 process.nextTick() 改善遞迴呼叫

你可以看到,其實最終的解決方案都是改成遞迴方法,然後使用 setTimout() 或 process.nextTick() 去註冊事件,讓 JavaScript Engine 在下一次事件觸發時,才繼續處理資料。如此,便可避免事件阻塞,卡死整個程式。

改善後的遞迴方法大致上如下:
function goodRecursive() {
    /* Do Something... */

    /* setTimeout(goodRecursive, 0) */
    process.nextTick(goodRecursive);
}


goodRecusive();

所以說,想寫好 JavaScript ,遞迴技能要練好。 :-)


後記

個人認為,不只是要『盡可能』避免使用同步(Synchronize)的 API,而是『根本不要』去使用它,因為在 JavaScript Engine 中,阻塞所造成的不良效應會比想像中的還要嚴重,你會打亂掉很多其他事件應有的運作。據過去經驗,阻塞效應所導致的周邊問題,非常難以除錯(Debug),甚至是無法追蹤問題所在。