介紹:
在第一節呢,我花了大量的時間來介紹promises和deferreds的理論。現在呢,我們來看看jquery中的promises(作者一會兒用單數,一會兒用復數形式,妹的)。
Note:代碼示例將使用jQuery,盡管它偏離了Promise/A 協議。
排序模式:
deferred就是一個未完成的對象,promise呢則是一個未知的值。換句話說,prmises/deferreds 允許我們描述(represent)簡單的任務,可以很容易地組合來描述復雜的任務和任務流,允許我們細粒度地控制排序。這就意味着我們可以像寫同步代碼一樣去寫異步代碼,so easy,媽媽再也不用擔心我的學習了。此外,promises讓復雜的異步任務變得更容易去抽象成一些小塊的功能--比如動畫加載,動畫處理等等。
讓我們來看看三種常見的排序模式,promises使之變成了可能:堆放,並行和順序。
- 堆放:同一promise 事件綁定多個處理
var request = $.ajax(url); request.done(function () { console.log('Request completed'); }); // Somewhere else in the application request.done(function (retrievedData) { $('#contentPlaceholder').html(retrievedData); });
- 並行:請求多個promise,返回一個promise,該promise可以給出執行多個promise的完成情況。
$.when(taskOne, taskTwo).done(function () { console.log('taskOne and taskTwo are finished'); });
- 順序任務:按順序執行任務。
var step1, step2, url; url = 'http://fiddle.jshell.net'; step1 = $.ajax(url); step2 = step1.then( function (data) { var def = new $.Deferred(); setTimeout(function () { console.log('Request completed'); def.resolve(); },2000); return def.promise(); }, function (err) { console.log('Step1 failed: Ajax request'); } ); step2.done(function () { console.log('Sequence completed') setTimeout("console.log('end')",1000); });
這些模式可以組合使用或者單獨使用,用以建立復雜的任務或工作流。
常見示例:
許多的promise示例都使用Ajax請求和UI動畫。實際上,jQuery的Ajax請求默認返回的是個promise。這給人造成一種錯覺,以為解決異步任務完美解決方案就是promise了。其實不然,promise就是一個值得你在任何時候去考慮使用的工具,而不僅僅是回調。讓我們看看使用promise的實例吧。
- ajax,這個就算了,一搜一大把,我就略過啦。
- 定時:創建一個基於timeout的函數:
function wait(ms) { var deferred = $.Deferred(); setTimeout(deferred.resolve, ms); // We just need to return the promise not the whole deferred. return deferred.promise(); } // Use it wait(1500).then(function () { // Do something brilliant here! });
- 動畫:顯然下面的動畫是沒啥實際用處的,但是這個示例卻給出了promise和動畫如何一起使用。
var fadeIn = function (el) { var promise = $(el).animate({ opacity: 1 }, 1500); // Dynamically create and return an observable promise object which will be resolved when the animation completes. return promise.promise(); }; var fadeOut = function(el) { var promise = $(el).animate({ opacity: 0 }, 1500); // Dynamically create and return an observable promise object return promise.promise(); }; // With the setup out of the way, we can now do one of the following. // Parallel $.when( fadeOut('div'), fadeIn('div') ).done(function () { console.log('Animation finished'); $('p').css('color', 'red'); }); // OR // Chained fadeOut('div').then(function (el) { fadeIn(el); // returns a promise }).then(function (el) { fadeOut(el); // returns a promise });
- 使用$.when()同步並行任務
var promiseOne, promiseTwo, handleSuccess, handleFailure; // Promises promiseOne = $.ajax({ url: '../test.html' }); promiseTwo = $.ajax({ url: '../test.html' }); // Success callbacks // .done() will only run if the promise is successfully resolved promiseOne.done(function () { console.log('PromiseOne Done'); }); promiseTwo.done(function () { console.log('PromiseTwo Done'); }); // $.when() creates a new promise which will be: // resolved if both promises inside are resolved // rejected if one of the promises fails $.when( promiseOne, promiseTwo ) .done(function () { console.log('promiseOne and promiseTwo are done'); }) .fail(function () { console.log('One of our promises failed'); });
- 解耦事件和程序邏輯(jsfiddle demo)
var def, getData, updateUI, resolvePromise; // The Promise and handler def = new $.Deferred(); updateUI = function (data) { $('p').html('I got the data!'); $('div').html(data); }; getData = $.ajax({ url: '/echo/html/', data: { html: 'testhtml', delay: 3 }, type: 'post' }) .done(function(resp) { return resp; }) .fail(function (error) { throw new Error("Error getting the data"); }); // Event Handler resolvePromise = function (ev) { ev.preventDefault(); def.resolve(ev.type, this); return def.promise(); }; // Bind the Event $(document).on('click', 'button', resolvePromise); def.then(function() { return getData; }) .then(function(data) { updateUI(data); }) .done(function(promiseValue, el) { console.log('The promise was resolved by: ', promiseValue, ' on ', el); }); // Console output: The promise was resolved by: click on <button> </button>
Gotcha’s: 理解jQuery中的.then()
為了演示一對“gotcha's”,這些最終的實例將會貫穿我的整個promise實踐。
讓我們來創建兩個公用函數:
// Utility Functions function wait(ms) { var deferred = $.Deferred(); setTimeout(deferred.resolve, ms); return deferred.promise(); } function notifyOfProgress(message, promise) { console.log(message + promise.state()); }
第一次的promise鏈式寫法看起來就像這樣:
// Naive attempt at working with .then() // Create two new deferred objects var aManualDeferred = new $.Deferred(), secondManualDeferred = aManualDeferred.then(function () { console.log('1 started'); wait(3500).done(function () { console.log('1 ended'); }); }); // After secondManualDeferred is resolved secondManualDeferred.then(function () { console.log('2 started'); wait(2500).done(function () { console.log('2 ended'); }); }); // Resolve the first promise aManualDeferred.resolve();
執行的輸出結果:
1 started 2 started 2 ended 1 ended
納尼?jQuery API不是說 .then()方法可以鏈式並返回promise么?我所期望的是無論我在.then()方法中插入了任何代碼,程序都應順序執行,只有上一個任務完成了,才可以執行下一個。但是這很明顯不是我所期望的結果啊?為毛啊?
.then()原理
查看jQuery 源代碼,我們可以發現:
- .then()方法始終返回一個新的promise
- .then()必須傳遞一個函數作為回調
如果.then()沒有傳遞函數,那么:
- 新的promise將會擁有和初始promise行為一致(這就意味着它立即 被解決/被拒絕)
- .then()中的輸入將被執行但是會被.then()忽略
如果.then()被傳遞了一個函數,該函數返回了一個promise 對象,那么:
- 新的promise將和返回的promise行為一致
var deferred = $.Deferred(), secondDeferred = deferred.then(function () { return $.Deferred(function (newDeferred) { setTimeout(function() { console.log('timeout complete'); newDeferred.resolve(); }, 3000); }); }), thirdDeferred = secondDeferred.then(function () { console.log('thirdDeferred'); }); secondDeferred.done(function () { console.log('secondDeferred.done'); }); deferred.resolve();
- 如果.then()傳遞的是一個函數,該函數返回一個值,那么這個值將成為新對象的值
var deferred = $.Deferred(), filteredValue = deferred.then(function (value) { return value * value; }); filteredValue.done(function (value) { console.log(value); }); deferred.resolve(2); // 4
你可能已經注意到了,為毛我的版本無法運行(可是我能運行啊,作者,你腫么了)。我沒有立即從.then()返回一個promise,所以由.then()創建的新的promise擁有同樣的值。
避免被回調坑爹(Avoiding the descent into callback hell)
我們知道.then()需要一個回調函數並返回一個promise,所以我們可以像下面這么做:
// Anti-pattern - Return to callback hell var aManualDeferred = new $.Deferred(); aManualDeferred.then(function () { console.log('1 started'); return wait(3500).then(function () { console.log('1 ended'); }).then(function () { console.log('2 started'); return wait(2500).done(function () { console.log('2 ended'); }); }); }); // Resolve the first promise aManualDeferred.resolve();
運行啦。不幸的是,這個回調太shit了,我們又掉到回調嵌套的坑里了。還好,我們有絕招來規避這種嵌套。那么,如何解決這個問題呢,當然,這得具體情況具體分析咯。
避免使用大量的無名promise
舉例如下:
// A chain // Create new deferred objects var aManualDeferred = $.Deferred(); aManualDeferred.then(function () { console.log('1 started'); // We need to return this, we return a new promise which is resolved upon completion. return wait(3500); }) .then(function () { console.log('1 ended'); }) .then(function () { console.log('2 started'); return wait(2500); }) .then(function () { console.log('2 ended'); }); // Resolve the first promise aManualDeferred.resolve();
這次看起來就漂亮多啦。缺點是,只有一個promise是命名的,不利於我們細粒度地控制每個步驟,這個在很多情況下是非常有用的哦。
解耦promise和處理函數
假如我們不想深層嵌套,我們就得命名promise,這樣我們就有了每個步驟的控制權。
看看最終版本:
var aManualDeferred, secondManualDeferred, thirdManualDeferred; // Create two new deferred objects aManualDeferred = $.Deferred(); secondManualDeferred = aManualDeferred.then(function () { console.log('1 started'); // We need to return this, we return a new promise which is resolved upon completion. return wait(3500); }) .done(function () { console.log('1 ended'); }); thirdManualDeferred = secondManualDeferred.then(function () { console.log('2 started'); return wait(2500); }) .done(function () { console.log('2 ended'); }); // Check current state thirdManualDeferred.notify( notifyOfProgress('thirdManualDeferred ', thirdManualDeferred) ); // Resolve the first promise aManualDeferred.resolve(); // Console output // aManualDeferred pending // secondManualDeferred pending // 1 started // 1 ended // 2 started // 2 ended
這個優點就很明顯了,現在的程序分為三個步驟,我們可以訪問每個promise的狀態,用以發送進程通知,或者在管理代碼執行順序時,也不需要重寫代碼(誰說的,只不過修改代價很小了)。
上下文和傳值
在Ajax示例中,我們看到,可以給.resolve()和.fail()函數傳值。如果一個promise resolved(我覺得“resolved”這里還是不翻譯的好)了一個值,那么新的promise就是返回值本身。
var passingData = function () { var def = new $.Deferred(); setTimeout(function () { def.resolve('50'); }, 2000); return def.promise(); }; passingData().done(function (value) { console.log(value); });
當我們 resolve 了一個promise,我們可以給它設置 this。
// Create an object var myObject = { myMethod: function (myString) { console.log('myString was passed from', myString); } }; // Create deferred var deferred = $.Deferred(); // deferred.done(doneCallbacks [, doneCallbacks ]) deferred.done(function (method, string) { console.log(this); // myObject // myObject.myMethod(myString); this[method](string); }); deferred.resolve.call(myObject, 'myMethod', 'the context'); => myString was passed from the context // We could also do this: // deferred.resolveWith(myObject, ['myMethod', 'resolveWith']); // but it's somewhat annoying to pass an array of arguments. // => myString was passed from resolveWith
剩下的是最佳實踐和jquery 中的一些方法介紹,作者一帶而過,我就不翻譯了。想看,就看原文吧。
ps:
所有的deffered對象都支持的方法:
------------------------
then(doneCallbacks,failCallbacks):由此可知,then可以處理成功和失敗的回調,這就是和done的區別啊
done(doneCallbacks)
fail(failCallbacks)
---------------
ajax對象還包括
sucess
fail
它們會分別映射到deffered對象的done 和fail方法上
譯自:http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt2-practical-use