用過jQuery的朋友都知道他強大的鏈式操作,方便,簡潔,易於理解,如下
1
2
3
|
$(
"has_children"
).click(
function
(){
$(
this
).addClass(
"highlight"
).children(
"a"
).show().end().siblings().removeClass(
"highlight"
).children(
"a"
).hide();
});
|
1.jQuery的鏈式操作是如何實現的?
2.為什么要用鏈式操作?
鏈式操作
原理相信百度一下一大把,實際上鏈式操作僅僅是通過對象上的方法最后
1
|
return
this
|
把對象再返回回來,對象當然可以繼續調用方法啦,所以就可以鏈式操作了。那么,簡單實現一個:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//定義一個JS類
function
Demo() {
}
//擴展它的prototype
Demo.prototype ={
setName:
function
(name) {
this
.name = name;
return
this
;
},
getName:
function
() {
return
this
.name;
},
setAge:
function
(age) {
this
.age = age;
return
this
;
}
};
////工廠函數
function
D() {
return
new
Demo();
}
//去實現可鏈式的調用
D().setName(
"CJ"
).setAge(18).setName();
|
但……為什么要用呢?
一般的解釋:節省代碼量,代碼看起來更優雅。
例如如果沒有鏈式,那么你可能需要這樣寫代碼:
1
2
|
document.getElementById(
"ele"
).dosomething();
document.getElementById(
"ele"
).dootherthing();
|
這個代碼中調用了兩次document.getElementById來獲取DOM樹的元素,這樣消耗比較大,而且要寫兩行,而鏈式只要寫一行,節省了代碼……
但我們也可以用緩存元素啊。比如:
1
2
3
|
var
ele = document.getElementById(
"ele"
);
ele.dosomething();
ele.dootherthing();
|
而且兩行並沒有比一行多多少代碼,甚至相應的封裝反而使得代碼更多了。
最糟糕的是所有對象的方法返回的都是對象本身,也就是說沒有返回值,這不一定在任何環境下都適合。
舉個例子,我們想弄一個超大整數BigInteger(意思是如果用Javascript的Number保存可能會溢出的整數),順便擴展他的運算方法,會適合用鏈式操作么?
例如運算31415926535 * 4 - 271828182,如果設計成鏈式風格的方法可能會是這樣的:
1
2
|
var
result = (
new
BigInteger(
"31415926535"
)).multiply(
new
BigInteger(
"4"
)).subtract(
new
BigInteger(
"271828182"
)).val();
console.log(
"result == "
+ result);
|
這看起來似乎也很優雅,但是如果我們想要中間的結果怎么辦呢?或許會寫成這樣:
1
2
3
4
|
var
bigInteger =
new
BigInteger(
"31415926535"
);
var
result1 = bigInteger.multiply(
new
BigInteger(
"4"
)).val();
var
result2 = bigInteger.subtract(
new
BigInteger(
"271828182"
)).val();
console.log(
"result1 == "
+ result1 +
", result2 == "
+ result2);
|
這似乎一點也不優雅了,和不用鏈式操作沒啥不同嘛!
那么如果要求是原來的BigInteger不能改變呢?好吧,鏈式操作似乎不能滿足這個需求了。
jQuery專注於DOM對象操作,而DOM的操作會在頁面上體現,不需要在Javascript中通過返回值來表示,但計算操作卻不一樣,我們很可能需要通過Javascript返回中間過程值另作他用。
在設計的時候,我們需要考慮鏈式帶來的好處和壞處,因為別人用了鏈式,所以就用鏈式,可能並不是一個很好的方案。
那么到底為什么要用鏈式操作呢?
為了更好的異步體驗
Javascript是無阻塞語言,所以他不是沒阻塞,而是不能阻塞,所以他需要通過事件來驅動,異步來完成一些本需要阻塞進程的操作。
但是異步編程是一種令人瘋狂的東西……運行時候是分離的倒不要緊,但是編寫代碼時候也是分離的就……
常見的異步編程模型有哪些呢?
-
回調函數 所謂的回調函數,意指先在系統的某個地方對函數進行注冊,讓系統知道這個函數的存在,然后在以后,當某個事件發生時,再調用這個函數對事件進行響應。
1
2
3
4
5
6
7
8
9
10
11
12
|
function
f(num, callback){
if
(num<0) {
alert(
"調用低層函數處理!"
);
alert(
"分數不能為負,輸入錯誤!"
);
}
else
if
(num==0){
alert(
"調用低層函數處理!"
);
alert(
"該學生可能未參加考試!"
);
}
else
{
alert(
"調用高層函數處理!"
);
setTimeout(
function
(){callback();}, 1000);
}
}
|
這里callback則是回調函數。可以發現只有當num為非負數時候callback才會調用。
但是問題,如果我們不看函數內部,我們並不知道callback會幾時調用,在什么情況下調用,代碼間產生了一定耦合,流程上也會產生一定的混亂。
雖然回調函數是一種簡單而易於部署的實現異步的方法,但從編程體驗來說它卻不夠好。
-
事件監聽 也就是采用事件驅動,執行順序取決於事件順序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
function
EventTarget(){
this
.handlers = {};
}
EventTarget.prototype = {
constructor: EventTarget,
addHandler:
function
(type, handler){
this
.handlers[type] = [];
},
fire:
function
(){
if
(!event.target){
event.target =
this
;
}
if
(
this
.handlers[event.type
instanceof
Array]){
var
handlers =
this
.handlers[event.type];
for
(
var
i = 0, len = handlers.length, i < len; i++){
handlers[i](event);
}
}
},
removeHandler:
function
(type, handler){
if
(
this
.handlers[type]
instanceof
Array){
var
handlers =
this
.handlers[type];
for
(
var
i = 0, le = handlers.length; i < len; i++){
if
(handlers[i] === handler){
break
;
}
}
handlers.splice(i, 1);
}
}
};
|
上面是《JavaScript高級程序設計》中的自定義事件實現。於是我們就可以通過addHandler來綁定事件處理函數,用fire來觸發事件,用removeHandler來刪除事件處理函數。
雖然通過事件解耦了,但流程順序更加混亂了。
-
鏈式異步
個人覺得鏈式操作最值得稱贊的還是其解決了異步編程模型的執行流程不清晰的問題。jQuery中$(document).ready就非常好的闡釋了這一理念。DOMCotentLoaded是一個事件,在DOM並未加載前,jQuery的大部分操作都不會奏效,但jQuery的設計者並沒有把他當成事件一樣來處理,而是轉成一種“選其對象,對其操作”的思路。$選擇了document對象,ready是其方法進行操作。這樣子流程問題就非常清晰了,在鏈條越后位置的方法就越后執行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
(
function
(){
var
isReady=
false
;
//判斷onDOMReady方法是否已經被執行過
var
readyList= [];
//把需要執行的方法先暫存在這個數組里
var
timer;
//定時器句柄
ready=
function
(fn) {
if
(isReady )
fn.call( document);
else
readyList.push(
function
() {
return
fn.call(
this
);});
return
this
;
}
var
onDOMReady=
function
(){
for
(
var
i=0;i<readyList.length;i++){
readyList[i].apply(document);
}
readyList =
null
;
}
var
bindReady =
function
(evt){
if
(isReady)
return
;
isReady=
true
;
onDOMReady.call(window);
if
(document.removeEventListener){
document.removeEventListener(
"DOMContentLoaded"
, bindReady,
false
);
}
else
if
(document.attachEvent){
document.detachEvent(
"onreadystatechange"
, bindReady);
if
(window == window.top){
clearInterval(timer);
timer =
null
;
}
}
};
if
(document.addEventListener){
document.addEventListener(
"DOMContentLoaded"
, bindReady,
false
);
}
else
if
(document.attachEvent){
document.attachEvent(
"onreadystatechange"
,
function
(){
if
((/loaded|complete/).test(document.readyState))
bindReady();
});
if
(window == window.top){
timer = setInterval(
function
(){
try
{
isReady||document.documentElement.doScroll(
'left'
);
//在IE下用能否執行doScroll判斷dom是否加載完畢
}
catch
(e){
return
;
}
bindReady();
},5);
}
}
})();
|
上面的代碼不能用$(document).ready,而應該是window.ready。
-
Deferred & Promise
CommonJS中的異步編程模型也延續了這一想法,每一個異步任務返回一個Promise對象,該對象有一個then方法,允許指定回調函數。
所以我們可以這樣寫:
1
|
f1().then(f2).then(f3);
|