2012年5月22日 星期二

簡單理解 JavaScript 的記憶體管理機制

Standard
不像其他的語言,JavaScript 開發者永遠沒有辦法自己去釋放記憶體,頂多只能移除物件的 Reference (代表這物件已經沒有人在使用),而且這物件所佔的記憶體並不會馬上被釋放,而是 Garbage Collection 在滿足某些條件的情況下,才在背景自動去尋找沒有被使用的物件,然後釋放。若你嘗試過尋找釋放記憶體或移除物件 Reference 的方法,得到的解答,應該不外乎是使用 delete 關鍵字或是將變數設為 null,但是你真的了解它的意義嗎?事實上,有很多人都是在不瞭解的情況下使用它們,還可能因此產生 Memory leaks 的狀況。欲弄清楚這一切,我們必須先簡單理解一下 JavaScript 的記憶體管理機制,更準確的說,是物件的管理機制。

從 JavaScript 開發者角度來看,JavaScript Engine 在運作時,記憶體使用是呈現樹狀結構,也就是所有命名或建立的變數或物件,都是存放在一個全域(global)的 Object 中。在 Node.js 中,你可以直接讀取 global 變數看到目前 Context 的環境狀態:
console.log(global);

我們可以做個實驗理解一下:
var myVar = 'Hello';
function myFunc() {
    return 123;
}
var myObj = {
    a: 1,
    b: 2
};

console.log(global);

執行以上程式,你應該可以從 global 中找到我們自己定義的變數和函式:
{
    ...(已省略基本預設的環境變數)...
    myVar: 'Hello',
    myFunc: [function],
    myObj: {
        a: 1,
        b: 2
    },
    ...
}

從結果可以發現,所有的物件都以樹狀的形式被 global Object 保存著,無論是變數還是任何一種類型的物件,都是一組組 Key/Value 的存在。而 Value 就是各種不同形態的物件,如字串、函數、陣列、數值等。

所以,移除某物件的 Reference,就意味著將把物件從這棵樹上拔除掉。因此,我們可以直接將該變數設為 null:
myVar = null;

由於該變數被設為 null,原本的字串(包含著『Hello』)物件就失去了依附的樹枝,如枯葉般從樹上掉下來,等著 Garbage Collection 來回收它。對於開發者而言,其實就是告訴 GC 我不需要這物件了,隨時可以把這個物件的記憶體釋放。

然而,雖然變數被設為 null 後,原本的物件被釋放了,但該變數還是存在的,別忘了,他是一個在 global Object 中的 Key,現在只是沒有 Value 為 null 而已。要真正把這個變數給刪除,這時就要用到 delete 關鍵字。如果你去查一下 JavaScript 的 API 參考文獻,就會發現 delete 關鍵字其實是拿來刪除 Object 中的一組 Key/Value。因此,既然 JavaScript 所有的變數其實都只是一組存放在 global Object 的 Key/Value,我們理所當然可以用 delete 關鍵字去移除掉他:
delete myVar;

知曉了 JavaScript 的記憶體管理機制後,你就會了解使用 delete 關鍵字和將變數設為 null,其實並不是代表物件就會被釋放,只是砍樹枝去減少物件的 Reference。

此外,如果一個物件有多個 Reference,只是單單刪其中一個也不會讓物件被 GC 釋放:
var myVar = 'Hello';
var myVar1 = myVar;

myVar = null;
delete myVar;

console.log(myVar1);

以上的程式會顯示『Hello』字串,該物件並不會因為失去 myVar 這 Reference 而被 GC 移除。若想要這一個字串被釋放,必需清空物件所有的 Reference(包括 myVar 和 myVar1),才能讓物件具有被 GC 回收的條件。所以,如果你不小心讓一個不明顯的變數勾搭上了物件,然後你忘記了這個變數的存在,很有可能就會造成 Memory Leaks,讓以為已經被釋放的物件,偷偷存活在於記憶體上。

備註:Reference 是常見於各種系統的設計,主要做法是幫物件建立一個 Reference 計數器,當有人關聯或使用到他,就會讓這計數器加一,等到關聯被移除或使用完畢後,就會讓計數器減一。所以,一旦計數器為零時,代表現在沒有任何外部的物件在使用或關聯到它,是可以被釋放掉的狀態。

後記

這是很多人的誤解,在 JavaScript 中,千萬不要傻傻的以為用 delete 關鍵字就可以把物件給釋放了,需要特別注意。:-)

2012年5月19日 星期六

【JSDC.tw 2012 簡報檔釋出】Non-MVC Web Framework

Standard
由於一些因素,被徵招去救火,在 JSDC.tw 2012 (JavaScript Developer Conference 2012) 給與了一場 Talk,由於準備時間相當緊迫,就把最近在研究和試驗的半成品匆匆拿上場展示和說明,題目是『Non-MVC Web Framework』。你可能在議程上沒有看到這場 Talk,實際上是因為主辦單位的一些小疏忽,議程上仍是寫著之前的舊題目『Build Node.js Module』,而且小弟的名字也打錯了,從 Fred 變成了 Frend,差一點就變成二十世紀少年的友民黨教主。:-)

本次議程其實和前幾天發的舊文『前端工程師也可以淡定的開發網站應用!RedTea Web Framework!』息息相關,探討的是改善 MVC 的開發流程,和引入 JavaScript/Node.js 後的大膽嘗試,而且進一步討論實作細節。此外,因為 RedTea 本身只是一個實驗性的專案,目的只在於驗證可行性,還不到實際使用程度,所以藉著這次機會,公開並宣布新專案『Kamalan Web Framework(葛瑪蘭)』,Kamalan 引入了 Express Web Framework 為基礎,然後將結合 RedTea 的成果,並更進一步簡化其中細節和使用方法,讓前端工程師能真正無痛上手。


雖然還沒完全將 RedTea 的機制完全整合進 Kamalan,但目前 Kamalan 最少可以和 Express 一樣使用,你如果有用 Express 寫的程式,直接將 require() 引入的模組名改成 Kamalan 即可。由於 Kamalan 在根本上就是 Express,甚至是可以使用 Express 的所有 Middleware。

後記

這場 Talk 後,引起了一些 JavaScript Developer 趕的迴響,一些建議和聽眾的期待相當令人開心,討論過程也非常愉快。感謝大家的參與。:-)

2012年5月18日 星期五

【GNOME.Asia 2012】Enjoy Writing a Modern Desktop Application in JavaScript

Standard
依稀記得,當年去越南(Vietnam)參加 GNOME.Asia Summit 2009 的情景,當時甚至寫了一篇[遊記心得]。因為那是首次以講者身份參加國外的 Open Source 研討會活動,意義非凡。更重要的是,那也是當時和 Penk 組成的 ULLab(/usr/local/lab) 第一次出征,如今這樣的組合雖不復存在,但仍在心中藏有很多回憶。


今年度的『GNOME.Asia Summit 2012』將在 6/9 - 6/10 於香港舉行,很感謝 GNOME Foundation 再次贊助,讓小弟幸運的又能以講者身份飛去參加這次活動,而這次要給予的主題是『Enjoy Writing a Modern Desktop Application in JavaScript』。

今日科技變革相當快速,但桌面應用的開發一直跟不上節奏,原因是『開發效率差』和『入門門檻高』一直都是 C/C++ 這類中低階的語言的痛處,不足因應這個時代對應用的大量需求。因此使用有高生產力的腳本(Script)語言來開發桌面應用程式,便是目前可行的解決之道。

JavaScript 自 World Wide Web 澎渤發展以來,一直處於相當重要的地位,用於處理 User Interface 的工作,已經有快要二十年的時間,可說是非常成熟。事實上,GLib/GTK+/Clutter 等底層的事件模型,就與 JavaScript Engine 本身的機制大同小異,在同樣解決使用者介面的工作上,可說是殊途同歸。因此,有這樣豐富的前線作戰經驗當後盾,我們可以確信,JavaScript 非常適合被用於開發桌面應用程式。若是與 GTK+/Clutter 這類底層 Toolkit 相結合,跳脫出瀏覽器,寫原生的桌面應用程式也未嘗不可。

這次的議程,小弟將提及:
  1. 回顧 JavaScript 歷史和發展
  2. 探討如何使用 JavaScript 開發原生的桌面應用程式(搭配 GTK+/Clutter/Mx)

既然這次活動的主角是 GNOME,主要將以探討原生的桌面程式為主,當然也會因應未來發展,提及 Web Application 的部份。

期待與香港和世界各地前來的朋友相見。:-)

2012年5月14日 星期一

前端工程師也可以淡定的開發網站應用!RedTea Web Framework!

Standard
還記得,尚未投入 Node.js 前,一直覺得 Node.js 帶來了未來,讓我們可以用 JavaScript 同時開發 Web 前端(Front-end)和後端(Back-end),等到真的投入 Node.js 後,發現雖然事實的確是如此,但由於前端和後端應用所需要的背景知識不盡相同,開發模式和概念更是大異其趣,所以,雖然同樣是使用大家熟悉的 JavaScript 語言,但前端開發者仍然不見得能夠如願地來開發後端應用。

這樣的情況讓我想起有位來台灣工作的外國朋友,曾告訴過我一個多年前發生在他身上的笑話。故事是:
他原本是個美術方面的設計師,然後,但有一天老闆對他說:『你從現在起,去做開發程式的工作吧。』

他問:『為什麼?』

老闆回答:『寫程式是用英文寫,而你又會英文,不就可以寫嗎?』

是的,同樣道理,雖然對前端工程師而言,JavaScript 是最熟悉的程式語言,而 Node.js 又可以讓你使用 JavaScript 寫整個 Web 應用,但這不代表對這些人而言,就可以輕易上手 Web service 的後端開發。要真正讓前後端開發合而為一,不只是語言要統一,開發經驗也要相同才是,這才是所有 Web 開發者最期盼的事。

於是筆者基於 Node.js,開發了『紅茶(RedTea)』,這是一個和現有的網站框架(Web Framework)所不一樣的全新的嘗試。以前端開發者視角和經驗為出發點,專門設計給前端工程師上手使用的網站框架,讓前端工程師也可以『淡定』的開發網站應用。由於 RedTea 不是傳統 MVC 模型的 Framework(至於 RedTea 採用的是哪種開發模型,筆者一時也說不上來。),所以,如果你是一個已經被 MVC 涂毒已深的開發者,可能要先花點時間重新理解一下。:-)

此外,RedTea 有一個最大的特點,就是支援了在瀏覽器環境下,呼叫後端 JavaScript Class/Function 的功能,就像在使用本地端的 JavaScript 物件般。因此,前後台交換資料,不用再以 GET/POST、URL Path Routing 或 Ajax 相關的方法實作,只要學會怎麼使用 JavaScript Class 即可。重點是,即使你沒有任何 HTTP 通訊協定的知識,或後前後端資料處理的經驗,依然可以開發出網站程式。

當然,設計 RedTea 的目的,除了是為前端工程師著想之外,也是因為長久以來在思考 MVC 模型的問題後,所嘗試提出的解決方案。至於是什麼問題,可以從專案開發的流程探討,一個基於 MVC 模型的 Web 專案開發流程大致上如下(問題點也將補充在後面):
*註:『程式架構和演算法:Model』、『視覺部份和 UI:View』、『控制機制:Controller』
  1. 設計 Model(問題:當你一開始根本不完全清楚功能需求時,你如何能設計一個完善的架構去容下一切?)
  2. 設計 View
  3. 設計 Controller,用來連接程式邏輯架構和前端 UI (問題:有太多種交換方式,GET/POST Routing stuffs, Ajax APIs 等)
  4. 為了更完整的功能需求,一次又一次修改和重構 Model
  5. 為了配合程式架構和演算法的修改,一次又一次修改和重構 View
  6. Controller 零亂又噁心不已

說穿了,這種來來回回式的開發方法,若是在前後端是不同語言的情況下,將會是不得已的情況。畢竟,前後端需要共同定出一個雙方可以接受的資料交換格式,然後在開發過程中逐漸磨合。不過,當前後端都是 JavaScript 的情況下,是否可以簡化這部份的開發流程,就是一個可以思考的地方。

所以,RedTea 的設計,主要是為了達成這樣的 Web 開發流程:
  1. 設計你眼前第一時間想看到的東西
  2. 在所看到的 UI 上,實作功能需求
  3. 在後端(Server-side)處理使用者因需求產生的資料

由於前後端都是 JavaScript,很容易做到前後端邏輯上的同步,再加上簡單的轉換器和 RPC 設計,就可以讓前端程式直接呼叫後端的 Class 和 Function。後端的 API,就彷彿是你前端程式的一部份。

說了這麼多,到底如何使用 RedTea 開發出一個網站呢?如果你參考 Github 上的 RedTea 範例,會發現主要有四個目錄,分別說明如下:
  1. ui - Layout Template
  2. runner - Browser-side Script
  3. routes - URL Path Routing
  4. apis - Server-side APIs

若你有摸過其他的 MVC Web Framework,對 routes 的功用應該不陌生,而 RedTea 當然也可以像其他框架一樣,讓你隨意自定 URL 的橋接。但若要善用 RedTea 的優勢,routes 的主要目的應該只是決定 UI 和 Runner 的組合,如下:
module.exports = {
    '/': index
};

function index(app, req, res) {
    /* Using index.jade (UI) and index.js (Runner) */
    res.render('index', { title: 'RedTea' });
};

RedTea 在回應瀏覽器的要求時,會合併指定的 UI 和 Runner,並自動代入 RedTea Caller 的機制,讓 runner 被輸出到前端時,有能力呼叫 Server-side APIs。

一個 runner 程式大概長的是這個樣子(examples/runner/index.js):
RedTea.import();

RedTea.main(function() {
    var chat = new RedTea.API.Chat;

    chat.say('Fred', 'Hello World!');
    chat.getConversation(function(err, data) {
        for (var i in data) {
            var lineObj = data[0];
            var dom = document.getElementById('conversation');
            dom.innerHTML += lineObj.name + ':' + lineObj.content + '<br>';
        }
    });
});

從範例中,你會看到瀏覽器上才有的 document 物件出現,事實上 runner 就是一支跑在瀏覽器上的 JavaScript 程式。 RedTea.import() 會去 Server 載入目前已經被定義的 API,等初始化完成,會以 RedTea.main() 為程式進入點開始跑。這時,你就可以用 RedTea.API 去建立 Server API 定義的物件,然後呼叫在 Server-side 的函數方法。進一步,你也可以用得到的資料,去更新網頁上的 DOM。

至於 Server-side APIs 的定義與寫一般 JavaScript 無異,你可以定義 Class、Function 或是變數,RedTea 會自動將這些 API 轉換成前端可以跑的形式,而原始的 API 定義長這個樣子(examples/apis/chat.js):
module.exports = {
    Chat: Chat
};

function Chat(externalData) {
    this.externalData = externalData;
    this.publicData = externalData.userdata;
}

Chat.prototype.getUserList = function(callback) {

    callback(null, this.publicData.users);
};

Chat.prototype.getConversation = function(callback) {

    callback(null, this.publicData.conversation);
};

Chat.prototype.say = function(name, content) {
    var line = {
        name: name,
        content: content
    };

    this.publicData.conversation.push(line);
};

然後和一般的 Node.js 程式一樣,你會需要一支主程式(examples/app.js):
var RedTea = require('redtea');

var app = new RedTea;

app.routeDirs.push(__dirname + '/routes');
app.uiDirs.push(__dirname + '/ui');
app.runnerDirs.push(__dirname + '/runner');
app.apiDirs.push(__dirname + '/apis');

var publicData = {
    users: [],
    conversation: []
};

app
    .initRoute()
    .initRender()
    .initAPI(publicData)
    .listen(9876);


只會 JavaScript 嗎?開始享受單純用 JavaScript 開發網站應用程式的人生吧!卡關了?就喝杯紅茶淡定一下! :-D

後記

RedTea 目前只是原型,還不是相當完整,像是 static file 的功能都尚未實作,很多功能也還只是堪用,歡迎各界一同補齊它。 :-)

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),甚至是無法追蹤問題所在。