1. 程式人生 > >異步三部曲之回調

異步三部曲之回調

scrip 擔心 麻煩 現在 mic som 遇到的問題 out AS

概述

這是我看你不知道的JavaScript(中卷)的讀書筆記,供以後開發時參考,相信對其他人也有用。

異步機制

分塊的程序:我們寫的代碼有一部分是{現在運行的},其余的則是{將來運行的}。

我們不把它們分開寫,因為它們是有聯系的,比如{將來運行的代碼}需要部分{現在運行的代碼}的變量,那麽怎麽使這些變量在{現在運行的代碼}運行結束後仍然存在並且能被{將來運行的代碼}調用?答案很簡單,就是閉包,我們把{將來運行的代碼}放在一個函數作用域中,使它能夠使用外部作用域的變量,而且,即使外部作用域被銷毀,這些變量也一直存在。而產生這個閉包的函數就被稱為回調函數

我們寫的代碼中,可能不止一個地方需要在將來運行,一般的情況是,js的主線程運行完{現在運行的代碼}之後,繼續運行{將來運行的代碼1},運行完之後繼續運行{將來運行的代碼2}。。。所以當運行{現在運行的代碼}的時候,{將來運行的代碼1},{將來運行的代碼2}。。。這些將來運行的代碼放在哪兒?答案是放在一個隊列裏面,這個隊列被稱為任務隊列

於是,主線程在運行完{現在運行的代碼}之後,會拿出任務隊列中的{將來運行的代碼1}運行,運行完之後繼續拿出任務隊列中的{將來運行的代碼2}運行。。。這種主線程不斷拿出任務隊列中的代碼運行的機制被稱為事件循環

需要註意的是,可能有這麽一個情況,在運行{將來運行的代碼1}的時候,又發現了一個{將來運行的代碼x},這個時候會重新創建一個任務隊列2,並且把{將來運行的代碼x}塞進去,等之前的任務隊列中的代碼運行完之後再來運行任務隊列2中的代碼。

需要註意的第二點是,{將來運行的代碼1},{將來運行的代碼2},,,{將來運行的代碼x}的運行順序並不一定是隊列中先進先出的順序,通常情況是,各自滿足一定條件之後才運行,比如多少秒之後,或者接收到某個數據之後。

需要註意的第三點是,在es6之前,這個任務隊列並不是js創建的,而是瀏覽器實現的,它一般被用來運行settimeout和ajax等異步操作。

在es6,js規範了任務隊列,這個任務隊列叫做microtask,而之前的任務隊列被叫做macrotask,microtask用來運行promise等異步操作,並且運行在同一個事件循環的macrotask之前。

並發

由於js的異步機制,導致js在運行的時候能看起來好像同時處理多個任務,這種同時發生的情況就叫做並發。實現並發還有另一個機制,就是多個進程或線程同時運行,這種多個進程或線程同時運行的情況,就叫做並行

在並發時候有一個很重要的情況,就是未來執行的代碼的執行先後順序會對最終結果產生影響。示例如下,塊2和塊3執行順序的不同會造成a,b最後取值的不同。這種代碼運行順序的不確定性就被稱為競態條件。

//塊 1:
var a = 1;
var b = 2;
//塊 2( foo() ):
a++;
b = b * a;
a = b + 3;
//塊 3( bar() ):
b--;
a = 8 + b;
b = a * 2;

一個很現實的異步競態條件例子如下:

var a, b;
function foo(x) {
    a = x * 2;
    baz();
}
function bar(y) {
    b = y * 2;
    baz();
}
function baz() {
    console.log(a + b);
}
// ajax(..)是某個庫中的某個Ajax函數
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

怎麽處理這種競態條件呢?方法是加一個判斷(所以判斷在異步中非常常用)。

var a, b;
function foo(x) {
    a = x * 2;
    if (a && b) {
        baz();
    }
}
function bar(y) {
    b = y * 2;
    if (a && b) {
        baz();
    }
}
function baz() {
    console.log( a + b );
}
// ajax(..)是某個庫中的某個Ajax函數
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

這種需要2個異步同時完成就叫做門(gate),另一種是我們只需要最先完成的異步的數據,這種情況就叫做閂(latch),實例如下:

var a;
function foo(x) {
    if (!a) {
        a = x * 2;
        baz();
    }
}
function bar(x) {
    if (!a) {
        a = x / 2;
        baz();
    }
}
function baz() {
    console.log( a );
}
// ajax(..)是某個庫中的某個Ajax函數
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

回調函數

我們上面說了,我們一般把未來執行的代碼包裹在一個回調函數裏面,等滿足某個條件之後再執行,比如下列代碼:

listen( "click", function handler(evt){
    setTimeout( function request(){
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} )

初看之下,回調函數貌似看起來非常清晰,但是這只是表面的,再來看下面這段偽代碼,其中doABCDEF都是異步函數。

doA( function(){
    doB();
    doC( function(){
        doD();
    } )
    doE();
} );
doF();

實際運行順序並不是ABDEF,而是AFBCED。當嵌套更多的時候,會更加復雜,需要看半天才能知道執行順序。這就是著名的回調地獄。(註意,回調地獄並不是說嵌套太多了由於縮進寫起來不方便,而是嵌套多了之後可讀性很差。)

信任問題

回調地獄只是回調問題的一部分,還有一些更加深入的問題需要考慮。比如下面這個例子:

// A
ajax( "..", function(..){
    // C
} );
// B

執行A和B以後我們會執行異步代碼塊C。就是說,現在異步代碼塊C獲得了程序的全部控制權,可以控制作用域中的全部變量和方法。

這個時候,我們有理由擔心:

  1. 異步代碼塊C根本不執行怎麽辦?
  2. 異步代碼塊C調用所需要的變量也是異步的沒拿到怎麽辦?(調用過早)
  3. 異步代碼塊C調用太晚了怎麽辦?
  4. 異步代碼塊C獲得的ajax數據不符合規範怎麽辦?
  5. 異步代碼塊C執行的時間太長怎麽辦?可能永久執行?
  6. 錯誤被吞掉怎麽辦?

更一般的情況是,我們有時候執行的這個代碼塊C是一個第三方函數,我們看不見。這個時候由於代碼塊C能夠調用作用域中的全部變量和方法,如果這個第三方函數對這些變量亂改怎麽辦?

上面就是回調函數帶來的信任問題,根源是我們把控制權交給了回調函數C。

當然,上面的問題有補救方法,但是要處理所有這些問題依然非常麻煩。細心的人可能看出來了,上面有部分問題是由於異步和同步同時進行導致的,而這也引出了一個非常有效的建議:永遠異步調用回調,即使在事件循環的下一輪。比如下面的代碼:

function result(data) {
    console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;

如果ajax獲得數據的速度比console.log的IO端口讀寫更快的話(cache儲存),會打印0,否則會打印1。

需要說明的是,即使我們在回調函數中遇到了這麽多問題呢,但是在小項目中,我們實際遇到的問題會少很多,所以用回調還是很安全的。

異步三部曲之回調