面向?qū)ο缶幊痰哪J?/h1>

本節(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
}

上面代碼中枪孩,子類Bprint方法先調(diào)用父類Aprint方法,再部署自己的代碼藻肄。這就等于繼承了父類Aprint方法蔑舞。

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í)繼承了父類M1M2。這種模式又稱為 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ù)m1m2,都封裝在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ì)外暴露initdestroy接口谚殊,內(nèi)部方法gohandleEvents蛤铜、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ù)f1f2惶傻,編程的意圖是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ā)生与涡。

還是以f1f2為例惹谐。首先,為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.yobj的作用域執(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()

setTimeoutsetInterval函數(shù)晨川,都返回一個(gè)整數(shù)值,表示計(jì)數(shù)器編號(hào)删豺。將該整數(shù)傳入clearTimeoutclearInterval函數(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í)器都被取消了。

setTimeoutsetInterval返回的整數(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ī)制

setTimeoutsetInterval的運(yùn)行機(jī)制蚯嫌,是將指定的代碼移出本輪事件循環(huán),等到下一輪事件循環(huán)丙躏,再檢查是否到了指定時(shí)間择示。如果到了,就執(zhí)行對(duì)應(yīng)的代碼晒旅;如果不到栅盲,就繼續(xù)等待。

這意味著废恋,setTimeoutsetInterval指定的回調(diào)函數(shù)谈秫,必須等到本輪事件循環(huán)的所有同步任務(wù)都執(zhí)行完,才會(huì)開始執(zhí)行鱼鼓。由于前面的任務(wù)到底需要多少時(shí)間執(zhí)行完拟烫,是不確定的,所以沒有辦法保證蚓哩,setTimeoutsetInterval指定的任務(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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者

  • 序言:七十年代末惭聂,一起剝皮案震驚了整個(gè)濱河市窗声,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辜纲,老刑警劉巖笨觅,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侨歉,居然都是意外死亡屋摇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門幽邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炮温,“玉大人,你說我怎么就攤上這事牵舵∑馄。” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵畸颅,是天一觀的道長担巩。 經(jīng)常有香客問我,道長没炒,這世上最難降的妖魔是什么涛癌? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上拳话,老公的妹妹穿的比我還像新娘先匪。我一直安慰自己,他們只是感情好弃衍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布呀非。 她就那樣靜靜地躺著,像睡著了一般镜盯。 火紅的嫁衣襯著肌膚如雪岸裙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天速缆,我揣著相機(jī)與錄音降允,去河邊找鬼。 笑死激涤,一個(gè)胖子當(dāng)著我的面吹牛拟糕,可吹牛的內(nèi)容都是我干的判呕。 我是一名探鬼主播倦踢,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侠草!你這毒婦竟也來了辱挥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤边涕,失蹤者是張志新(化名)和其女友劉穎晤碘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體功蜓,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡园爷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了式撼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片童社。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖著隆,靈堂內(nèi)的尸體忽然破棺而出扰楼,到底是詐尸還是另有隱情,我是刑警寧澤美浦,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布弦赖,位于F島的核電站,受9級(jí)特大地震影響浦辨,放射性物質(zhì)發(fā)生泄漏蹬竖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望币厕。 院中可真熱鬧庆冕,春花似錦、人聲如沸劈榨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽同辣。三九已至拷姿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旱函,已是汗流浹背响巢。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棒妨,地道東北人踪古。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像券腔,于是被迫代替她去往敵國和親伏穆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355