背景
移動web app開發,異步代碼是時常的事,比如有常見的異步操作:
- Ajax(XMLHttpRequest)
- Image Tag,Script Tag,iframe(原理類似)
- setTimeout/setInterval
- CSS3 Transition/Animation
- HTML5 Web Database
- postMessage
- Web Workers
- Web Sockets
- and more…
后面幾個是CSS3 HML5加入的新API.這些接口都是會產生異步的操作
比如本人的一個phonegap項目,操作HTML5本地數據庫(HTML5 Web Database)就是一個異步的過程,如果同時執行多個查詢,勢必同步代碼要等待數據查詢結束后調用
附項目源碼:執行多次異步查詢
/** * 初始化操作 * @return */ proto.initProcess = function(){ var self = this, prev = null , curr = null , next = null ; debug.group("start of init process"); var idx = self.chapterIndex; debug.info("PageBase: 執行初始化之前的操作!"); self.initProcessBefore(); if(idx == 0){ debug.info("PageBase: 初始化入口點從第一章開始進入"); debug.info("PageBase: 解析器解析第一章數據!"); curr = self.process(self.chapters[idx]); curr.then(function(pages){ debug.info(self.format("PageBase: 第一章數據解析完成,解析頁面數為{0}" , pages.length)); self.cPages = pages; if(self.isChangeFont){ self.idx = Math.ceil((pages.length - 1) * self.idx); } self.cPages.idx = idx; ///////////////////////////////////////////////// // // 2013.1.10修改 // 如果只有一個章節的情況下 // if(1 === self.chapters.length){ deferred.all([curr]).then(self.steup.bind(self)); }else{ debug.info("PageBase:解析器解析后一章數據!"); next = self.loadNextData(idx + 1); next.then(function(args){ debug.info(self.format("PageBase: 后一章數據解析完成,解析頁面數為{0}" , args.pages.length)); self.nPages = args.pages; self.nPages.idx = idx + args.index; debug.info(self.format("PageBase: 初始化數據解析完成, 當章索引{0} 當章頁數{1} 下章索引{2} 下章頁數{3}" , self.cPages.idx , self.cPages.length , self.nPages.idx , self.nPages.length)); debug.info("PageBase: 初始化數據解析完成,即將生成結構操作!"); }); deferred.all([curr , next]).then(self.steup.bind(self)); } }); }else if(idx == self.chapters.length -1){ debug.info("PageBase: 初始化入口點從最后一章開始進入"); debug.info("PageBase:解析器解析最后一章數據!"); prev = self.loadPrevData(idx - 1); prev.then(function(args){ self.pPages = args.pages; self.pPages.idx = args.index + 1; debug.info(self.format("PageBase: 最后一章的前一章數據解析完成,解析頁面數為{0}" , args.pages.length)); curr = self.process(self.chapters[idx]); curr.then(function(pages , data){ if(self.isChangeFont){ self.idx = Math.ceil((pages.length - 1) * self.idx); } self.cPages = pages ; self.cPages.idx = idx; debug.info(self.format("PageBase: 最后一章數據解析完成,解析頁面數為{0}" , pages.length)); debug.info(self.format("PageBase: 初始化數據解析完成, 前章索引{0} 前章頁數{1} 當章索引{2} 當章頁數{3} " , self.pPages.idx , self.pPages.length , self.cPages.idx , self.cPages.length )); debug.info("PageBase: 初始化數據解析完成,即將生成結構操作!"); }); deferred.all([prev , curr]).then(self.steup.bind(self)); }); }else{ debug.info("PageBase: 初始化入口點從中間章開始進入"); prev = self.loadPrevData(idx - 1); debug.info("PageBase:解析器解析中間章的前一章數據!"); prev.then(function(args){ self.pPages = args.pages ; self.pPages.idx = args.index; debug.info(self.format("PageBase: 中間章前一章數據解析完成,解析頁面數為{0}" , args.pages.length)); debug.info("PageBase:解析器解析中間章數據!"); curr = self.process(self.chapters[idx]); curr.then(function(pages , data){ if(self.isChangeFont){ self.idx = Math.ceil((pages.length) * self.idx); // console.log("spages.length - 1",pages.length) // console.log("self.idx",self.idx) } self.cPages = pages ; self.cPages.idx = idx; debug.info(self.format("PageBase: 中間章數據解析完成,解析頁面數為{0}" ,pages.length)); debug.info("PageBase:解析器解析中間章的后一章數據!"); next = self.loadNextData(idx + 1); next.then(function(args){ self.nPages = args.pages ; self.nPages.idx = idx + args.index; debug.info(self.format("PageBase: 中間章后一章數據解析完成,解析頁面數為{0}" , args.pages.length)); debug.info(self.format("PageBase: 初始化數據解析完成, 前章索引{0} 前章頁數{1} 當章索引{2} 當章頁數{3} 下章索引{4} 下章頁數{5}" , self.pPages.idx , self.pPages.length , self.cPages.idx , self.cPages.length , self.nPages.idx , self.nPages.length)); debug.info("PageBase: 初始化數據解析完成,即將生成結構操作!") }); deferred.all([prev , curr , next]).then(self.steup.bind(self)); }); }); }
如何組織代碼
但是對於異步+回調的模式,當需要對一系列異步操作進行流程控制的時候似乎必然會面臨着回調嵌套。因此怎么把異步操作“拉平”,用更好的方法去優化異步編程的體驗,同時也寫出更健壯的異步代碼,是這兩年來前端圈子里很火的話題。
代表的
- 消息驅動——代表:@朴靈 的EventProxy
- Promise模式——代表:CommonJS Promises,jQuery,Dojo
- 二次編譯——代表:@老趙 的Jscex
- jQuery 是唯一的實現了這種 when 方法的庫。其他的 promises 庫,例如 Q, Dojo, 和 when 依照 Promises/B spec 實現了 when 方法, 但是並沒有實現注釋者提及的 when 方法。但是,Q 庫有一個 all方法,when.js 也有一個 parallel方法,與上面的 jQuery.when 方法作用一樣,只是它們接受一個數組類型的參數,而不是任意數量的參數。
回顧Jquery Deferred
- 從1.5版本開始,jQuery加入了Deferred功能,讓事件處理隊列更加的完善。並用 這個機制重寫了Ajax模塊。雖然還沒輪到Ajax,但是接下來的事件處理函數中牽扯到了 這個機制
- Deferred把回調函數注冊到一個隊列中,統一管理,並且可以同步或者異步地調用 這些函數。jQuery.Deferred()用來構造一個Deferred對象。該對象有狀態值,共有三種: Rejected, Resolved和初始狀態。其中Resolved表示該操作成功完成了,而Rejected 則表示出現了錯誤,調用失敗。Deferred對象的主要成員如下:
- done(callback): 注冊一個callback函數,當狀態為resolved時被調用。
- fail(callback): 注冊一個callback函數,當狀態為rejected時被調用。
- always(callback): 注冊一個callback函數,無論是resolved或者rejected都會被 調用。
- then(successCallback, failureCallback): 同時傳入成功和失敗的回調函數。
- pipe(successFilter, failureFilter): 在調用成功和失敗的回調函數前先調用pipe 指定的函數。算是一種管道機制,攔截了函數調用。
- resolve(args): 把狀態設置為Resolved。
- reject(args): 把狀態設置為Rejected。
- promse(): 返回的是一個不完整的Deferred的接口,沒有resolve和reject。即不能 修改Deferred對象的狀態。可以看作是一種只讀視圖。這是為了不讓外部函數提早觸發 回調函數。比如$.ajax在1.5版本后不再返回XMLHttpRequest,而是返回一個封裝了 XMLHttpRequest和Deferred對象接口的object。其中Deferred部分就是promise()得到 的,這樣不讓外部函數調用resolve和reject,防止在ajax完成前觸發回調函數。把這 兩個函數的調用權限保留給ajax內部。
deferred-js
本人在項目中使用 Promise/A 規范實現的 deferred-js , 比較簡單輕巧.
如何使用?
API:
var DeferredAPI = { deferred : deferred, all : all, Deferred : Deferred, DeferredList : DeferredList, wrapResult : wrapResult, wrapFailure : wrapFailure, Failure : Failure }
最簡單常用的案例
//Deferred對象創建 var d = new deferred.Deferred() //添加一個回調到遞延的回調鏈 d.then(function(result) { console.log('Hello ' + result) return result }) //等待回調后觸發 d.resolve('World')
每個鏈接在一個回調鏈可以是兩個函數,代表一個成功,一個失敗
只有一個成功回調
d.then(function(result) { // 自己的代碼 return result })
失敗回調
d.fail(function(failure) { // optionally do something useful with failure.value() return failure });
添加一個成功方法和一個失敗方法
d.then(function(result) { // do something useful with the result return result }, function(failure) { // optionally do something useful with failure.value() return failure })
不管回調成功或者失敗都執行同一份代碼
d.both(function(result) { // in the case of failure, result is a Failure // do something in either case return result })
如果許多異步在操作,比如提供的案例,在要執行HTML5數據庫N次后,如何操作呢?
請仔細對照下案例中的
deferred.all([prev , curr , next]).then(self.steup.bind(self));
all的方法等待所有的延時隊列加載完畢后,才執行后續代碼
使用起來很方便,很精簡沒有那么多復雜的概念
使用教程之后,下一節附源碼的實現