jQuery.Deferred.pipe

這次的 COSCUP 有介紹jQuerydeferred,當時沒講到的 pipe,其實是非常強大的,當我開始會使用 pipe 時,那種衝擊不遜於當初看到 deferred 和 when 的時候。deferred 是用來監聽非同步變數的狀態,簡單說就是拿到變數的時候,程式還不知道它的值是什麼,deferred 常使用於像是 ajax call,使用者回應等等地方,而配合 deferred 的 when 則是用來監聽複數個 deferred 物件,利用 when 還可以處理比較複雜的非同步相依性問題,不過其實光是有這兩個工具,實際開發一些 Web Application 偶爾還是會覺得有不夠的地方。

先舉一個簡單的例子,要做一個登入頁面,然後要支援 one time password(OTP),就是像 battle.net 或是 google 的兩步認證那樣,如果簡單寫的話,用 callback,第一階段的程式碼:

$.post('/api/login', idpw, function (res) {
    if (res.requireOTP) {
        showOTPUI();
    } else {
        loginSuccess();
    }
}, loginFail);

然後接著使用者輸入認證碼後的部份:

$.post('/api/', otp, loginSuccess, loginFai);

這兩段程式碼的流程其實很簡單,就是如果帳號密碼錯,執行 loginFail,如果對的話,看有沒有需要 OTP 驗證,沒需要的話執行 loginSuccess,需要的話再跟使用者要 OTP,然後送去 server 做驗證,結果正確的話執行 loginSuccess,不正確的話執行 loginFail,可以畫成流程圖如下:

no pipe

這個流程基本上沒問題,可是身為一個程式設計師,看到重複的東西出現,就會想要把它拿掉,在這張流程圖中什麼東西重複出現了呢,就是最後的終點,Success 和 Fail 分別出現兩次,看到這種終點出現兩次就會很想修改掉,這時候 pipe 就派上用場了。

Pipe 顧名思義就是管路,和 linux 作業系統命令列介面的 pipeline 很像,一樣是一個程式處理完,結果丟到下一個程式繼續處理,一個接一個這樣,只不過 deferred 的 pipe 處理的是非同步的流程,如果使用 jQuery 的 pipe 來處理這個問題,程式碼大概會變成:

$.post('/api/login', idpw).pipe(function (res) {
    _dfd = $.Deferred();
    if (res.requireOTP) {
        showOTPUI(_dfd);
    } else {
        _dfd.resolve();
    }
    return _dfd;
}).then(loginSuccess, loginFail);

showOTPUI 那邊則要處理使用者輸入認證碼後的行為:

$.post('/api/', otp, _dfd.resolve, _dfd.reject);

這樣的程式碼就可以看到重複的 loginSuccess 和 loginFail 消失了,流程圖則變成像是下面這樣:

pipe

再舉一個例子,假設某個網路服務的使用者資料更新,要同時在前端處理上傳頭像、加密資料等,流程可能會是:

  1. 檢查欄位
  2. 上傳頭像
  3. 跟 server 要求加密用的 key
  4. 加密資料
  5. 把資料上傳

這樣的流程中,有三個動作是跟 server 作溝通的非同步工作,分別是上傳頭像、跟 server 要 key 和最後的把資料上傳,但是這五個動作又要照順序作,這種情形就非常適合使用 pipe,下面是一個大概的範例,先定義三個後面 pipe 裡面會用到函數:

var validator = function ($form) {
    return $.Deferred()[_validate($form)? 'resolve' : 'reject']();
};

var upload = function ($file) {
    var prepared = prepare($file);
    return $.post(prepared);
};

var encrypt = function (data, key) {
    var crypt_data = _encrypt(data, key);
    return $.Deferred().resolve(crypt_data);
};

這些函數在寫的時候要注意到,他們回傳的最好都是 deferred 物件,根據情況可以直接決定它的狀態,接著是重點的,表單的送出事件:

$('#profile-form').on('submit', function () {
    var $form = $(this),
        data = $form.serialize();

    validate($form).pipe(function () {
        return upload($form.find('[name=avatar]'));
    }).pipe(function (avatar_id) {
        data.avatar_id = avatar_id;
    }).pipe(function () {
        return $.get('/api/key');
    }).pipe(function (key) {
        return encrypt(data, key);
    }).pipe(function (crypt_data) {
        return $.post('/api/profile', crypt_data);
    }).done(function () {
        //Do some response to user
    });
});

而除了這類的應用外,還有一個用途,就是處理複雜的動畫效果,在 COSCUP 的 queue 的那部分,最後的例子,要把 #A, #B, #C 照順序 fade out,其實也是可以用 pipe 來處理,而這要多虧 jQuery 的 .promisequeue 和 deferred 可以接在一起,程式碼如下:

$('#A').fadeOut().promise().pipe(function () {
    return $('#B').fadeOut().promise();
}).pipe(function () {
    return $('#C').fadeOut().promise();
});

如果單純只是作動畫,那這樣寫並沒有比較好,不過要是你的動畫會和一些其他的 deferred 物件整合、串接,那這功能就很好用了。

最後下個小結論,deferred 是用來代表非同步的變數,when 是平行處理非同步變數,也可以說是並聯的狀態,pipe 則是處理序列的非同步變數,也可以說是串聯的狀態,並聯和串聯當然可以在自己任意連接,所以就可以兼顧到各種狀況了。