第二十二章:高級(jí)技巧
本章內(nèi)容:
- 使用高階函數(shù)
- 防篡改對(duì)象
- Yieding Timers
22.1 高級(jí)函數(shù)
22.1.3 惰性載入
因?yàn)闉g覽器之間的行為差異,多數(shù)javascript代碼包含了大量的if語句,將執(zhí)行引導(dǎo)到正確的代碼中。看看上面一章的createXHR()函數(shù)
function createXHR(){
if(typeof XMLHttpRequest != 'undefined'){
return new XMLHttpRequest();
} else if(typeof ActiveXObject != 'undefined'){
if(typeof arguments.callee.activeXString != 'string'){
// 跳過
}
return new ActiveXObject(arguments.callee.activeXString)
} else {
throw new Error('No XHR object')
}
}
每次調(diào)用createXHR的時(shí)候,它都要對(duì)瀏覽器所支持的能力仔細(xì)檢查,影響效率。如果if語句不必每次執(zhí)行,這種解決方案就是惰性載入。
惰性摘入表示函數(shù)執(zhí)行的分支僅會(huì)發(fā)生一次。有兩種實(shí)現(xiàn)惰性載入的方式。
第一種方案
在函數(shù)被調(diào)用時(shí)再處理函數(shù)。該函數(shù)會(huì)被覆蓋為另一個(gè)按合適方式執(zhí)行的函數(shù)。
function createXHR(){
if(typeof XMLHttpRequest != 'undefined'){
createXHR = function(){
return new XMLHttpRequest();
}
} else if(typeof ActiveXObject != 'undefined'){
createXHR = function(){
if(typeof arguments.callee.activeXString != 'string'){
// 跳過
}
return new ActiveXObject(arguments.callee.activeXString)
}
} else {
createXHR = function(){
throw new Error('No XHR object')
}
}
return createXHR();
}
在這種惰性摘入的createXHR()中,if語句的每一個(gè)分支都會(huì)重新覆蓋createXHR變量賦值,有效的覆蓋了原有的函數(shù)。最后一步就是調(diào)用新賦值的函數(shù)。
第二種方案
在聲明函數(shù)的時(shí)候就指定適當(dāng)?shù)暮瘮?shù)。這樣,在第一次調(diào)用的時(shí)候就不會(huì)損失性能,
var createXHR = (function(){
if(typeof XMLHttpRequest != 'undefined'){
return function(){
new XMLHttpRequest();
}
} else if(typeof ActiveXObject != 'undefined'){
return function(){
if(typeof arguments.callee.activeXString != 'string'){
// 跳過
}
return new ActiveXObject(arguments.callee.activeXString)
}
} else {
return function(){
throw new Error('No XHR object')
}
}
})();
這個(gè)例子使用的技巧是創(chuàng)建一個(gè)匿名、自執(zhí)行的函數(shù),用以確定使用哪一個(gè)函數(shù)實(shí)現(xiàn)。另外每個(gè)分支都會(huì)返回正確的函數(shù)定義,以便立即將其賦值給createXHR()。
惰性載入函數(shù)的優(yōu)點(diǎn)就是指在執(zhí)行分支的時(shí)候犧牲一點(diǎn)性能。
22.1.4 函數(shù)綁定
函數(shù)綁定要?jiǎng)?chuàng)建一個(gè)函數(shù),可以在特定的this環(huán)境中以指定參數(shù)調(diào)用另一個(gè)函數(shù)。這技巧常常和回調(diào)函數(shù)與事件處理程序一起使用,以便在將函數(shù)作為變量傳遞的同事保留代碼執(zhí)行環(huán)境。
var handler = {
message: 'Event handled',
handleClick: function(event){
alert(this.message);
}
}
var btn = document.querySelector('#my-btn');
btn.addEventListener('click',handler.handleClick);
點(diǎn)擊按鈕實(shí)際顯示結(jié)果為undefined。這個(gè)問題是處理函數(shù)直接引用對(duì)象的方法,方法被獨(dú)立調(diào)用,沒有保存handler.handleClick()的環(huán)境,this最后指向的是DOM元素而非handler。
從圖可知當(dāng)前活動(dòng)對(duì)象有兩個(gè)變量event和this。 this指向的偽button元素。
我們可以用閉包來修正這個(gè)問題
var handler = {
message: 'Event handled',
handleClick: function(event){
alert(this.message);
}
}
var btn = document.querySelector('#my-btn');
btn.addEventListener('click',function(event){handler.handleClick(event)});
創(chuàng)建多個(gè)閉包變得難以理解,于是很多庫都實(shí)現(xiàn)了一個(gè)bind函數(shù)
// 自己定義bind 函數(shù)
function bind(fn,context){
return function(){
return fn.apply(context,context);
}
}
// 調(diào)用
btn.addEventListener('click',bind(handler.handleClick, handler));
ECMAScript5為所有函數(shù)定義了一個(gè)原生的bind方法
btn.addEventListener('click',handler.handleClick.bind(handler));
只要是將某個(gè)函數(shù)指針以值得形式進(jìn)行傳遞,同時(shí)該函數(shù)必須在特定環(huán)境中執(zhí)行,被綁定函數(shù)的效用就凸顯出來了。他們主要用于事件處理、setTimeout()和setInterval()。
22.1.5 函數(shù)柯里化
與函數(shù)緊密相關(guān)的主題是函數(shù)柯里化(function curring),它用于創(chuàng)建已經(jīng)設(shè)置好一個(gè)或者多個(gè)參數(shù)的函數(shù)。函數(shù)柯里化的基本方法與函數(shù)綁定以一樣。使用一個(gè)閉包返回一個(gè)函數(shù)。兩者的區(qū)別在于,當(dāng)函數(shù)調(diào)用的時(shí)候,返回的函數(shù)還需要設(shè)置一些傳入的參數(shù)。可以看下面的例子
function add(num1, num2){
return num1 + num2;
}
function curriedAdd(num2){
return 5 + num2;
}
alert(add(5,2)); // 7
alert(curriedAdd(2)); // 7
盡管上面的例子并非柯里化的函數(shù),但很好講出了其概念。
柯里化函數(shù)通常由以下步驟動(dòng)態(tài)創(chuàng)建:調(diào)用另一個(gè)函數(shù)并為它傳遞要柯里化的函數(shù)和必要參數(shù)。下面是創(chuàng)建柯里化的函數(shù)的通用方式
// 自己定義curry函數(shù)
function curry(fn){
var args = Array.prototype.slice.call(arguments,1); //獲取除了fn,要傳遞給curry的參數(shù)
return function(){
var innerArgs = Array.prototype.slice.call(arguments); // 內(nèi)部函數(shù)的參數(shù),以后的真正傳參
var finalArgs = args.concat(innerArgs);
return fn.apply(null,finalArgs)
}
}
function add(num1,num2){
return num1 + num2;
}
var curriedAdd = curry(add,5);
alert(curriedAdd(2)); //7
函數(shù)柯里化還常常作為函數(shù)綁定的一部分,構(gòu)造更復(fù)雜的bind
// 增強(qiáng)型bind 可以傳遞參數(shù)
function bind(fn,context){
var args = Array.prototype.slice.call(arguments, 2);
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(args,innerArgs);
return fn.apply(context,finalArgs);
}
}
var handler = {
message: 'Event handled',
handleClick: function(name,event){
alert(this.message +":" +name +"," +event.type);
}
}
var btn = document.querySelector('#my-btn');
btn.addEventListener('click',bind(handler.handleClick,handler,'my-btn'));
es5中的bind()方法也實(shí)現(xiàn)了柯里化,只要在this的值后面再傳入另一個(gè)參數(shù)即可:
btn.addEventListener('click',handler.handleClick.bind(handler,'my-btn');
延伸閱讀1: 深入詳解函數(shù)的柯里化
柯里化是指這樣一個(gè)函數(shù)(假設(shè)叫做createCurry),他接收函數(shù)A作為參數(shù),運(yùn)行后能夠返回一個(gè)新的函數(shù)。并且這個(gè)新的函數(shù)能夠處理函數(shù)A的剩余參數(shù)。
這樣的定義可能不太好理解,我們可以通過下面的例子配合理解。
假如有一個(gè)接收三個(gè)參數(shù)的函數(shù)A。
function A(a, b, c) {
// do something
}
又假如我們有一個(gè)已經(jīng)封裝好了的柯里化通用函數(shù)createCurry。他接收bar作為參數(shù),能夠?qū)轉(zhuǎn)化為柯里化函數(shù),返回結(jié)果就是這個(gè)被轉(zhuǎn)化之后的函數(shù)。
var _A = createCurry(A);
那么_A作為createCurry運(yùn)行的返回函數(shù),他能夠處理A的剩余參數(shù)。因此下面的運(yùn)行結(jié)果都是等價(jià)的。
_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);
函數(shù)A被createCurry轉(zhuǎn)化之后得到柯里化函數(shù)_A,_A能夠處理A的所有剩余參數(shù)。因此柯里化也被稱為部分求值。
在簡(jiǎn)單的場(chǎng)景下,我們可以不用借助柯里化通用式來轉(zhuǎn)化得到柯里化函數(shù),我們可以憑借眼力自己封裝。
例如有一個(gè)簡(jiǎn)單的加法函數(shù),他能夠?qū)⒆陨淼娜齻€(gè)參數(shù)加起來并返回計(jì)算結(jié)果。
function add(a, b, c) {
return a + b + c;
}
那么add函數(shù)的柯里化函數(shù)_add則可以如下:
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
因此下面的運(yùn)算方式是等價(jià)的。
add(1, 2, 3);
_add(1)(2)(3);
當(dāng)然,柯里化通用式具備更加強(qiáng)大的能力,我們靠眼力自己封裝的柯里化函數(shù)則自由度偏低。因此我們?nèi)匀恍枰雷约喝绾稳シ庋b這樣一個(gè)柯里化的通用式。
首先通過_add可以看出,柯里化函數(shù)的運(yùn)行過程其實(shí)是一個(gè)參數(shù)的收集過程,我們將每一次傳入的參數(shù)收集起來,并在最里層里面處理。因此我們?cè)趯?shí)現(xiàn)createCurry時(shí),可以借助這個(gè)思路來進(jìn)行封裝。
// 簡(jiǎn)單實(shí)現(xiàn),參數(shù)只能從右到左傳遞
function createCurry(func,args){
args = args || [];
var arity = func.length;
return function(){
var _args = [].slice.apply(arguments);
var finalArgs = args.concat(_args);
if(finalArgs.length < arity){
return createCurry.call(this,func,finalArgs)
}
return func.apply(this,finalArgs)
}
}
function add(num1,num2,num3){
return num1 + num2 + num3;
}
var _add = createCurry(add);
console.log(_add(1,2)(3)) // 6
createCurry函數(shù)的封裝借助閉包與遞歸,實(shí)現(xiàn)了一個(gè)參數(shù)收集,并在收集完畢之后執(zhí)行所有參數(shù)的一個(gè)過程。
因此聰明的讀者可能已經(jīng)發(fā)現(xiàn),把函數(shù)經(jīng)過createCurry轉(zhuǎn)化為一個(gè)柯里化函數(shù),最后執(zhí)行的結(jié)果,不是正好相當(dāng)于執(zhí)行函數(shù)自身嗎?柯里化是不是把簡(jiǎn)單的問題復(fù)雜化了?
如果你能夠提出這樣的問題,那么說明你確實(shí)已經(jīng)對(duì)柯里化有了一定的了解。柯里化確實(shí)是把簡(jiǎn)答的問題復(fù)雜化了,但是復(fù)雜化的同時(shí),我們?cè)谑褂煤瘮?shù)時(shí)擁有了更加多的自由度。而這里對(duì)于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。
我們來舉一個(gè)非常常見的例子。
如果我們想要驗(yàn)證一串?dāng)?shù)字是否是正確的手機(jī)號(hào),那么按照普通的思路來做,大家可能是這樣封裝,如下:
function checkPhone(phoneNumber) {
return /^1[34578]\d{9}$/.test(phoneNumber);
}
而如果我們想要驗(yàn)證是否是郵箱呢?這么封裝:
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}
我們還可能會(huì)遇到驗(yàn)證身份證號(hào),驗(yàn)證密碼等各種驗(yàn)證信息,因此在實(shí)踐中,為了統(tǒng)一邏輯,,我們就會(huì)封裝一個(gè)更為通用的函數(shù),將用于驗(yàn)證的正則與將要被驗(yàn)證的字符串作為參數(shù)傳入。
function check(reg, targetString) {
return reg.test(targetString);
}
但是這樣封裝之后,在使用時(shí)又會(huì)稍微麻煩一點(diǎn),因?yàn)闀?huì)總是輸入一串正則,這樣就導(dǎo)致了使用時(shí)的效率低下。
check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
那么這個(gè)時(shí)候,我們就可以借助柯里化,在check的基礎(chǔ)上再做一層封裝,以簡(jiǎn)化使用。
var _check = createCurry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
最后在使用的時(shí)候就會(huì)變得更加直觀與簡(jiǎn)潔了。
checkPhone('183888888');
checkEmail('xxxxx@test.com');
經(jīng)過這個(gè)過程我們發(fā)現(xiàn),柯里化能夠應(yīng)對(duì)更加復(fù)雜的邏輯封裝。當(dāng)情況變得多變,柯里化依然能夠應(yīng)付自如。
我們繼續(xù)來思考一個(gè)例子。這個(gè)例子與map有關(guān),由于我們沒有辦法確認(rèn)一個(gè)數(shù)組在遍歷時(shí)會(huì)執(zhí)行什么操作,因此我們只能將調(diào)用for循環(huán)的這個(gè)統(tǒng)一邏輯封裝起來,而具體的操作則通過參數(shù)傳入的形式讓使用者自定義。這就是map函數(shù)。
但是,這是針對(duì)了所有的情況我們才會(huì)這樣想。
實(shí)踐中我們常常會(huì)發(fā)現(xiàn),在我們的某個(gè)項(xiàng)目中,針對(duì)于某一個(gè)數(shù)組的操作其實(shí)是固定的,也就是說,同樣的操作,可能會(huì)在項(xiàng)目的不同地方調(diào)用很多次。
于是,這個(gè)時(shí)候,我們就可以在map函數(shù)的基礎(chǔ)上,進(jìn)行二次封裝,以簡(jiǎn)化我們?cè)陧?xiàng)目中的使用。假如這個(gè)在我們項(xiàng)目中會(huì)調(diào)用多次的操作是將數(shù)組的每一項(xiàng)都轉(zhuǎn)化為百分比 1 --> 100%。
普通思維下我們可以這樣來封裝。
// 普通思維下 數(shù)組的每一項(xiàng)都轉(zhuǎn)化為百分比 1 --> 100%。
function getNewArray(array) {
return array.map(function(item){
return item * 100 + '%'
})
}
getNewArray([1, 2, 3, 0.12]); // ['100%', '200%', '300%', '12%'];
而如果借助柯里化來二次封裝這樣的邏輯,則會(huì)如下實(shí)現(xiàn):
function _map(func,array){
return array.map(func);
}
var _getNewArray = createCurry(_map);
var getNewArray = _getNewArray(function(item){
return item * 100 + '%'
})
console.log(getNewArray([1, 2, 3, 0.12])); // ['100%', '200%', '300%', '12%'];
console.log(getNewArray([0.01, 1])); // ['1%', '100%']
如果我們的項(xiàng)目中的固定操作是希望對(duì)數(shù)組進(jìn)行一個(gè)過濾,找出數(shù)組中的所有Number類型的數(shù)據(jù)。借助柯里化思維我們可以這樣做。
function _filter(func,array){
return array.map(func);
}
var _find = createCurry(_filter);
var findNumber = _find(function(item) {
if (typeof item == 'number') {
return item;
}
})
findNumber([1, 2, 3, '2', '3', 4]); // [1, 2, 3, 4]
// 當(dāng)我們繼續(xù)封裝另外的過濾操作時(shí)就會(huì)變得非常簡(jiǎn)單
// 找出數(shù)字為20的子項(xiàng)
var find20 = _find(function(item, i) {
if (typeof item === 20) {
return i;
}
})
find20([1, 2, 3, 30, 20, 100]); // 4
// 找出數(shù)組中大于100的所有數(shù)據(jù)
var findGreater100 = _find(function(item) {
if (item > 100) {
return item;
}
})
findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]
我采用了與check例子不一樣的思維方向來想大家展示我們?cè)谑褂每吕锘瘯r(shí)的想法。目的是想告訴大家,柯里化能夠幫助我們應(yīng)對(duì)更多更復(fù)雜的場(chǎng)景。
22.3 高級(jí)定時(shí)器
關(guān)于這一章可以參考第13章--事件。
Javascript是運(yùn)行在單線程的環(huán)境中,而定時(shí)器僅僅是計(jì)劃在未來的某個(gè)時(shí)間執(zhí)行,執(zhí)行時(shí)機(jī)是不能確定的。實(shí)際上,瀏覽器負(fù)責(zé)進(jìn)行排序,指派某一段代碼在某個(gè)時(shí)間點(diǎn)運(yùn)行的優(yōu)先級(jí)。(這里還分宏任務(wù),和微任務(wù))
除了主javascript執(zhí)行進(jìn)程外,還有需要再進(jìn)程下一次空閑時(shí)執(zhí)行的代碼隊(duì)列。隨著頁面在其生命周期中的推移,代碼會(huì)按照?qǐng)?zhí)行順序添加到對(duì)應(yīng)的隊(duì)列中,并在下一個(gè)可能的時(shí)間里執(zhí)行。當(dāng)接受到某個(gè)ajax相應(yīng)時(shí),回調(diào)函數(shù)的代碼會(huì)被添加到隊(duì)列。在Javascript中沒有任何代碼時(shí)立即執(zhí)行的,但一旦進(jìn)程空閑則盡快執(zhí)行。
定時(shí)器對(duì)隊(duì)列的工作方式是,當(dāng)特定時(shí)間過去后將代碼插入。注意:給隊(duì)列添加代碼并不意味著對(duì)它執(zhí)行。
var btn = document.querySelector('#my-btn');
btn.onclick = function(){
setTimeout(function(){
// dosomething
},250)
// dosomething
}
在這里給一個(gè)按鈕設(shè)置了一個(gè)事件處理程序,事件處理程序設(shè)置了一個(gè)250ms后調(diào)用的定時(shí)器。點(diǎn)擊按鈕之后,首先onclick事件處理程序加入到隊(duì)列。改程序執(zhí)行后才能設(shè)置定時(shí)器,再有250ms后,指定的代碼才被添加到隊(duì)列中等待執(zhí)行。
關(guān)于定時(shí)器要記住最重要的事,指定的時(shí)間間隔表示何時(shí)將定時(shí)器的代碼添加到隊(duì)列,而不是何時(shí)實(shí)際執(zhí)行代碼。假設(shè)前面的例子onclick時(shí)間處理程序要執(zhí)行300ms。那么定時(shí)器的代碼至少要在定時(shí)器設(shè)置后的300ms后才會(huì)執(zhí)行。
如圖所示,盡管255ms處添加了定時(shí)器代碼,但這個(gè)時(shí)候還不能執(zhí)行,因?yàn)閛nclick事件處理程序仍在繼續(xù)。
22.3.1 重復(fù)定時(shí)器
使用setInterval()創(chuàng)建的定時(shí)器確保了定時(shí)器代碼規(guī)則的插入隊(duì)列中。這個(gè)方式問題在于,定時(shí)器代碼可能在代碼再次被添加到隊(duì)列之前還沒有完成執(zhí)行。當(dāng)使用setInterval時(shí),僅當(dāng)沒有該定時(shí)器的任何其他代碼時(shí),才能被添加到隊(duì)列中。
這種重復(fù)定時(shí)器規(guī)則有兩個(gè)問題:(1)某些間隔會(huì)被跳過。(2)多個(gè)定時(shí)器的代碼執(zhí)行之間的間隔可能會(huì)比預(yù)期的小。
假設(shè)onclick事件處理程序?yàn)?00ms的間隔,事件處理函數(shù)得300ms多一點(diǎn)的時(shí)間完成完成。就會(huì)同時(shí)出現(xiàn)跳過間隔連續(xù)運(yùn)行定時(shí)器代碼的問題。
這里例子的第一個(gè)定時(shí)器是在205ms被添加到隊(duì)列中的,但是知道300ms處才能夠執(zhí)行。當(dāng)執(zhí)行這個(gè)定時(shí)器代碼時(shí),在405ms處又給隊(duì)列添加了另外一個(gè)副本。在下一個(gè)間隔即605ms處,第一個(gè)定時(shí)器還在運(yùn)行,隊(duì)列中已經(jīng)有一個(gè)定時(shí)器的代碼實(shí)例了。結(jié)果是這個(gè)不會(huì)被添加到隊(duì)列中。
為了避免setInterval()重復(fù)定時(shí)器的2個(gè)缺點(diǎn),可以用如下模式使用鏈?zhǔn)絪etTimeout()
setTimeout(function(){
// 處理中
setTimeout(arguments.callee,interval)
},interval)
這樣做的好處是在前一個(gè)定時(shí)器代碼執(zhí)行之前,不會(huì)向隊(duì)列添加新的定時(shí)器代碼,確保不會(huì)有任何的缺失間隔。
arguments.callee在非嚴(yán)格模式下獲取當(dāng)前執(zhí)行函數(shù)的引用。
22.3.2 Yielding Processes
在展開該循環(huán)之前,你需要回答以下兩個(gè)重要的問題。
- 該處理的是否必須同步完成?如果這個(gè)數(shù)據(jù)的處理會(huì)造成其他運(yùn)行的阻塞,那么最好不要改動(dòng)它。
- 數(shù)據(jù)是否必須按照順序完成?
當(dāng)你發(fā)現(xiàn)某個(gè)循環(huán)占用了大量的時(shí)間,同時(shí)對(duì)于上面的問題,你的回答是‘否’。
定時(shí)器分割這個(gè)循環(huán),這是一種數(shù)組分塊(array chunking)的技術(shù),小塊小塊的處理數(shù)組。基本思路是為要處理的項(xiàng)目創(chuàng)建一個(gè)隊(duì)列,然后使用定時(shí)器取下笑一個(gè)要處理的項(xiàng)目進(jìn)行處理,接著在設(shè)置另一個(gè)定時(shí)器。基本模式如下:
setTimeout(function(){
// 取出下一個(gè)條目并處理
var item = array.shift(); //取出隊(duì)列的第一個(gè)
process(item);
// 如果還有條目
if(array.length > 0){
setTimeout(arguments.callee,100)
}
},100)
要實(shí)現(xiàn)數(shù)組分塊非常簡(jiǎn)單,可以使用下面函數(shù)
function chunck(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context,item);
if(array.length > 0){
setTimeout(arguments.callee,100)
}
},100)
}
下面是實(shí)例:
var data = [111,222,333,444,555,666,777,888,123,234,345,456,678,789,890];
function printValue(item){
console.log(item);
}
chunk(data,printValue);
應(yīng)該當(dāng)心的是,傳遞的是引用類型的數(shù)組,當(dāng)處理數(shù)據(jù)的時(shí)候,數(shù)組條目也在發(fā)生改變。如果想保持?jǐn)?shù)組不變,應(yīng)該將數(shù)組的克隆傳遞給chunk
chunk(data.concat(),printValue);
數(shù)組分塊的重要性在于它可以將多個(gè)項(xiàng)目的處理在執(zhí)行隊(duì)列上分開,在每個(gè)項(xiàng)目處理之后,給與其他的瀏覽器處理的機(jī)會(huì)運(yùn)行,這樣就可以避免長時(shí)間運(yùn)行腳本的錯(cuò)誤。
22.3.3 函數(shù)節(jié)流
某些高頻率的更改可能會(huì)讓瀏覽器崩潰。為了繞開這個(gè)問題,可以使用定時(shí)器對(duì)該函數(shù)進(jìn)行節(jié)流。
函數(shù)節(jié)流的背后思想是:某些代碼不可以在沒有時(shí)間間隔的情況下連續(xù)執(zhí)行。
第一次調(diào)用函數(shù)的時(shí)候,創(chuàng)建一個(gè)定時(shí)器,在指定的時(shí)間間隔之后運(yùn)行代碼。當(dāng)?shù)诙握{(diào)用該函數(shù)時(shí),它會(huì)清除前一次的定時(shí)器并設(shè)置另外一個(gè)。如果一個(gè)定時(shí)器已經(jīng)執(zhí)行過了,這個(gè)操作就沒有任何意義。然而,如果前一個(gè)定時(shí)器尚未執(zhí)行,其實(shí)就是將其替換為新的一個(gè)定時(shí)器。目的是只有在執(zhí)行函數(shù)的請(qǐng)求停止了一段時(shí)間之后才執(zhí)行。
var processor = {
timeoutId: null,
// 實(shí)際進(jìn)行處理的方法
performProcessing: function(){
//實(shí)際執(zhí)行的方法
},
// 初始處理調(diào)用方法
process: function(){
clearTimeout(this.timeOutId);
var that = this;
this.timeoutId = setTimeOut(function(){
that.performProcessing();
},100)
}
}
processor.process();
這個(gè)模式可以使用throttle函數(shù)來簡(jiǎn)化。
function throttle(method,context){
clearTimeout(method.tId);
method.tId = setTimeout(function(){
method.call(context);
},100)
}
throttle()函數(shù)接受兩個(gè)參數(shù):要執(zhí)行的函數(shù)與在哪個(gè)作用域執(zhí)行。上面這個(gè)函數(shù)首先清除之前設(shè)置的任何定時(shí)器。定時(shí)器ID是存儲(chǔ)在函數(shù)的tId屬性中。如果這是第一次對(duì)這個(gè)方法調(diào)用throttle()的話,那么這段代碼會(huì)生成該屬性。
function clickBox(){
console.log('click');
}
window.onresize = function(){
throttle(clickBox)
}
我覺得上面的throttle實(shí)現(xiàn)有問題
延伸閱讀2:Debounce 和 Throttle 的原理及實(shí)現(xiàn)
在處理諸如 resize
、scroll
、mousemove
和 keydown/keyup/keypress
等事件的時(shí)候,通常我們不希望這些事件太過頻繁地觸發(fā),尤其是監(jiān)聽程序中涉及到大量的計(jì)算或者有非常耗費(fèi)資源的操作。
可以參看這個(gè) Demo 體會(huì)下。
Debounce
DOM 事件里的 debounce
概念其實(shí)是從機(jī)械開關(guān)和繼電器的“去彈跳”(debounce)衍生 出來的,基本思路就是把多個(gè)信號(hào)合并為一個(gè)信號(hào)。這篇文章 解釋得非常清楚,感興趣的可以一讀。
在 JavaScript 中,debounce
函數(shù)所做的事情就是,強(qiáng)制一個(gè)函數(shù)在某個(gè)連續(xù)時(shí)間段內(nèi)只執(zhí)行一次,哪怕它本來會(huì)被調(diào)用多次。我們希望在用戶停止某個(gè)操作一段時(shí)間之后才執(zhí)行相應(yīng)的監(jiān)聽函數(shù),而不是在用戶操作的過程當(dāng)中,瀏覽器觸發(fā)多少次事件,就執(zhí)行多少次監(jiān)聽函數(shù)。
比如,在某個(gè) 3s 的時(shí)間段內(nèi)連續(xù)地移動(dòng)了鼠標(biāo),瀏覽器可能會(huì)觸發(fā)幾十(甚至幾百)個(gè) mousemove
事件,不使用 debounce
的話,監(jiān)聽函數(shù)就要執(zhí)行這么多次;如果對(duì)監(jiān)聽函數(shù)使用 100ms 的“去彈跳”,那么瀏覽器只會(huì)執(zhí)行一次這個(gè)監(jiān)聽函數(shù),而且是在第 3.1s 的時(shí)候執(zhí)行的。
現(xiàn)在,我們就來實(shí)現(xiàn)一個(gè) debounce
函數(shù)。
實(shí)現(xiàn)
我們這個(gè) debounce
函數(shù)接收兩個(gè)參數(shù),第一個(gè)是要“去彈跳”的回調(diào)函數(shù) fn
,第二個(gè)是延遲的時(shí)間 delay
。
實(shí)際上,大部分的完整
debounce
實(shí)現(xiàn)還有第三個(gè)參數(shù)immediate
,表明回調(diào)函數(shù)是在一個(gè)時(shí)間區(qū)間的最開始執(zhí)行(immediate
為true
)還是最后執(zhí)行(immediate
為false
),比如 underscore 的 _.debounce。本文不考慮這個(gè)參數(shù),只考慮最后執(zhí)行的情況,感興趣的可以自行研究。
/**
*
* @param fn {Function} 實(shí)際要執(zhí)行的函數(shù)
* @param delay {Number} 延遲時(shí)間,也就是閾值,單位是毫秒(ms)
*
* @return {Function} 返回一個(gè)“去彈跳”了的函數(shù)
*/
var debounce = function(fn,delay){
// 定時(shí)器事件
var time;
return function(){
// 保存函數(shù)調(diào)用時(shí)的上下文和參數(shù),傳遞給 fn
var context = this;
var args = arguments;
clearTimeout(time);
time = setTimeout(function(){
fn.apply(context,args);
},delay)
}
}
其實(shí)思路很簡(jiǎn)單,debounce
返回了一個(gè)閉包,這個(gè)閉包依然會(huì)被連續(xù)頻繁地調(diào)用,但是在閉包內(nèi)部,卻限制了原始函數(shù) fn
的執(zhí)行,強(qiáng)制 fn
只在連續(xù)操作停止后只執(zhí)行一次。
debounce
的使用方式如下:
window.onresize = debounce(function (e) {
console.log(111)
}, 250)
Throttle
throttle
的概念理解起來更容易,就是固定函數(shù)執(zhí)行的速率,即所謂的“節(jié)流”。正常情況下,mousemove
的監(jiān)聽函數(shù)可能會(huì)每 20ms(假設(shè))執(zhí)行一次,如果設(shè)置 200ms 的“節(jié)流”,那么它就會(huì)每 200ms 執(zhí)行一次。比如在 1s 的時(shí)間段內(nèi),正常的監(jiān)聽函數(shù)可能會(huì)執(zhí)行 50(1000/20) 次,“節(jié)流” 200ms 后則會(huì)執(zhí)行 5(1000/200) 次。
我們先來看 Demo。可以看到,不管鼠標(biāo)移動(dòng)的速度是慢是快,“節(jié)流”后的監(jiān)聽函數(shù)都會(huì)“勻速”地每 250ms 執(zhí)行一次。
實(shí)現(xiàn)
與 debounce
類似,我們這個(gè) throttle
也接收兩個(gè)參數(shù),一個(gè)實(shí)際要執(zhí)行的函數(shù) fn
,一個(gè)執(zhí)行間隔閾值 threshhold
。
同樣的,
throttle
的更完整實(shí)現(xiàn)可以參看 underscore 的 _.throttle。
// 這里實(shí)現(xiàn)一個(gè)簡(jiǎn)單版本的, 最后為了保證最后一次,設(shè)置了個(gè)定時(shí)器。
var throttle = function(fn,threshhold){
// 定時(shí)器事件
var last;
var timer;
threshhold = threshhold || 250;
return function(){
var context = this;
var args = arguments;
var now = +new Date();
// 定時(shí)器保證最后一次
if(last && now < last + threshhold){
clearTimeout(timer);
timer = setTimeout(function(){
last = now;
fn.apply(context,args);
},threshhold)
} else {
// 在時(shí)間區(qū)間的最開始和到達(dá)指定間隔的時(shí)候執(zhí)行一次 fn
last = now
fn.apply(context,args);
}
}
}
throttle
常用的場(chǎng)景是限制 resize
和 scroll
的觸發(fā)頻率。以 scroll
為例,查看這個(gè) Demo 感受下。
可視化解釋
如果還是不能完全體會(huì) debounce
和 throttle
的差異,可以到 這個(gè)頁面 看一下兩者可視化的比較。
總結(jié)
debounce
強(qiáng)制函數(shù)在某段時(shí)間內(nèi)只執(zhí)行一次,throttle
強(qiáng)制函數(shù)以固定的速率執(zhí)行。在處理一些高頻率觸發(fā)的 DOM 事件的時(shí)候,它們都能極大提高用戶體驗(yàn)。
22.4 自定義事件
事件是一種觀察者的設(shè)計(jì)模式,這是一種創(chuàng)建松散耦合代碼的技術(shù)。對(duì)象可以發(fā)布事件,用來表示該對(duì)象生命周期中有某個(gè)有趣的時(shí)刻到了。然后其他對(duì)象可以觀察該對(duì)象,等待這些有趣的時(shí)刻到來并通過運(yùn)行代碼相應(yīng)。
觀察者模式由兩類對(duì)象組成:主體和觀察者
主體負(fù)責(zé)發(fā)布事件,同時(shí)觀察者通過訂閱這些事件來觀察主體。該模式的一個(gè)重要概念就是主體并不知道觀察者的任何事情。
自定義事件背后的概念是創(chuàng)建一個(gè)管理事件的對(duì)象,讓其他對(duì)象監(jiān)聽那些事件。實(shí)現(xiàn)這種的基本模式如下:
function EventTarget(){
this.handlers = {};
}
EventTarget.prototype = {
constructor: EventTarget,
addHandler: function(type, handler){
if(typeof this.handlers[type] == 'undefined'){
this.handlers[type] = []
}
this.handlers[type].push(handler)
},
fire: function(event){
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); //綁定上的監(jiān)聽全部都要執(zhí)行一遍
}
}
},
removeHandler: function(type,handler){
if(this.handlers[type] instanceof Array){
var handlers = this.handlers[type];
for(var i=0, len=handlers.length; i<len; i++){
if(handlers[i] == handler){
break;
}
}
handlers.splice(i,1);
}
}
}
然后使用EventTarget類型的自定義事件可以如下使用:
// 創(chuàng)建主體對(duì)象
var target = new EventTarget();
//添加一個(gè)事件
target.addHandler("message",handleMeessage);
//觸發(fā)事件
target.fire({type:'message',message:'hello'});
// 刪除事件
target.removeHandler("message",handleMeessage);
//再次觸發(fā)事件
target.fire({type:'message',message:'hello'});
如果每個(gè)對(duì)象都有對(duì)其他所有對(duì)象的引用,那么整個(gè)代碼就會(huì)緊密耦合,同時(shí)維護(hù)也變得困難,因?yàn)閷?duì)某個(gè)對(duì)象的修改會(huì)影響到其他對(duì)象。是由自定義事件可以解耦相關(guān)對(duì)象。在很多情況下觸發(fā)事件的代碼和監(jiān)聽事件的代碼時(shí)完全分離的。
小結(jié):
- 可以使用惰性載入,將任何代碼分支推遲到第一次調(diào)用函數(shù)的時(shí)候。
- 函數(shù)綁定可以讓你創(chuàng)建始終在指定環(huán)境中運(yùn)行的函數(shù),同時(shí)函數(shù)柯里化可以讓你創(chuàng)建已經(jīng)填了某些參數(shù)的函數(shù)。
- 將綁定和柯里化組合起來,就能夠給你一種在任何環(huán)境中以任意參數(shù)執(zhí)行任意函數(shù)的方法。
javaScript中可以使用setTimeOut()和setInterval()如下創(chuàng)建定時(shí)器。
- 定時(shí)器代碼時(shí)放在一個(gè)等待區(qū)域,知道時(shí)間間隔到了之后,此時(shí)將代碼添加到Javascript的處理隊(duì)列中,等待下一次Javascript進(jìn)程空閑時(shí)被執(zhí)行。
- 每次一段代碼執(zhí)行結(jié)束之后,都會(huì)有一段空閑時(shí)間進(jìn)行瀏覽器處理。
- 這種行為意味著,可以使用定時(shí)器將長時(shí)間運(yùn)行的腳本氛圍一小塊一小塊可以在以后運(yùn)行的代碼段。這種做法有助于Web應(yīng)用對(duì)用戶交互有更積極的相應(yīng)。
javascript中經(jīng)常以事件的形式應(yīng)用觀察者模式。雖然事件常常和DOM一起用,但是你也可以通過實(shí)現(xiàn)自定義事件在自己的代碼中應(yīng)用。實(shí)現(xiàn)自定義事件有助于將不同部分的代碼相互之間解耦。