10分鐘理解JS異步編程(從概念到原理)

1、異步編程簡介

  衆所周知JavaScript語言執行環境是「單線程」(單線程,就是指一次只能完成一件任務,若是有多個任務就必須排隊等候,前面一個任務完成,再執行後面一個任務)。這種「單線程」模式執行效率較低,任務耗時長。
  爲了解決這個問題,提出了「異步模式」(異步模式,是指後一個任務不等前一個任務執行完就執行,每一個任務有一個或多個回調函數)。
  異步模式使得JavaScript在處理事務時很是高效,但也帶來不少問題,如異常處理困難、嵌套過深。es6

2、異步編程的發展

JavaScript異步編程從出現不斷髮展和精進,主要經歷瞭如下4個階段:ajax

階段1 傳統callback回調函數

  回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。它的英語名字 callback,直譯過來就是"從新調用"。雖然回調函數多用於異步編程,但帶有回調函數的方法不必定是異步的。編程

// demo1(簡單callback封裝)
function successCallback() {
    console.log('callback');
}
function fn(successCallback) {
    console.log('這裏表示執行了一大堆各類代碼');

    // 其餘代碼執行完畢,最後執行回調函數
   successCallback instanceof Function && successCallback();
}
fn(successCallback);
複製代碼
// demo2(回調地域)

//callback hell
doSomethingAsync1(function(){
    doSomethingAsync2(function(){
        doSomethingAsync3(function(){
            doSomethingAsync4(function(){
                doSomethingAsync5(function(){
                    // code...
                });
            });
        });
    });
});
複製代碼

  能夠發現一個問題,在回調函數嵌套層數不深的狀況下,代碼還算容易理解和維護,一旦嵌套層數加深,就會出現「回調金字塔」的問題,就像demo2那樣,若是這裏面的每一個回調函數中又包含了不少業務邏輯的話,整個代碼塊就會變得很是複雜。從邏輯正確性的角度來講,上面這幾種回調函數的寫法沒有任何問題,可是隨着業務邏輯的增長和趨於複雜,這種寫法的缺點立刻就會暴露出來,想要維護它們實在是太痛苦了,這就是「回調地獄(callback hell)」。json

階段2 事件發佈/訂閱模式

  發佈訂閱模式,它定義了一種一對多的關係,可使多個觀察者對象對一個主題對象進行事件監聽,當這個主題對象發生改變時,依賴的全部對象都會被通知到。promise

var PubSub = function(){
    this.handlers = {}; 
};
PubSub.prototype.subscribe = function(eventType, handler) {
    if (!(eventType in this.handlers)) {
        this.handlers[eventType] = [];
    }
    this.handlers[eventType].push(handler); //添加事件監聽器
    return this;//返回上下文環境以實現鏈式調用
};
PubSub.prototype.publish = function(eventType) {
    var _args = Array.prototype.slice.call(arguments, 1);
    for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) {
        _handlers[i].apply(this, _args);// 遍歷事件監聽器
    }
    return this;
};
var event = new PubSub;// 構造PubSub實例
event.subscribe('list', function(msg) {
    console.log(msg);
});
event.publish('list', {data: ['one,', 'two']});// Object {data: Array[2]}
複製代碼

階段3 Deferred

  jQuery $.Deferred() 是一個構造函數,用來返回一個鏈式實用對象方法來註冊多個回調,而且調用回調隊列,傳遞任何同步或異步功能成功或失敗的狀態。成功回調done()中使用deferred.resolve,失敗回調fail()中使用deferred.rejectbash

var deferred = $.Deferred();
$.ajax(url, {
    type: "post",
    dataType: "json",
    data: data
}).done(function(json) [
    if (json.code !== 0) {
        showError(json.message || "操做發生錯誤");
        deferred.reject();
    } else {
        deferred.resolve(json);
    }
}).fail(function() {
    showError("服務器錯誤,請稍後再試");
    deferred.reject();
}).always(function() {
    if (button) {
        button.prop("disabled", false);
    }
});
return deferred.promise();
複製代碼

階段4 Promise

  Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
  所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。
  使用Promise對象能夠用同步操做的流程寫法來表達異步操做,避免了層層嵌套的異步回調,代碼也更加清晰易懂,方便維護,也能夠捕捉異常。服務器

function fn(num) {
  return new Promise(function(resolve, reject) {
    if (typeof num == 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(function() {
    console.log('第1個then:參數是一個number值');
  })
  .then(null, function() {
    console.log('第2個then');
  })
}
fn('haha');
fn(1234);// 第1個then:參數是一個number值、第2個then
複製代碼

異步編程解決方案的優缺點:app

callback回調 事件發佈/訂閱 Deferred Promise
優勢 簡單、容易理解 容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數 避免了層層嵌套的回調函數,有統一的API,使得控制異步操做更加容易 ES6將promise寫進了語言標準,統一了使用的語法,使用簡潔方便,比傳統異步解決方案更合理強大
缺點 不利於代碼的閱讀和維護,會出現「回調地域」,並且每一個任務只能指定一個回調函數 由事件驅動,運行流程變得很不清晰 狀態不可逆,肯定狀態後再次調用resolve/reject對原狀態不起任何做用 狀態不可逆

3、異步編程的實現機制

咱們從如下3個步驟來深刻理解異步編程的實現機制:異步

一、建立一個容器,存放多個callback

/**
 * 1.建立容器list
 * 2.依次執行
 */
(function(root){
    var _ = {
    	callbacks: function(options) {
            var list = [];
            var index, length;
            // 依次執行
            var fire = function(data) {
            	index = 0;
            	length = list.length;
            	for (; index < length; index++) {
                    list[index].apply(data[0], data[1]);
            	}
            }
            var self = {
                add: function() {
                    var args = [].slice.call(arguments);
                    args.forEach(function(fn){
                    	if(toString.call(fn) == '[object Function]') {
                    		list.push(fn);
                    	}
                    })
                },
                fireWith: function(context, arguments) {
                    var args = [context, arguments];
                    fire(args)
                },
                // 傳參
                fire: function() {
                    self.fireWith(this, arguments)
                }
            }
            return self;
    	}
    }
    root._ = _;
})(this);

// 調用
var callList = _.callbacks();
callList.add(function(){
    console.log('1111')
})
callList.add(function(){
    console.log('2222')
})
callList.fire();
複製代碼

二、容器添加狀態控制(stopFalse、once、memory)

1)stopFalse某個回調函數執行完,後面中止執行的狀態

var fire = function(data) {
    index = 0;
    length = list.length;
    for (; index < length; index++) {
    	if(list[index].apply(data[0], data[1]) === false && options.stopFalse) {// 配置了stopFalse狀態,且有回調函數返回false,終止後面執行
    	    break;
    	}
    }
}

// 調用
var callList = _.callbacks('stopFalse');
callList.add(function(){
    console.log('1111');
    return false;
});
callList.add(function(){
    console.log('2222')
});
callList.fire();// 111
複製代碼

2)once僅執行一次的狀態

fireWith: function(context, arguments) {
    var args = [context, arguments];
    // 第一次調fire:false || true
    // 調用fire後就將testing設爲true,!testing就爲false
    if(!options.once || !testing) {
    	fire(args)
    }
}

// 調用
var callList = _.callbacks('once');
callList.add(function(){
    console.log('1111');
});
callList.add(function(){
    console.log('2222')
});
callList.fire();// 1十一、222,只執行一次fire
callList.fire();
複製代碼

3)memory記住上一次執行的狀態

(function(root){
    var _ = {
        callbacks: function(options) {
            options = typeof options === 'string' ? createOptions(options) : {};
            
            var list = [];
            var index, length, testing, memory, start, starts;
            
            // 依次執行
            var fire = function(data) {
            	console.log(data)
            	memory = options.memory && data;
            	testing = true;
            	index = starts || 0;
            	starts = 0;
            	length = list.length;
            	for (; index < length; index++) {
            		if(list[index].apply(data[0], data[1]) === false && options.stopFalse) {
            			break;
            		}
            	}
            }
            var self = {
            	add: function() {
                    var args = [].slice.call(arguments);
                    start = list.length;
                    args.forEach(function(fn){
                    	if(toString.call(fn) == '[object Function]') {
                            list.push(fn);
                    	}
                    })
                    // 從新調用fire,執行callback
                    if(memory) {
                    	starts = start
                    	fire(memory)
                    }
            	},
            	fireWith: function(context, arguments) {
                    var args = [context, arguments];
                    // 第一次調fire:false || true
                    if(!options.once || !testing) {
                    	fire(args)
                    }
            	},
            	// 傳參
            	fire: function() {
                    self.fireWith(this, arguments)
            	}
            }
            return self;
        }
    }
    function createOptions(options) {
    	var obj = {};
    	options.split(/\s+/).forEach(function(value){
    		obj[value] = true;
    	})
    	return obj;
    }
    root._ = _;
})(this);

// 調用
var callList = _.callbacks('memory');
callList.add(function(){
    console.log('1111');
});
callList.add(function(){
    console.log('2222')
});
callList.fire(); // 11十一、2222
callList.add(function(){
    console.log('memory')
}); // memory
複製代碼

三、resolve、reject、notify等語法糖封裝

  若是理解了異步編程的容器建立和狀態控制,resolve、reject、notify等異步回調函數就更容易理解了,這僅僅是語法糖的封裝。封裝語法糖實現機制圖以下:async

實現代碼:

Deferred: function() {
    // 建立語法糖的過程
    var tuples = [
    	[ "resolve", "done", _.callbacks("once memory"), "resolved" ],
    	[ "reject", "fail", _.callbacks("once memory"), "rejected" ],
    	[ "notify", "progress", _.callbacks("memory") ]
    ]
    
    var promise = {}; // 添加數據(回調)
    var deferred = {}; // 狀態控制
    var state; // 最終狀態
    tuples.forEach(function(tuple, i) {
    	var callList = tuple[2],
    		stateString = tuple[3];
    	// promise[ done | fail | progress] = callList.add
    	promise[tuple[1]] = callList.add;
    
    	// Handle state
    	if(stateString) {
    		// state = [ resolved | rejected ]
    		state = stateString;
    	}
    
    	// deferred[ resolve | reject | notify] = callList.fire
    	deferred[tuple[0]] = function() {
    		deferred[tuple[0] + 'With'](this === deferred ? promise : this, arguments);
    		return this;
    	}
    	deferred[tuple[0] + 'With'] = callList.fireWith;
    	
    	return deferred;
    })
}
複製代碼

小結

  現在異步編程還在不斷精進和改善中,又有generator+coasync+await等異步編程方式的出現。咱們重要的是理解異步編程的機制,這樣才能更快地學習掌握突飛猛進的異步編程方式~~

相關文章
相關標籤/搜索