在工作中我們可能會把jQuery
選擇做自己專案的基礎庫,因為其提供了簡便的DOM
選擇器以及封裝了很多實用的方法,比如$.ajax()
,它使得我們不用操作xhr
和xdr
物件,直接書寫我們的程式碼邏輯即可。更為豐富的是它在ES6
沒有原生支援的那段時間,提供了Deferred
物件,類似於Promise
物件,支援done/fail/progress/always
方法和when
批處理方法,這可能在專案上幫助過你。
ES6
提供了Promise
物件,但由於它是內建C++
實現的,所以你也沒法看它的設計。不如我們通過jQuery
的原始碼來探究其設計思路,並比較一下兩者的區別。本文采用jquey-3.1.2.js
版本,其中英文註釋為原版,中文註釋為我新增。
jQuery
的ajax
總體設計
jQuery
在內部設定了全域性的ajax
引數,在每一個ajax
請求初始化時,用傳遞的引數與預設的全域性引數進行混合,並構建一個jqXHR
物件(提供比原生XHR
更為豐富的方法,同時實現其原生方法),通過傳遞的引數,來判斷其是否跨域、傳遞的引數型別等,設定好相關頭部資訊。同時其被初始化為一個內建Deferred
物件用於非同步操作(後面講到),新增done/fail
方法作為回撥。同時我們也封裝了$.get/$.post
方法來快捷呼叫$.ajax
方法。
上面提到的Deferred
物件,與ES6的Promise
物件類似,用於更為方便的非同步操作,多種回撥以及更好的書寫方式。提供progress/fail/done
方法,並分別用該物件的notify/reject/resolve
方法觸發,可以使用then
方法快速設定三個方法,使用always
新增都會執行的回撥,並且提供when
方法支援多個非同步操作合併回撥。可以追加不同的回撥列表,其回撥列表是使用內部Callbacks
物件,更方便的按照佇列的方式來進行執行。
Callbacks
回撥佇列物件,用於構建易於操作的回撥函式集合,在操作完成後進行執行。支援四種初始化的方式once/unique/memory/stopOnFalse
,分別代表只執行依次、去重、快取結果、鏈式呼叫支援終止。提供fired/locked/disabled
狀態值,代表是否執行過、上鎖、禁用。提供add/remove/empty/fire/lock/disable
方法操作回撥函式佇列。
主要涉及到的概念就是這三個,不再做延伸,三個物件的設計程式碼行數在1200行左右,斷斷續續看了我一週 (´ཀ`」 ∠) 。我們從這三個倒序開始入手剖析其設計。
jQuery.Callbacks
物件
Callbacks
物件,用於管理回撥函式的多用途列表。它提供了六個主要方法:
-
add
: 向列表中新增回撥函式 -
remove
: 移除列表中的回撥函式 -
empty
: 清空列表中的回撥函式 -
fire
: 依次執行列表中的回撥函式 -
lock
: 對列表上鎖,禁止一切操作,清除資料,但保留快取的環境變數(只在memory
引數時有用) -
disable
: 禁用該回撥列表,所有資料清空
在初始化時,支援四個引數,用空格分割:
-
once
: 該回撥列表只執行依次 -
memory
: 快取執行環境,在新增新回撥時執行先執行一次 -
unique
: 去重,每一個函式均不同(指的是引用地址) -
stopOnFalse
: 在呼叫中,如果前一個函式返回false
,中斷列表的後續執行
我們來看下其例項使用:
let cl = $.Callbacks(`once memory unique stopOnFalse`);
fn1 = function (data) {
console.log(data);
};
fn2 = function (data) {
console.log(`fn2 say:`, data);
return false;
};
cl.add(fn1);
cl.fire(`Nicholas`); // Nicholas
// 由於我們使用memory引數,儲存了執行環境,在新增新的函式時自動執行一次
cl.add(fn2); // fn2 say: Nicholas
// 由於我們使用once引數,所以只能執行(fire)一次,此處無任何輸出
cl.fire(`Lee`);
// 後面我們假設這裡沒有傳入once引數,每次fire都可以執行
cl.fire(`Lee`); // Lee fn2 say: Lee
// 清空列表
cl.empty();
cl.add(fn2, fn1);
// 由於我們設定了stopOnFalse,而fn2返回了false,則後新增的fn1不會執行
cl.fire(`Nicholas`); // fn2 say: Nicholas
// 上鎖cl,禁用其操作,清除資料,但是我們新增了memory引數,它依然會對後續新增的執行一次
cl.lock();
// 無響應
cl.fire();
cl.add(fn2); // fn2 say: Nicholas
// 禁用cl,禁止一切操作,清除資料
cl.disable();
除了上面所說的主要功能,還提供has/locked/disabled/fireWith/fired
等輔助函式。
其所有原始碼實現及註釋為:
jQuery.Callbacks = function( options ) {
options = typeof options === "string" ?
// 將字串中空格分割的子串,轉換為值全為true的物件屬性
createOptions( options ) :
jQuery.extend( {}, options );
var // Flag to know if list is currently firing
firing,
// Last fire value for non-forgettable lists
memory,
// Flag to know if list was already fired
fired,
// Flag to prevent firing
locked,
// Actual callback list
list = [],
// Queue of execution data for repeatable lists
queue = [],
// Index of currently firing callback (modified by add/remove as needed)
firingIndex = -1,
// Fire callbacks
fire = function() {
// Enforce single-firing
locked = locked || options.once;
// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
fired = firing = true;
// 為quene佇列中不同的[context, args]執行list回撥列表,執行過程中會判斷stopOnFalse中間中斷
for ( ; queue.length; firingIndex = -1 ) {
memory = queue.shift();
while ( ++firingIndex < list.length ) {
// Run callback and check for early termination
if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stopOnFalse ) {
// Jump to end and forget the data so .add doesn`t re-fire
firingIndex = list.length;
memory = false;
}
}
}
// Forget the data if we`re done with it
if ( !options.memory ) {
memory = false;
}
firing = false;
// Clean up if we`re done firing for good
// 如果不再執行了,就將儲存回撥的list清空,對記憶體更好
if ( locked ) {
// Keep an empty list if we have data for future add calls
if ( memory ) {
list = [];
// Otherwise, this object is spent
} else {
list = "";
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
if ( list ) {
// If we have memory from a past run, we should fire after adding
// 如果我們選擇快取執行環境,會在新新增回撥時執行一次儲存的環境
if ( memory && !firing ) {
firingIndex = list.length - 1;
queue.push( memory );
}
( function add( args ) {
jQuery.each( args, function( _, arg ) {
// 如果是函式,則判斷是否去重,如果為類陣列,則遞迴執行該內部函式
if ( jQuery.isFunction( arg ) ) {
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
// Inspect recursively
add( arg );
}
} );
} )( arguments );
if ( memory && !firing ) {
fire();
}
}
return this;
},
// Remove a callback from the list
// 移除所有的相同回撥,並同步將firingIndex-1
remove: function() {
jQuery.each( arguments, function( _, arg ) {
var index;
while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// Handle firing indexes
if ( index <= firingIndex ) {
firingIndex--;
}
}
} );
return this;
},
// Check if a given callback is in the list.
// If no argument is given, return whether or not list has callbacks attached.
// 檢查是否存在該函式,如果不傳遞引數,則返回是否有回撥函式
has: function( fn ) {
return fn ?
jQuery.inArray( fn, list ) > -1 :
list.length > 0;
},
// Remove all callbacks from the list
empty: function() {
if ( list ) {
list = [];
}
return this;
},
// Disable .fire and .add
// Abort any current/pending executions
// Clear all callbacks and values
// 置locked為[],即!![] === true,同時將佇列和列表都清空,即禁用了該回撥集合
disable: function() {
locked = queue = [];
list = memory = "";
return this;
},
disabled: function() {
return !list;
},
// Disable .fire
// Also disable .add unless we have memory (since it would have no effect)
// Abort any pending executions
// 不允許執行,但如果有快取,則我們允許新增後在快取的環境下執行新新增的回撥
lock: function() {
locked = queue = [];
if ( !memory && !firing ) {
list = memory = "";
}
return this;
},
locked: function() {
return !!locked;
},
// Call all callbacks with the given context and arguments
// 為fire附帶了一個上下文來呼叫fire函式,
fireWith: function( context, args ) {
if ( !locked ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
queue.push( args );
if ( !firing ) {
fire();
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};
return self;
};
jQuery.Deferred
物件
jQuery.Deferred
物件是一個工廠函式,返回一個用於非同步或同步呼叫的deferred
物件,支援鏈式呼叫、回撥函式佇列,並且能針對返回的狀態不同執行不同的回撥。它類似於ES6
提供的Promise
物件,提供9個主要的方法:
-
done
: 操作成功響應時的回撥函式(同步或非同步,以下相同) -
fail
: 操作失敗響應時的回撥函式 -
progress
: 操作處理過程中的回撥函式 -
resolve
: 通過該方法解析該操作為成功狀態,呼叫done -
reject
: 通過該方法解析該操作為失敗狀態,呼叫fail -
notify
: 通過該方法解析該操作為執行過程中,呼叫progress -
then
: 設定回撥的簡寫,接收三個引數,分別是done/fail/progress -
always
: 設定必須執行的回撥,無論是done還是fail -
promise
: 返回一個受限制的Deferred物件,不允許外部直接改變完成狀態
它的實現思想是建立一個物件,包含不同狀態下回撥函式的佇列,並在狀態為失敗或成功後不允許再次改變。通過返回的Deferred
物件進行手動呼叫resolve/reject/notify
方法來控制流程。
看一個例項(純屬胡扯,不要當真)。我們需要從間諜衛星返回的資料用不同的演算法來進行解析,如果解析結果訊號強度大於90%,則證明該資料有效,可以被解析;如果強度小於10%,則證明只是宇宙噪音;否則,證明資料可能有效,換一種演算法解析:
// 我們封裝Deferred產生一個promise物件,其不能被外部手動解析,只能內部確定最終狀態
asynPromise = function () {
let d = $.Deferred();
(function timer() {
setTimeout(function () {
// 產生隨機數,代替解析結果,來確定本次的狀態
let num = Math.random();
if (num > 0.9) {
d.resolve(); // 解析成功
} else if (num < 0.1) {
d.reject(); // 解析失敗
} else {
d.notify(); // 解析過程中
}
setTimeout(timer, 1000); // 持續不斷的解析資料
}, 1000);
})();
// 如果不返回promise物件,則可以被外部手動調整解析狀態
return d.promise();
};
// then方法的三個引數分別代表完成、失敗、過程中的回撥函式
asynPromise().then(function () {
console.log(`resolve success`);
}, function () {
console.log(`reject fail`);
}, function () {
console.log(`notify progress`);
});
// 本地執行結果(每個人的不一樣,隨機分佈,但最後一個一定是success或fail)
notify progress
notify progress
notify progress
notify progress
notify progress
reject fail // 後面不會再有輸出,因為一旦解析狀態為success或fail,則不會再改變
除了上面的主要功能,還提供了notifyWith/resolveWith/rejectWith/state
輔助方法。
其所有的原始碼實現和註釋為:
Deferred: function( func ) {
var tuples = [
// action, add listener, callbacks,
// ... .then handlers, argument index, [final state]
// 用於後面進行第一個引數繫結呼叫第二個引數,第三個和第四個引數分別是其不同的回撥函式佇列
[ "notify", "progress", jQuery.Callbacks( "memory" ),
jQuery.Callbacks( "memory" ), 2 ],
[ "resolve", "done", jQuery.Callbacks( "once memory" ),
jQuery.Callbacks( "once memory" ), 0, "resolved" ],
[ "reject", "fail", jQuery.Callbacks( "once memory" ),
jQuery.Callbacks( "once memory" ), 1, "rejected" ]
],
state = "pending",
promise = {
state: function() {
return state;
},
// 同時新增done和fail控制程式碼
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
"catch": function( fn ) {
return promise.then( null, fn );
},
then: function( onFulfilled, onRejected, onProgress ) {
var maxDepth = 0;
function resolve( depth, deferred, handler, special ) {
return function() {
var that = this,
args = arguments,
mightThrow = function() {
var returned, then;
// Support: Promises/A+ section 2.3.3.3.3
// https://promisesaplus.com/#point-59
// Ignore double-resolution attempts
if ( depth < maxDepth ) {
return;
}
returned = handler.apply( that, args );
// Support: Promises/A+ section 2.3.1
// https://promisesaplus.com/#point-48
if ( returned === deferred.promise() ) {
throw new TypeError( "Thenable self-resolution" );
}
// Support: Promises/A+ sections 2.3.3.1, 3.5
// https://promisesaplus.com/#point-54
// https://promisesaplus.com/#point-75
// Retrieve `then` only once
then = returned &&
// Support: Promises/A+ section 2.3.4
// https://promisesaplus.com/#point-64
// Only check objects and functions for thenability
( typeof returned === "object" ||
typeof returned === "function" ) &&
returned.then;
// Handle a returned thenable
if ( jQuery.isFunction( then ) ) {
// Special processors (notify) just wait for resolution
if ( special ) {
then.call(
returned,
resolve( maxDepth, deferred, Identity, special ),
resolve( maxDepth, deferred, Thrower, special )
);
// Normal processors (resolve) also hook into progress
} else {
// ...and disregard older resolution values
maxDepth++;
then.call(
returned,
resolve( maxDepth, deferred, Identity, special ),
resolve( maxDepth, deferred, Thrower, special ),
resolve( maxDepth, deferred, Identity,
deferred.notifyWith )
);
}
// Handle all other returned values
} else {
// Only substitute handlers pass on context
// and multiple values (non-spec behavior)
if ( handler !== Identity ) {
that = undefined;
args = [ returned ];
}
// Process the value(s)
// Default process is resolve
( special || deferred.resolveWith )( that, args );
}
},
// Only normal processors (resolve) catch and reject exceptions
// 只有普通的process能處理異常,其餘的要進行捕獲,這裡不是特別明白,應該是因為沒有改最終的狀態吧
process = special ?
mightThrow :
function() {
try {
mightThrow();
} catch ( e ) {
if ( jQuery.Deferred.exceptionHook ) {
jQuery.Deferred.exceptionHook( e,
process.stackTrace );
}
// Support: Promises/A+ section 2.3.3.3.4.1
// https://promisesaplus.com/#point-61
// Ignore post-resolution exceptions
if ( depth + 1 >= maxDepth ) {
// Only substitute handlers pass on context
// and multiple values (non-spec behavior)
if ( handler !== Thrower ) {
that = undefined;
args = [ e ];
}
deferred.rejectWith( that, args );
}
}
};
// Support: Promises/A+ section 2.3.3.3.1
// https://promisesaplus.com/#point-57
// Re-resolve promises immediately to dodge false rejection from
// subsequent errors
if ( depth ) {
process();
} else {
// Call an optional hook to record the stack, in case of exception
// since it`s otherwise lost when execution goes async
if ( jQuery.Deferred.getStackHook ) {
process.stackTrace = jQuery.Deferred.getStackHook();
}
window.setTimeout( process );
}
};
}
return jQuery.Deferred( function( newDefer ) {
// progress_handlers.add( ... )
tuples[ 0 ][ 3 ].add(
resolve(
0,
newDefer,
jQuery.isFunction( onProgress ) ?
onProgress :
Identity,
newDefer.notifyWith
)
);
// fulfilled_handlers.add( ... )
tuples[ 1 ][ 3 ].add(
resolve(
0,
newDefer,
jQuery.isFunction( onFulfilled ) ?
onFulfilled :
Identity
)
);
// rejected_handlers.add( ... )
tuples[ 2 ][ 3 ].add(
resolve(
0,
newDefer,
jQuery.isFunction( onRejected ) ?
onRejected :
Thrower
)
);
} ).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
// 通過該promise物件返回一個新的擴充套件promise物件或自身
promise: function( obj ) {
return obj != null ? jQuery.extend( obj, promise ) : promise;
}
},
deferred = {};
// Add list-specific methods
// 給promise新增done/fail/progress事件,並新增互相的影響關係,併為deferred物件新增3個事件函式notify/resolve/reject
jQuery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ],
stateString = tuple[ 5 ];
// promise.progress = list.add
// promise.done = list.add
// promise.fail = list.add
promise[ tuple[ 1 ] ] = list.add;
// Handle state
// 只有done和fail有resolved和rejected狀態欄位,給兩個事件新增回撥,禁止再次done或者fail,鎖住progress不允許執行回撥
if ( stateString ) {
list.add(
function() {
// state = "resolved" (i.e., fulfilled)
// state = "rejected"
state = stateString;
},
// rejected_callbacks.disable
// fulfilled_callbacks.disable
tuples[ 3 - i ][ 2 ].disable,
// progress_callbacks.lock
tuples[ 0 ][ 2 ].lock
);
}
// progress_handlers.fire
// fulfilled_handlers.fire
// rejected_handlers.fire
// 執行第二個回撥列表
list.add( tuple[ 3 ].fire );
// deferred.notify = function() { deferred.notifyWith(...) }
// deferred.resolve = function() { deferred.resolveWith(...) }
// deferred.reject = function() { deferred.rejectWith(...) }
// 繫結notify/resolve/reject的事件,實際執行的函式體為加入上下文的With函式
deferred[ tuple[ 0 ] ] = function() {
deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
return this;
};
// deferred.notifyWith = list.fireWith
// deferred.resolveWith = list.fireWith
// deferred.rejectWith = list.fireWith
deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
} );
// Make the deferred a promise
// 將deferred擴充套件為一個promise物件
promise.promise( deferred );
// Call given func if any
// 在建立前執行傳入的回撥函式進行修改
if ( func ) {
func.call( deferred, deferred );
}
// All done!
return deferred;
},
jQuery.when
方法
$.when()
提供一種方法執行一個或多個函式的回撥函式。如果傳入一個延遲物件,則返回該物件的Promise物件,可以繼續繫結其餘回撥,在執行結束狀態之後也同時呼叫其when
回撥函式。如果傳入多個延遲物件,則返回一個新的master
延遲物件,跟蹤所有的聚集狀態,如果都成功解析完成,才呼叫其when
回撥函式;如果有一個失敗,則全部失敗,執行錯誤回撥。
其使用方法:
$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
.then(myFunc, myFailure);
其所有原始碼實現和註釋為(能力有限,有些地方實在不能準確理解執行流程):
// 給when傳遞的物件繫結master.resolve和master.reject,用於聚集多非同步物件的狀態
function adoptValue( value, resolve, reject, noValue ) {
var method;
try {
// Check for promise aspect first to privilege synchronous behavior
// 如果when傳入的引數promise方法可用,則封裝promise並新增done和fail方法呼叫resolve和reject
if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
method.call( value ).done( resolve ).fail( reject );
// Other thenables
// 否則,就判斷傳入引數的then方法是否可用,如果可用就傳入resolve和reject方法
} else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
method.call( value, resolve, reject );
// Other non-thenables
// 如果均不可用,則為非非同步物件,直接resolve解析原值
} else {
// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
// * false: [ value ].slice( 0 ) => resolve( value )
// * true: [ value ].slice( 1 ) => resolve()
resolve.apply( undefined, [ value ].slice( noValue ) );
}
// For Promises/A+, convert exceptions into rejections
// Since jQuery.when doesn`t unwrap thenables, we can skip the extra checks appearing in
// Deferred#then to conditionally suppress rejection.
} catch ( value ) {
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
// 一個安卓4.0的bug,這裡不做闡釋
reject.apply( undefined, [ value ] );
}
}
// Deferred helper
when: function( singleValue ) {
var
// count of uncompleted subordinates
remaining = arguments.length,
// count of unprocessed arguments
i = remaining,
// subordinate fulfillment data
resolveContexts = Array( i ),
resolveValues = slice.call( arguments ),
// the master Deferred
master = jQuery.Deferred(),
// subordinate callback factory
// 將每一個響應的環境和值都儲存到列表裡,在全部完成後統一傳給主Promise用於執行
updateFunc = function( i ) {
return function( value ) {
resolveContexts[ i ] = this;
resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( !( --remaining ) ) {
master.resolveWith( resolveContexts, resolveValues );
}
};
};
// Single- and empty arguments are adopted like Promise.resolve
// 如果只有一個引數,則直接將其作為master的回撥
if ( remaining <= 1 ) {
adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
!remaining );
// Use .then() to unwrap secondary thenables (cf. gh-3000)
if ( master.state() === "pending" ||
jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
return master.then();
}
}
// Multiple arguments are aggregated like Promise.all array elements
// 多引數時,進行所有引數的解析狀態聚合到master上
while ( i-- ) {
adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
}
return master.promise();
}
後續
本來想把jQuery.Deferred
和jQuery.ajax
以及ES6
的Promise
物件給統一講一下,結果發現牽涉的東西太多,每一個都可以單獨寫一篇文章,怕大家說太長不看,這裡先寫第一部分jQuery.Deferred
吧,後續再補充另外兩篇。
看jQuery
的文件很容易,使用也很方便,但其實真正想要講好很複雜,更不要說寫篇原始碼分析文章了。真的是努力理解設計者的思路,爭取每行都能理解邊界條件,但踩坑太少,應用場景太少,確實有很大的疏漏,希望大家能夠理解,不要偏聽一面之詞。
參考資料
- jQuery – Callbacks: http://api.jquery.com/jQuery….
- segment – jQuery Callbacks: https://segmentfault.com/a/11…
- jQuery-3.2.1版本
- jQuery – Deferred: http://api.jquery.com/jQuery….
- jQuery – when: http://www.jquery123.com/jQue…
- cnblogs – 搞懂jQuery的Promise: http://www.cnblogs.com/lvdaba…
- Promise A+ 規範: http://malcolmyu.github.io/ma…