本節(jié)介紹 JavaScript 語言實(shí)際編程中国拇,涉及面向?qū)ο缶幊痰囊恍┠J健?/p>
1.構(gòu)造函數(shù)的繼承
讓一個(gè)構(gòu)造函數(shù)繼承另一個(gè)構(gòu)造函數(shù),是非常常見的需求绩卤。
這可以分成兩步實(shí)現(xiàn)。
第一步是在子類的構(gòu)造函數(shù)中纠修,調(diào)用父類的構(gòu)造函數(shù)密浑。
function Sub(value) {
Super.call(this);
this.prop = value;
}
上面代碼中胆剧,Sub
是子類的構(gòu)造函數(shù)辕近,this
是子類的實(shí)例浅乔。在實(shí)例上調(diào)用父類的構(gòu)造函數(shù)Super
莹妒,就會(huì)讓子類實(shí)例具有父類實(shí)例的屬性拘哨。
第二步,是讓子類的原型指向父類的原型唆缴,這樣子類就可以繼承父類原型砰粹。
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
上面代碼中,Sub.prototype
是子類的原型,要將它賦值為Object.create(Super.prototype)
趟卸,而不是直接等于Super.prototype
。否則后面兩行對(duì)Sub.prototype
的操作嘀掸,會(huì)連父類的原型Super.prototype
一起修改掉。
另外一種寫法是Sub.prototype
等于一個(gè)父類實(shí)例拣技。
Sub.prototype = new Super();
上面這種寫法也有繼承的效果千诬,但是子類會(huì)具有父類實(shí)例的方法。有時(shí)膏斤,這可能不是我們需要的徐绑,所以不推薦使用這種寫法。
舉例來說掸绞,下面是一個(gè)Shape
構(gòu)造函數(shù)泵三。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
我們需要讓Rectangle
構(gòu)造函數(shù)繼承Shape
。
// 第一步衔掸,子類繼承父類的實(shí)例
function Rectangle() {
Shape.call(this); // 調(diào)用父類構(gòu)造函數(shù)
}
// 另一種寫法
function Rectangle() {
this.base = Shape;
this.base();
}
// 第二步烫幕,子類繼承父類的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
采用這樣的寫法以后,instanceof
運(yùn)算符會(huì)對(duì)子類和父類的構(gòu)造函數(shù)敞映,都返回true
较曼。
var rect = new Rectangle();
rect.move(1, 1) // 'Shape moved.'
rect instanceof Rectangle // true
rect instanceof Shape // true
上面代碼中,子類是整體繼承父類振愿。有時(shí)只需要單個(gè)方法的繼承捷犹,這時(shí)可以采用下面的寫法。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
上面代碼中冕末,子類是整體繼承父類萍歉。有時(shí)只需要單個(gè)方法的繼承,這時(shí)可以采用下面的寫法档桃。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
上面代碼中枪孩,子類B
的print
方法先調(diào)用父類A
的print
方法,再部署自己的代碼藻肄。這就等于繼承了父類A
的print
方法蔑舞。
2.多重繼承
JavaScript 不提供多重繼承功能,即不允許一個(gè)對(duì)象同時(shí)繼承多個(gè)對(duì)象嘹屯。但是攻询,可以通過變通方法,實(shí)現(xiàn)這個(gè)功能州弟。
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 繼承 M1
S.prototype = Object.create(M1.prototype);
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定構(gòu)造函數(shù)
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello:'
s.world // 'world'
上面代碼中钧栖,子類S
同時(shí)繼承了父類M1
和M2
。這種模式又稱為 Mixin(混入)呆馁。
3.模塊
隨著網(wǎng)站逐漸變成”互聯(lián)網(wǎng)應(yīng)用程序”桐经,嵌入網(wǎng)頁的JavaScript代碼越來越龐大,越來越復(fù)雜浙滤。網(wǎng)頁越來越像桌面程序阴挣,需要一個(gè)團(tuán)隊(duì)分工協(xié)作、進(jìn)度管理纺腊、單元測試等等……開發(fā)者不得不使用軟件工程的方法畔咧,管理網(wǎng)頁的業(yè)務(wù)邏輯。
JavaScript模塊化編程揖膜,已經(jīng)成為一個(gè)迫切的需求誓沸。理想情況下,開發(fā)者只需要實(shí)現(xiàn)核心的業(yè)務(wù)邏輯壹粟,其他都可以加載別人已經(jīng)寫好的模塊拜隧。
但是宿百,JavaScript不是一種模塊化編程語言,ES5不支持”類”(class)洪添,更遑論”模塊”(module)了垦页。ES6正式支持”類”和”模塊”,但還沒有成為主流干奢。JavaScript社區(qū)做了很多努力痊焊,在現(xiàn)有的運(yùn)行環(huán)境中,實(shí)現(xiàn)模塊的效果忿峻。
3.1 基本的實(shí)現(xiàn)方法
模塊是實(shí)現(xiàn)特定功能的一組屬性和方法的封裝薄啥。
只要把不同的函數(shù)(以及記錄狀態(tài)的變量)簡單地放在一起,就算是一個(gè)模塊逛尚。
function m1() {
//...
}
function m2() {
//...
}
上面的函數(shù)m1()和m2()垄惧,組成一個(gè)模塊。使用的時(shí)候绰寞,直接調(diào)用就行了赘艳。
這種做法的缺點(diǎn)很明顯:”污染”了全局變量,無法保證不與其他模塊發(fā)生變量名沖突克握,而且模塊成員之間看不出直接關(guān)系蕾管。
為了解決上面的缺點(diǎn),可以把模塊寫成一個(gè)對(duì)象菩暗,所有的模塊成員都放到這個(gè)對(duì)象里面掰曾。
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
上面的函數(shù)m1
和m2
,都封裝在module1
對(duì)象里停团。使用的時(shí)候旷坦,就是調(diào)用這個(gè)對(duì)象的屬性。
module1.m1();
但是佑稠,這樣的寫法會(huì)暴露所有模塊成員秒梅,內(nèi)部狀態(tài)可以被外部改寫。比如舌胶,外部代碼可以直接改變內(nèi)部計(jì)數(shù)器的值捆蜀。
module1._count = 5;
3.2 封裝私有變量:構(gòu)造函數(shù)的寫法
我們可以利用構(gòu)造函數(shù),封裝私有變量幔嫂。
function StringBuilder() {
var buffer = [];
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
這種方法將私有變量封裝在構(gòu)造函數(shù)中辆它,違反了構(gòu)造函數(shù)與實(shí)例對(duì)象相分離的原則。并且履恩,非常耗費(fèi)內(nèi)存锰茉。
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};
這種方法將私有變量放入實(shí)例對(duì)象中,好處是看上去更自然切心,但是它的私有變量可以從外部讀寫飒筑,不是很安全片吊。
3.3 封裝私有變量:立即執(zhí)行函數(shù)的寫法
使用“立即執(zhí)行函數(shù)”(Immediately-Invoked Function Expression,IIFE)协屡,將相關(guān)的屬性和方法封裝在一個(gè)函數(shù)作用域里面定鸟,可以達(dá)到不暴露私有成員的目的。
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
使用上面的寫法著瓶,外部代碼無法讀取內(nèi)部的_count
變量。
console.info(module1._count); //undefined
上面的module1
就是JavaScript模塊的基本寫法啼县。下面材原,再對(duì)這種寫法進(jìn)行加工。
3.4 模塊的放大模式
如果一個(gè)模塊很大季眷,必須分成幾個(gè)部分余蟹,或者一個(gè)模塊需要繼承另一個(gè)模塊,這時(shí)就有必要采用“放大模式”(augmentation)子刮。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
上面的代碼為module1
模塊添加了一個(gè)新方法m3()
威酒,然后返回新的module1
模塊。
在瀏覽器環(huán)境中挺峡,模塊的各個(gè)部分通常都是從網(wǎng)上獲取的葵孤,有時(shí)無法知道哪個(gè)部分會(huì)先加載。如果采用上面的寫法橱赠,第一個(gè)執(zhí)行的部分有可能加載一個(gè)不存在空對(duì)象尤仍,這時(shí)就要采用”寬放大模式”(Loose augmentation)。
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
與”放大模式”相比狭姨,“寬放大模式”就是“立即執(zhí)行函數(shù)”的參數(shù)可以是空對(duì)象宰啦。
3.5 輸入全局變量
獨(dú)立性是模塊的重要特點(diǎn),模塊內(nèi)部最好不與程序的其他部分直接交互饼拍。
為了在模塊內(nèi)部調(diào)用全局變量赡模,必須顯式地將其他變量輸入模塊。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的module1
模塊需要使用jQuery庫和YUI庫师抄,就把這兩個(gè)庫(其實(shí)是兩個(gè)模塊)當(dāng)作參數(shù)輸入module1
漓柑。這樣做除了保證模塊的獨(dú)立性,還使得模塊之間的依賴關(guān)系變得明顯叨吮。
立即執(zhí)行函數(shù)還可以起到命名空間的作用欺缘。
(function($, window, document) {
function go(num) {
}
function handleEvents() {
}
function initialize() {
}
function dieCarouselDie() {
}
//attach to the global scope
window.finalCarousel = {
init : initialize,
destroy : dieCouraselDie
}
})( jQuery, window, document );
上面代碼中,finalCarousel
對(duì)象輸出到全局挤安,對(duì)外暴露init
和destroy
接口谚殊,內(nèi)部方法go
、handleEvents
蛤铜、initialize
嫩絮、dieCarouselDie
都是外部無法調(diào)用的丛肢。
異步操作概述
1.單線程模型
單線程模型指的是,JavaScript 只在一個(gè)線程上運(yùn)行剿干。也就是說蜂怎,JavaScript 同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待置尔。
注意杠步,JavaScript 只在一個(gè)線程上運(yùn)行,不代表 JavaScript 引擎只有一個(gè)線程榜轿。事實(shí)上幽歼,JavaScript 引擎有多個(gè)線程,單個(gè)腳本只能在一個(gè)線程上運(yùn)行(稱為主線程)谬盐,其他線程都是在后臺(tái)配合甸私。
JavaScript 之所以采用單線程,而不是多線程飞傀,跟歷史有關(guān)系皇型。JavaScript 從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜砸烦,因?yàn)槎嗑€程需要共享資源弃鸦、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁腳本語言來說幢痘,這就太復(fù)雜了寡键。如果 JavaScript 同時(shí)有兩個(gè)線程,一個(gè)線程在網(wǎng)頁 DOM 節(jié)點(diǎn)上添加內(nèi)容雪隧,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn)西轩,這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?是不是還要有鎖機(jī)制脑沿?所以藕畔,為了避免復(fù)雜性,JavaScript 一開始就是單線程庄拇,這已經(jīng)成了這門語言的核心特征注服,將來也不會(huì)改變。
這種模式的好處是實(shí)現(xiàn)起來比較簡單措近,執(zhí)行環(huán)境相對(duì)單純溶弟;壞處是只要有一個(gè)任務(wù)耗時(shí)很長,后面的任務(wù)都必須排隊(duì)等著瞭郑,會(huì)拖延整個(gè)程序的執(zhí)行辜御。常見的瀏覽器無響應(yīng)(假死),往往就是因?yàn)槟骋欢?JavaScript 代碼長時(shí)間運(yùn)行(比如死循環(huán))屈张,導(dǎo)致整個(gè)頁面卡在這個(gè)地方擒权,其他任務(wù)無法執(zhí)行袱巨。JavaScript 語言本身并不慢,慢的是讀寫外部數(shù)據(jù)碳抄,比如等待 Ajax 請(qǐng)求返回結(jié)果愉老。這個(gè)時(shí)候,如果對(duì)方服務(wù)器遲遲沒有響應(yīng)剖效,或者網(wǎng)絡(luò)不通暢嫉入,就會(huì)導(dǎo)致腳本的長時(shí)間停滯。
如果排隊(duì)是因?yàn)橛?jì)算量大璧尸,CPU 忙不過來咒林,倒也算了,但是很多時(shí)候 CPU 是閑著的逗宁,因?yàn)?IO 操作(輸入輸出)很慢(比如 Ajax 操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來梦湘,再往下執(zhí)行瞎颗。JavaScript 語言的設(shè)計(jì)者意識(shí)到,這時(shí) CPU 完全可以不管 IO 操作捌议,掛起處于等待中的任務(wù)哼拔,先運(yùn)行排在后面的任務(wù)。等到 IO 操作返回了結(jié)果瓣颅,再回過頭倦逐,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是 JavaScript 內(nèi)部采用的“事件循環(huán)”機(jī)制(Event Loop)宫补。
單線程模型雖然對(duì) JavaScript 構(gòu)成了很大的限制檬姥,但也因此使它具備了其他語言不具備的優(yōu)勢。如果用得好粉怕,JavaScript 程序是不會(huì)出現(xiàn)堵塞的健民,這就是為什么 Node 可以用很少的資源,應(yīng)付大流量訪問的原因贫贝。
為了利用多核 CPU 的計(jì)算能力秉犹,HTML5 提出 Web Worker 標(biāo)準(zhǔn),允許 JavaScript 腳本創(chuàng)建多個(gè)線程稚晚,但是子線程完全受主線程控制崇堵,且不得操作 DOM。所以客燕,這個(gè)新標(biāo)準(zhǔn)并沒有改變 JavaScript 單線程的本質(zhì)鸳劳。
2. 同步任務(wù)和異步任務(wù)
程序里面所有的任務(wù),可以分成兩類:同步任務(wù)(synchronous)和異步任務(wù)(asynchronous)也搓。
同步任務(wù)是那些沒有被引擎掛起棍辕、在主線程上排隊(duì)執(zhí)行的任務(wù)暮现。只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)楚昭。
異步任務(wù)是那些被引擎放在一邊栖袋,不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列的任務(wù)抚太。只有引擎認(rèn)為某個(gè)異步任務(wù)可以執(zhí)行了(比如 Ajax 操作從服務(wù)器得到了結(jié)果)塘幅,該任務(wù)(采用回調(diào)函數(shù)的形式)才會(huì)進(jìn)入主線程執(zhí)行。排在異步任務(wù)后面的代碼尿贫,不用等待異步任務(wù)結(jié)束會(huì)馬上運(yùn)行电媳,也就是說,異步任務(wù)不具有“堵塞”效應(yīng)庆亡。
舉例來說匾乓,Ajax 操作可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理又谋,由開發(fā)者決定拼缝。如果是同步任務(wù),主線程就等著 Ajax 操作返回結(jié)果彰亥,再往下執(zhí)行咧七;如果是異步任務(wù),主線程在發(fā)出 Ajax 請(qǐng)求以后任斋,就直接往下執(zhí)行继阻,等到 Ajax 操作有了結(jié)果,主線程再執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)废酷。
3.任務(wù)隊(duì)列和事件循環(huán)
JavaScript 運(yùn)行時(shí)瘟檩,除了一個(gè)正在運(yùn)行的主線程,引擎還提供一個(gè)任務(wù)隊(duì)列(task queue)澈蟆,里面是各種需要當(dāng)前程序處理的異步任務(wù)芒帕。(實(shí)際上,根據(jù)異步任務(wù)的類型丰介,存在多個(gè)任務(wù)隊(duì)列背蟆。為了方便理解,這里假設(shè)只存在一個(gè)隊(duì)列哮幢。)
首先带膀,主線程會(huì)去執(zhí)行所有的同步任務(wù)。等到同步任務(wù)全部執(zhí)行完橙垢,就會(huì)去看任務(wù)隊(duì)列里面的異步任務(wù)垛叨。如果滿足條件,那么異步任務(wù)就重新進(jìn)入主線程開始執(zhí)行,這時(shí)它就變成同步任務(wù)了嗽元。等到執(zhí)行完敛纲,下一個(gè)異步任務(wù)再進(jìn)入主線程開始執(zhí)行。一旦任務(wù)隊(duì)列清空剂癌,程序就結(jié)束執(zhí)行淤翔。
異步任務(wù)的寫法通常是回調(diào)函數(shù)。一旦異步任務(wù)重新進(jìn)入主線程佩谷,就會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)旁壮。如果一個(gè)異步任務(wù)沒有回調(diào)函數(shù),就不會(huì)進(jìn)入任務(wù)隊(duì)列谐檀,也就是說抡谐,不會(huì)重新進(jìn)入主線程,因?yàn)闆]有用回調(diào)函數(shù)指定下一步的操作桐猬。
JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果麦撵,能不能進(jìn)入主線程呢?答案就是引擎在不停地檢查溃肪,一遍又一遍免胃,只要同步任務(wù)執(zhí)行完了,引擎就會(huì)去檢查那些掛起來的異步任務(wù)乍惊,是不是可以進(jìn)入主線程了杜秸。這種循環(huán)檢查的機(jī)制放仗,就叫做事件循環(huán)(Event Loop)润绎。維基百科的定義是:“事件循環(huán)是一個(gè)程序結(jié)構(gòu),用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”诞挨。
4.異步操作的模式
下面總結(jié)一下異步操作的幾種模式莉撇。
4.1 回調(diào)函數(shù)
回調(diào)函數(shù)是異步操作最基本的方法。
下面是兩個(gè)函數(shù)f1
和f2
惶傻,編程的意圖是f2
必須等到f1
執(zhí)行完成棍郎,才能執(zhí)行。
function f1() {
// ...
}
function f2() {
// ...
}
f1();
f2();
上面代碼的問題在于银室,如果f1
是異步操作涂佃,f2
會(huì)立即執(zhí)行,不會(huì)等到f1
結(jié)束再執(zhí)行蜈敢。
這時(shí)辜荠,可以考慮改寫f1
,把f2
寫成f1
的回調(diào)函數(shù)抓狭。
function f1(callback) {
// ...
callback();
}
function f2() {
// ...
}
f1(f2);
回調(diào)函數(shù)的優(yōu)點(diǎn)是簡單伯病、容易理解和實(shí)現(xiàn),缺點(diǎn)是不利于代碼的閱讀和維護(hù)否过,各個(gè)部分之間高度耦合(coupling)午笛,使得程序結(jié)構(gòu)混亂惭蟋、流程難以追蹤(尤其是多個(gè)回調(diào)函數(shù)嵌套的情況),而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)药磺。
4.2 事件監(jiān)聽
另一種思路是采用事件驅(qū)動(dòng)模式告组。異步任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生与涡。
還是以f1
和f2
為例惹谐。首先,為f1
綁定一個(gè)事件(這里采用的 jQuery 的寫法)驼卖。
f1.on('done', f2);
上面這行代碼的意思是氨肌,當(dāng)f1
發(fā)生done
事件,就執(zhí)行f2
酌畜。然后怎囚,對(duì)f1
進(jìn)行改寫:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代碼中,f1.trigger('done')
表示桥胞,執(zhí)行完成后恳守,立即觸發(fā)done
事件,從而開始執(zhí)行f2
贩虾。
這種方法的優(yōu)點(diǎn)是比較容易理解催烘,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù)缎罢,而且可以“去耦合”(decoupling)伊群,有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型策精,運(yùn)行流程會(huì)變得很不清晰舰始。閱讀代碼的時(shí)候,很難看出主流程咽袜。
4.3 發(fā)布/訂閱
事件完全可以理解成“信號(hào)”丸卷,如果存在一個(gè)“信號(hào)中心”,某個(gè)任務(wù)執(zhí)行完成询刹,就向信號(hào)中心“發(fā)布”(publish)一個(gè)信號(hào)谜嫉,其他任務(wù)可以向信號(hào)中心“訂閱”(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開始執(zhí)行凹联。這就叫做“發(fā)布/訂閱模式”(publish-subscribe pattern)沐兰,又稱“觀察者模式”(observer pattern)。
這個(gè)模式有多種實(shí)現(xiàn)匕垫,下面采用的是 Ben Alman 的 Tiny Pub/Sub僧鲁,這是 jQuery 的一個(gè)插件。
首先,f2
向信號(hào)中心jQuery
訂閱done
信號(hào)寞秃。
jQuery.subscribe('done', f2);
然后斟叼,f1
進(jìn)行如下改寫。
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
上面代碼中春寿,jQuery.publish('done')
的意思是朗涩,f1
執(zhí)行完成后,向信號(hào)中心jQuery
發(fā)布done
信號(hào)绑改,從而引發(fā)f2
的執(zhí)行谢床。
f2
完成執(zhí)行后,可以取消訂閱(unsubscribe)厘线。
jQuery.unsubscribe('done', f2);
這種方法的性質(zhì)與“事件監(jiān)聽”類似识腿,但是明顯優(yōu)于后者。因?yàn)榭梢酝ㄟ^查看“消息中心”造壮,了解存在多少信號(hào)渡讼、每個(gè)信號(hào)有多少訂閱者,從而監(jiān)控程序的運(yùn)行耳璧。
異步操作的流程控制
如果有多個(gè)異步操作成箫,就存在一個(gè)流程控制的問題:如何確定異步操作執(zhí)行的順序,以及如何保證遵守這種順序旨枯。
function async(arg, callback) {
console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
上面代碼的async
函數(shù)是一個(gè)異步任務(wù)蹬昌,非常耗時(shí),每次執(zhí)行需要1秒才能完成攀隔,然后再調(diào)用回調(diào)函數(shù)皂贩。
如果有六個(gè)這樣的異步任務(wù),需要全部完成后竞慢,才能執(zhí)行最后的final
函數(shù)先紫。請(qǐng)問應(yīng)該如何安排操作流程治泥?
function final(value) {
console.log('完成: ', value);
}
async(1, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, final);
});
});
});
});
});
上面代碼中筹煮,六個(gè)回調(diào)函數(shù)的嵌套,不僅寫起來麻煩居夹,容易出錯(cuò)败潦,而且難以維護(hù)。
5.1 串行執(zhí)行
我們可以編寫一個(gè)流程控制函數(shù)准脂,讓它來控制異步任務(wù)劫扒,一個(gè)任務(wù)完成以后,再執(zhí)行另一個(gè)狸膏。這就叫串行執(zhí)行沟饥。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function series(item) {
if(item) {
async(item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results[results.length - 1]);
}
}
series(items.shift());
上面代碼中,函數(shù)series
就是串行函數(shù),它會(huì)依次執(zhí)行異步任務(wù)贤旷,所有任務(wù)都完成后广料,才會(huì)執(zhí)行final
函數(shù)。items
數(shù)組保存每一個(gè)異步任務(wù)的參數(shù)幼驶,results
數(shù)組保存每一個(gè)異步任務(wù)的運(yùn)行結(jié)果艾杏。
注意,上面的寫法需要六秒盅藻,才能完成整個(gè)腳本购桑。
5.2 并行執(zhí)行
流程控制函數(shù)也可以是并行執(zhí)行,即所有異步任務(wù)同時(shí)執(zhí)行氏淑,等到全部完成以后勃蜘,才執(zhí)行final
函數(shù)。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length === items.length) {
final(results[results.length - 1]);
}
})
});
上面代碼中假残,forEach
方法會(huì)同時(shí)發(fā)起六個(gè)異步任務(wù)元旬,等到它們?nèi)客瓿梢院螅艜?huì)執(zhí)行final
函數(shù)守问。
相比而言匀归,上面的寫法只要一秒,就能完成整個(gè)腳本穆端。這就是說嗽仪,并行執(zhí)行的效率較高,比起串行執(zhí)行一次只能執(zhí)行一個(gè)任務(wù),較為節(jié)約時(shí)間。但是問題在于如果并行的任務(wù)較多痢艺,很容易耗盡系統(tǒng)資源斤蔓,拖慢運(yùn)行速度。因此有了第三種流程控制方式卸留。
5.3 并行與串行的結(jié)合
所謂并行與串行的結(jié)合喳整,就是設(shè)置一個(gè)門檻呵晨,每次最多只能并行執(zhí)行n
個(gè)異步任務(wù)季二,這樣就避免了過分占用系統(tǒng)資源税手。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function async(arg, callback) {
console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final(results);
}
});
running++;
}
}
launcher();
上面代碼中兵扬,最多只能同時(shí)運(yùn)行兩個(gè)異步任務(wù)妙蔗。變量running
記錄當(dāng)前正在運(yùn)行的任務(wù)數(shù)寸五,只要低于門檻值,就再啟動(dòng)一個(gè)新的任務(wù),如果等于0
减响,就表示所有任務(wù)都執(zhí)行完了颂鸿,這時(shí)就執(zhí)行final
函數(shù)尖坤。
這段代碼需要三秒完成整個(gè)腳本,處在串行執(zhí)行和并行執(zhí)行之間闲擦。通過調(diào)節(jié)limit
變量慢味,達(dá)到效率和資源的最佳平衡场梆。
定時(shí)器
JavaScript 提供定時(shí)執(zhí)行代碼的功能,叫做定時(shí)器(timer)纯路,主要由setTimeout()
和setInterval()
這兩個(gè)函數(shù)來完成或油。它們向任務(wù)隊(duì)列添加定時(shí)任務(wù)。
1.setTimeout()
setTimeout
函數(shù)用來指定某個(gè)函數(shù)或某段代碼驰唬,在多少毫秒之后執(zhí)行顶岸。它返回一個(gè)整數(shù),表示定時(shí)器的編號(hào)叫编,以后可以用來取消這個(gè)定時(shí)器蜕琴。
var timerId = setTimeout(func|code, delay);
上面代碼中,setTimeout
函數(shù)接受兩個(gè)參數(shù)宵溅,第一個(gè)參數(shù)func|code
是將要推遲執(zhí)行的函數(shù)名或者一段代碼凌简,第二個(gè)參數(shù)delay
是推遲執(zhí)行的毫秒數(shù)。
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
// 1
// 3
// 2
上面代碼會(huì)先輸出1和3恃逻,然后等待1000毫秒再輸出2雏搂。注意,console.log(2)
必須以字符串的形式寇损,作為setTimeout
的參數(shù)凸郑。
如果推遲執(zhí)行的是函數(shù),就直接將函數(shù)名矛市,作為setTimeout
的參數(shù)芙沥。
function f() {
console.log(2);
}
setTimeout(f, 1000);
setTimeout
的第二個(gè)參數(shù)如果省略,則默認(rèn)為0浊吏。
setTimeout(f)
// 等同于
setTimeout(f, 0)
除了前兩個(gè)參數(shù)而昨,setTimeout
還允許更多的參數(shù)。它們將依次傳入推遲執(zhí)行的函數(shù)(回調(diào)函數(shù))找田。
setTimeout(function (a,b) {
console.log(a + b);
}, 1000, 1, 1);
上面代碼中歌憨,setTimeout
共有4個(gè)參數(shù)。最后那兩個(gè)參數(shù)墩衙,將在1000毫秒之后回調(diào)函數(shù)執(zhí)行時(shí)圈匆,作為回調(diào)函數(shù)的參數(shù)
還有一個(gè)需要注意的地方梆靖,如果回調(diào)函數(shù)是對(duì)象的方法述召,那么setTimeout
使得方法內(nèi)部的this
關(guān)鍵字指向全局環(huán)境满着,而不是定義時(shí)所在的那個(gè)對(duì)象。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y, 1000) // 1
上面代碼輸出的是1挫剑,而不是2去扣。因?yàn)楫?dāng)obj.y
在1000毫秒后運(yùn)行時(shí),this
所指向的已經(jīng)不是obj
了暮顺,而是全局環(huán)境厅篓。
為了防止出現(xiàn)這個(gè)問題秀存,一種解決方法是將obj.y
放入一個(gè)函數(shù)捶码。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(function () {
obj.y();
}, 1000);
// 2
上面代碼中羽氮,obj.y
放在一個(gè)匿名函數(shù)之中,這使得obj.y
在obj
的作用域執(zhí)行惫恼,而不是在全局作用域內(nèi)執(zhí)行档押,所以能夠顯示正確的值。
另一種解決方法是祈纯,使用bind
方法令宿,將obj.y
這個(gè)方法綁定在obj
上面。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y.bind(obj), 1000)
// 2
2.setInterval()
setInterval
函數(shù)的用法與setTimeout
完全一致腕窥,區(qū)別僅僅在于setInterval
指定某個(gè)任務(wù)每隔一段時(shí)間就執(zhí)行一次粒没,也就是無限次的定時(shí)執(zhí)行。
var i = 1
var timer = setInterval(function() {
console.log(2);
}, 1000)
上面代碼中簇爆,每隔1000毫秒就輸出一個(gè)2癞松,會(huì)無限運(yùn)行下去,直到關(guān)閉當(dāng)前窗口入蛆。
與setTimeout
一樣响蓉,除了前兩個(gè)參數(shù),setInterval
方法還可以接受更多的參數(shù)哨毁,它們會(huì)傳入回調(diào)函數(shù)枫甲。
下面是一個(gè)通過setInterval
方法實(shí)現(xiàn)網(wǎng)頁動(dòng)畫的例子。
var div = document.getElementById('someDiv');
var opacity = 1;
var fader = setInterval(function() {
opacity -= 0.1;
if (opacity >= 0) {
div.style.opacity = opacity;
} else {
clearInterval(fader);
}
}, 100);
上面代碼每隔100毫秒扼褪,設(shè)置一次div
元素的透明度想幻,直至其完全透明為止。
setInterval
的一個(gè)常見用途是實(shí)現(xiàn)輪詢话浇。下面是一個(gè)輪詢 URL 的 Hash 值是否發(fā)生變化的例子举畸。
var hash = window.location.hash;
var hashWatcher = setInterval(function() {
if (window.location.hash != hash) {
updatePage();
}
}, 1000);
setInterval
指定的是“開始執(zhí)行”之間的間隔,并不考慮每次任務(wù)執(zhí)行本身所消耗的時(shí)間凳枝。因此實(shí)際上抄沮,兩次執(zhí)行之間的間隔會(huì)小于指定的時(shí)間。比如岖瑰,setInterval
指定每 100ms 執(zhí)行一次叛买,每次執(zhí)行需要 5ms,那么第一次執(zhí)行結(jié)束后95毫秒蹋订,第二次執(zhí)行就會(huì)開始率挣。如果某次執(zhí)行耗時(shí)特別長,比如需要105毫秒露戒,那么它結(jié)束后椒功,下一次執(zhí)行就會(huì)立即開始捶箱。
為了確保兩次執(zhí)行之間有固定的間隔,可以不用setInterval
动漾,而是每次執(zhí)行結(jié)束后丁屎,使用setTimeout
指定下一次執(zhí)行的具體時(shí)間。
var i = 1;
var timer = setTimeout(function f() {
// ...
timer = setTimeout(f, 2000);
}, 2000);
3.clearTimeout()旱眯,clearInterval()
setTimeout
和setInterval
函數(shù)晨川,都返回一個(gè)整數(shù)值,表示計(jì)數(shù)器編號(hào)删豺。將該整數(shù)傳入clearTimeout
和clearInterval
函數(shù)共虑,就可以取消對(duì)應(yīng)的定時(shí)器。
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
上面代碼中呀页,回調(diào)函數(shù)f
不會(huì)再執(zhí)行了妈拌,因?yàn)閮蓚€(gè)定時(shí)器都被取消了。
setTimeout
和setInterval
返回的整數(shù)值是連續(xù)的蓬蝶,也就是說尘分,第二個(gè)setTimeout
方法返回的整數(shù)值,將比第一個(gè)的整數(shù)值大1疾党。
function f() {}
setTimeout(f, 1000) // 10
setTimeout(f, 1000) // 11
setTimeout(f, 1000) // 12
上面代碼中音诫,連續(xù)調(diào)用三次setTimeout
,返回值都比上一次大了1雪位。
利用這一點(diǎn)竭钝,可以寫一個(gè)函數(shù),取消當(dāng)前所有的setTimeout
定時(shí)器雹洗。
(function() {
var gid = setInterval(clearAllTimeouts, 0);
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
上面代碼中香罐,先調(diào)用setTimeout
,得到一個(gè)計(jì)算器編號(hào)时肿,然后把編號(hào)比它小的計(jì)數(shù)器全部取消庇茫。
4.實(shí)例:debounce 函數(shù)
有時(shí),我們不希望回調(diào)函數(shù)被頻繁調(diào)用螃成。比如旦签,用戶填入網(wǎng)頁輸入框的內(nèi)容,希望通過 Ajax 方法傳回服務(wù)器寸宏,jQuery 的寫法如下宁炫。
$('textarea').on('keydown', ajaxAction);
這樣寫有一個(gè)很大的缺點(diǎn),就是如果用戶連續(xù)擊鍵氮凝,就會(huì)連續(xù)觸發(fā)keydown
事件羔巢,造成大量的 Ajax 通信。這是不必要的,而且很可能產(chǎn)生性能問題竿秆。正確的做法應(yīng)該是启摄,設(shè)置一個(gè)門檻值,表示兩次 Ajax 通信的最小間隔時(shí)間幽钢。如果在間隔時(shí)間內(nèi)歉备,發(fā)生新的keydown
事件,則不觸發(fā) Ajax 通信搅吁,并且重新開始計(jì)時(shí)威创。如果過了指定時(shí)間落午,沒有發(fā)生新的keydown
事件谎懦,再將數(shù)據(jù)發(fā)送出去。
這種做法叫做 debounce(防抖動(dòng))溃斋。假定兩次 Ajax 通信的間隔不得小于2500毫秒界拦,上面的代碼可以改寫成下面這樣。
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 聲明計(jì)時(shí)器
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
上面代碼中梗劫,只要在2500毫秒之內(nèi)享甸,用戶再次擊鍵,就會(huì)取消上一次的定時(shí)器梳侨,然后再新建一個(gè)定時(shí)器蛉威。這樣就保證了回調(diào)函數(shù)之間的調(diào)用間隔,至少是2500毫秒走哺。
5.運(yùn)行機(jī)制
setTimeout
和setInterval
的運(yùn)行機(jī)制蚯嫌,是將指定的代碼移出本輪事件循環(huán),等到下一輪事件循環(huán)丙躏,再檢查是否到了指定時(shí)間择示。如果到了,就執(zhí)行對(duì)應(yīng)的代碼晒旅;如果不到栅盲,就繼續(xù)等待。
這意味著废恋,setTimeout
和setInterval
指定的回調(diào)函數(shù)谈秫,必須等到本輪事件循環(huán)的所有同步任務(wù)都執(zhí)行完,才會(huì)開始執(zhí)行鱼鼓。由于前面的任務(wù)到底需要多少時(shí)間執(zhí)行完拟烫,是不確定的,所以沒有辦法保證蚓哩,setTimeout
和setInterval
指定的任務(wù)构灸,一定會(huì)按照預(yù)定時(shí)間執(zhí)行。
setTimeout(someTask, 100);
veryLongTask();
上面代碼的setTimeout
,指定100毫秒以后運(yùn)行一個(gè)任務(wù)喜颁。但是稠氮,如果后面的veryLongTask
函數(shù)(同步任務(wù))運(yùn)行時(shí)間非常長,過了100毫秒還無法結(jié)束半开,那么被推遲運(yùn)行的someTask
就只有等著隔披,等到veryLongTask
運(yùn)行結(jié)束,才輪到它執(zhí)行寂拆。
再看一個(gè)setInterval
的例子奢米。
setInterval(function () {
console.log(2);
}, 1000);
sleep(3000);
上面代碼中,setInterval
要求每隔1000毫秒纠永,就輸出一個(gè)2鬓长。但是,緊接著的sleep
語句需要3000毫秒才能完成尝江,那么setInterval
就必須推遲到3000毫秒之后才開始生效涉波。注意,生效后setInterval
不會(huì)產(chǎn)生累積效應(yīng)炭序,即不會(huì)一下子輸出三個(gè)2啤覆,而是只會(huì)輸出一個(gè)2。