三個不要再用 jQuery Promise 的理由

Update 2016/04/05 20:27 話說 beta 中的 jQuery 3.0 確定 Promise 的部分會是相容於 Promises/A+,到時候出了正式版本可以開心升級!


其實不要用 jQuery promise 這回事兒,時有所聞,最主要的原因是其實它實作的內容跟外面大部分的 Promise library (Promises/A+)都不一樣。

所以很多人覺得 jQuery 的 Promise 只是名為 Promise 的「某種東西」,並不是真正的 Promise。

想了解詳情可以看一下 2012 年的這篇文章 You’re Missing the Point of Promises

然而我一直不以為意。

到這個星期我才切身的實際體會不要用的原因,這邊列出三點,應該是比較容易碰到的情況。

1. 面對靜態結果,$.when() 不會以非同步的方式產出 resolved promise

這個我想應該很多人已經有經驗 XD

舉例如下:

// 期待中,結果會是以 async 的方式取得
function xhrGet(something) {
    if (cache) {
        return $.when(cache);
    }
    return $.get(something);
}

/**
 * 在送出 XHR request 之後清空頁面,取得 response 之後重繪頁面
 * 
 * 沒有 cache - 正常運作
 *   有 cache - 在 `renderPage()` 之後才 `clearPage()`,頁面就沒了
 */
xhrGet(...).then(function (res) {
	renderPage(res);
});
clearPage();

那麼我們實際來確認一下大家的情況,下面用了三家的 library 來做測試: (其中 Promise 的部分是用 polyfill 的方式,如果你的瀏覽器有原生支援就會用瀏覽器內建的)

// $.when
log('# $.when()');
log(1);
$.when(true).then(function () {
    log('2 async [jquery]');
});
log(3);

log('\n');


// Promise.resolve
log('# Promise.resolve()');
log(1);
Promise.resolve(true).then(function () {
    log('2 async [Promise]');
});
log(3);

log('\n');


// Q.when
log('# Q()');
log(1);
Q(true).then(function () {
    log('2 async [Q]');
});
log(3);

範例擺在 Plunker 上:

然而如此微小的挫折並不足以澆熄我熱愛 jQuery Promise 的決心…

2. jQuery Promise 跟原生 Promise 或其他 Promises/A+ library 的相容性不好

如果是 jQuery Promise 剛出來的那個年代(jQuery 1.5),應該很難碰到這個 case,然而現在是原生 Promise 大放異彩的年代,避的了一時,躲不了一世啦~

這週碰巧用了 Mozilla 開源的 storage library localForage,因為它基本上是包裝 IndexedDB 或 WebSQL 成 key-value-pair 的使用方法,所以大部分的操作 API 都是非同步的;而非同步的部分,可以選擇用 callback 或是 Promise 來解決。

想當然而我選了 Promise ,配上 jQuery Promise 之後下面的範例就沒辦法直覺地正常運作。

var TEST_KEY = 'test123',
    TEST_VALUE = 'ooxx...';

function getData () {
    return localforage.setItem(TEST_KEY, TEST_VALUE)
    .then(function () {
        return localforage.getItem(TEST_KEY);
    });
}

$.when(getData())
.then(function (res) {
    // 期待的結果是: jquery ooxx...
    console.log('jquery', res);
});

結果: jquery_localforage.png

然而在 Promise 或 Q 裡面這樣寫是會得到如預期的結果的喔!

這是一個延伸的綜合範例,一樣是比較在第一點裡面的三個實作,有興趣的話,點過去之後打開 console 看看

3. 處理 reject 的方式跟大家不一樣

試問下面的執行結果應該是什麼呢?

$.Deferred().reject()
.then(null, function () {
    return 'error handled';
})
.then(function (res) {
    log('jquery resolve', res);
}, function (res) {
    log('jquery reject', res);
});

// output: jquery reject error handled

promise chain 中被 reject,經過 rejection handler 後

  • jQuery: 保持 rejected (除非在 rejection handler 裡面回傳一個 resolved promise)
  • Promises/A+: 轉為 resolved

一樣附上比較版

其實這個理由可能本文提到的三種之中最崩潰的…

結論

其實要不要棄用 jQuery Promise 還是見仁見智,如果你已經處理好了 static resolve 的 async 問題(即 第 1 點 的情況),並且又確定不會需要跟別的 Promise library 混用的話,只用 jQuery Promise 還是可以過得好好的(不過還是要注意 behavior 不同的部分,比如 第 3 點)。

不過在現在這個前後端JS爆炸並且 module 化的時代,我還是誠心推薦你做個轉換吧!

你說那那些原本就 return jQuery Promise 的 jQuery API 怎麼辦?

其實轉換 jQuery Promise 的最簡單方法就是直接用 Promise 把它包起來(resolve)就好囉:

// 以 $.get() 為例

// Promise
Promise.resolve($.get(...)).then(...)

// Q
Q($.get(...)).then(...)

更詳細資訊可以看看 Q 的文件裡精心製作的 migration guide

想當年我有一篇 Promise 相關的文章也是用 jQuery 寫的(遠目)