你不知道的JS(中冊)

本文系你不知道的JS(中冊)讀書筆記前端

第二部分 異步和性能

第1章 異步:如今與未來

事實上,程序中如今運行的部分和未來運行的部分之間的關係就是 異步編程的核心ajax

1.1 分塊的程序

function now() {
    return 21;
}

function later() {
    answer = answer * 2;//未來執行部分
    console.log( "Meaning of life:" answer );//未來執行部分
}

var answer = now();

setTimeout( later, 1000 ); //Meaning of life: 42

複製代碼
console.*方法族不是JavaScript正式的一部分,而是**宿主環境**添加到JavaScript中的。
在某些條件下,某些瀏覽器的console.log(..)並不會把傳入的內容當即輸出。出現這種狀況的主要緣由是,在許多程序(不僅是JavaScript)中,I/O是很是低速的阻塞的部分。因此,(從頁面/UI的角度來講)瀏覽器在後臺異步處理控制檯I/O可以提升性能。
複製代碼

1.2 事件循環

JavaScript引擎自己並無時間的概念,只是一個按需執行JavaScript任意代碼片斷的環境。「事件」(JavaScript代碼執行)調度老是由包含它的環境進行。算法

什麼是事件循環?編程

//eventLoop是一個用做隊列的數組
//(先進先出)
var eventLoop = [];
var event;

//「永遠執行」
while (true) {
    //一次tick(循環的每一輪稱爲一個tick)
    if (eventLoop.length > 0) {
        event = eventLoop.shift();
        
        //如今,執行下一個事件
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}
複製代碼

必定要清楚,setTimeout(..)並無把你的回調函數掛在事件循環隊列中。它所作的時設定一個定時器。當定時器到時後,環境會把你的回調函數放在事件循環中,這樣,在將來某個時刻的tick會摘下並執行這個回調。數組

ES6從本質上改變了在哪裏管理事件循環,ES6精確指定了事件循環的工做細節。------> Promise.promise

1.3 並行線程

異步是關於如今和未來的時間間隙,而並行是關於可以同時發生的事情。瀏覽器

並行計算最多見的工具就是進程線程。進程和線程獨立運行,並可能同時運行:在不一樣的處理器,甚至不一樣的計算機上,但多個線程可以共享單個進程的內存bash

與之相對的是,事件循環把自身的工做分紅一個個任務並順序執行,不容許對共享內存的並行訪問和修改。經過分立線程中彼此合做的事件循環,並行和順序執行能夠共存。網絡

多線程並行共享內存地址,交錯進行,結果不定,但JS不容許(單線程)。多線程

  • 完整運行

在Javascript的特殊中,函數順序的不肯定性就是一般所說的競態條件

1.4 併發

例子:

用戶向下滾動加載更多內容至少須要連個獨立的「進程」同時運行。「進程爲虛擬進程,或者稱爲任務」,一個「進程」觸發onscroll事件並響應,一個「進程」接收Ajax響應。

兩個或多個「進程」同時執行就出現了併發,無論組成它們的單個運算是否並行執行(在獨立的處理器或處理器核心上同時運行)。能夠把併發看做「進程」級(或者任務級)的並行,與運算級的並行相對。

  • 非交互

順序不影響對代碼的執行結果。

  • 交互
var res = [];
function response(data) {
    res.push( data );
}

//ajax(..)是某個庫中提供的某個Ajax函數
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
複製代碼

協調相互處理競態

var res = [];
function response(data) {
    if( data.url == "http://some.url.1") {
        res[0] = data;
    }
    else if(data.url == "http://some.url.2") {
        res[1] = data;
    }
}

//ajax(..)是某個庫中提供的某個Ajax函數
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
複製代碼
  • 協做

併發協做是取到一個長期運行的「進程」,並將其分割成多個步驟或多批任務,使得其餘併發「進程」有機會將本身的運算插入到事件循環隊列中交替運行。

1.5 任務

在事件循環的每一個tick中,可能出現的異步動做不會致使一個完整的新事件添加到事件循環隊列中,而會在當前tick的任務隊列末尾添加一個項目(一個任務)。

事件循環隊列和任務隊列(插隊接着玩)

console.log("A");

setTimeout( function() {
    console.log( "B" ); 
}, 0); //下一個事件循環tick

//理論上的「任務API」
schedule( function(){
    console.log( "c" );
    schedule(function(){
        console.log( "D" );
    });
});

// A C D B
複製代碼

1.6 語句順序

編譯器語句重排幾乎就是併發和交互的微型隱喻。

第2章 回調

回調函數包裹或者說封裝了程序的延續。

2.1 嵌套回調與鏈式回調

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)
});
//也被稱爲回調地獄或者毀滅金字塔
複製代碼

爲了不在函數上跳來跳去,可以使用硬編碼 ,但 硬編碼 確定會使代碼更脆弱,由於它並無考慮可能致使步驟執行順序偏離的異常狀況(當其中某個步驟失敗報錯的狀況)。

2.2 信任問題

//A
ajax( "..", function(..) {
    //C
});
//B
複製代碼

有時候ajax(..)不是你編寫的代碼,也不在你的直接控制下,多數狀況下。它是某個第三方提供的工具。

這時候就會出現控制反轉,把本身程序一部分的執行控制交給第三方。

控制反轉的修復:

var tracked = false;
 analytics.trackPurchase( purchaseData, function() {
    if(!tracked) {
        tracked = true; //只能回調一次
        chargeCreditCard();
        displayThankyouPage();
    }
 })
複製代碼

2.3 挽救回調(失敗)--- Promise

第3章 Promise

回調錶達程序異步和管理併發的兩個主要缺陷:缺少順序性和可信任性。

不把本身的程序的continuation傳給第三方,而是但願第三方給咱們提供瞭解其任務什麼時候結束的能力,而後由咱們本身的代碼來決定下一步作什麼。---> Promise

3.1 什麼是Promise

Promise就是在快餐店點餐付款以後服務員給你的收據小票,這是一個承諾,你將在後續憑藉這張小票拿到你的麪包。

從外部看,因爲Promise封裝了依賴時間的狀態 --- 等待底層值的完成或拒絕,因此Promise自己是與時間無關的。一旦Promise決議,它就永遠保持這個狀態。

使用回調的話,通知就是任務(foo(..))調用的回調。而使用Promise的話,咱們把這個關係反轉了過來,偵聽來自Foo(..)的事件,而後在獲得通知的時候,根據狀況繼續。

看一下僞代碼

foo(x) {
    //開始作點可能耗時的工做
    //構造一個listener事件通知處理對象來返回
    return listener;
}

var evt = foo( 42 );

evt.on ("completion", function() {
    // 能夠進行下一步了!
});

evt.on ( "failure", function(err) {
    // 啊,foo(..)中出錯了
})
複製代碼

對回調模式的反轉其實就是對反轉的反轉,或者說反控制反轉 --- 把控制還給調用代碼

  • 兩種Promise模式
//第一種
function foo(x) {
    //開始作一些可能耗時的工做
    
    //構造並返回一個Promise
    return new Promise( function(resolve, reject){
        //最終調用resolve(..)或者reject(..)
        //這是這個promise的決議回調
    });
}

var p = foo( 42 );

bar( p );
baz( p );
複製代碼
//第二種
function bar() {
    // foo(..)確定已經完成,因此執行bar(..)的任務
}

function oopsBar() {
    // 啊,foo(..)出錯了,因此bar(..)沒有執行
}

//對於baz()和oopsBaz()也是同樣
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
複製代碼

3.2 具備then方法的鴨子類型

要肯定某個值是否是真正的Promise ,用 p instance of Promise是不夠的。由於Promise值多是從其餘瀏覽器窗口(iframe)接收到的。不一樣窗口/不一樣iframe。此外,庫和框架會選擇實現本身的Promise,而不是使用原生的ES6 Promise實現。

識別Promise(或者行爲相似於Promise的東西)就是定義某種稱爲thenable的東西,將其定義爲任何具備then(..)方法的對象和函數。

全部值(或其委託),無論是過去的、現存的仍是將來的,都不能擁有then(..)函數,無論是有意的仍是無心的;不然這個值在Promise系統中就會被誤認爲是一個thenable,這可能致使難以追蹤的bug。

3.3 Promise的信任問題

  • 調用過早

主要是代碼是否會引入相似Zalgo(同步異步混亂)這樣的反作用。Promise能夠經過回調老是被異步調用來解決這個問題。

  • 調用過晚

Promise基於任務「插隊」

  • 回調未調用

Promise自己永遠不會被決議的解決辦法:--- (一種稱爲競態的高級抽象機制)

// 用於超時一個Promise工具
function timeoutPromise(delay) {
    return new Promise( function(resolve, reject){
        setTimeout( function(){
            reject( "Timeout" )
        }, delay)
    })
}

//設置foo()超時
Promise.race([
    foo(); //試着開始foo()
    timeoutPromise( 3000 ); //給它3秒鐘
]).then(
    function() {
        // foo(..)及時完成
    },
    function(err){
        // 或者foo()被拒絕,或者只是沒能按時完成
        //查看err來了解是哪一種狀況
    }
)
複製代碼
  • 調用次數過少或過多

Promise將只會接受第一次決議,並默默地忽略任何後續調用。

  • 未能傳遞參數/環境值

Promise至多隻能有一個決議值(完成或拒絕)

若是使用多個參數調用resolve(..)或者reject(..),第一個參數以後的全部參數都會被默默忽略。要傳遞多個值,則須要把它們封裝在單個值中傳遞,好比經過一個數組或對象。

對環境來講,JavaScript中的函數老是保持其定義所在的做用域的閉包。

  • 吞掉錯誤或異常

Promise甚至會把JavaScript異常也變成了異步行爲,進而極大下降了競態條件出現的可能,解決潛在的Zalgo風險(同步異步混亂)。

  • 是可信任的Promise嗎?(可預見/可靠)

使用Promise過濾獲得可信任值。

//這麼作使得foo(42)返回值可靠
Promise.resolve( foo(42) )
.then( function (){
    console.log( V ); 
} );
複製代碼

3.4 鏈式流

連石榴能夠實現的關鍵在於如下兩個Promise固有行爲特性:

  • 每次你對Promise調用then(..),它都會建立並返回一個新的Promise,能夠將其連接起來;
  • 無論從then(..)調用的完成回調(第一個參數)返回的值是什麼,它都會被自動設置爲被連接Promise(第一點中的)完成。

Promise.resolve(..)會直接返回接收到的真正Promise,或展開接收到的thenable值,並在持續展開thenable的同時遞歸地前進。

若是不顯式返回一個值,就會隱式返回````undefined,而且這些promise```仍然會以一樣的方式連接在一塊兒。

var p = Promise.resolve( 42 );
p.then(
    //假設的完成處理函數,若是省略或者傳入任何非函數值
    //function(v) {
        return v;
    }
    null,
    function rejected(err) {
        //永遠不會到達這裏
    }
)
複製代碼

默認的完成處理函數只是吧接受到的任何傳入值傳遞給下一個步驟(promise)而已。

then( null, function(err){...} )只處理拒絕模式 === catch( function(){...} );

若是向reject(..) 傳入一個Promise/thenable值,它會把這個值原封不動的設置爲拒絕理由。後續的拒絕處理函數接收到的是你實際傳給reject(..)的那個Promise/thenable,而不是其底層的當即值。

3.5 錯誤處理

try-catch只能是同步的,沒法用於異步代碼模式。

Promise採用分離回調風格,一個回調用於完成狀況,一個回調用於拒絕狀況。

  • 絕望的陷阱(使用catch捕捉不到的錯誤,未被查看的錯誤)
  • 處理未捕獲的狀況(瀏覽器有一個特有的功能是咱們的代碼所沒有的:它們能夠跟蹤並瞭解全部對象被丟棄以及被垃圾回收的時機)
  • 成功的坑(defer)

3.6 Promise模式

  • promise.all([..])
  • promise.race([..])
//超時競賽
//爲foo()設定超時
Promise.race([
    foo(),    //啓動foo()
    timeoutPromise( 3000 )   //給它三分鐘
])
.then(
    function() {
        //foo()按時完成
    },
    function(err){
        //要麼foo()被拒絕,要麼只是沒能按時完成
        //所以要查看err瞭解具體緣由
    }
)
複製代碼
//finally
var p = Promise.resolve( 42 );

p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );

複製代碼

3.7 Promise的侷限性

  • 順序錯誤處理
  • 單一值
  • 單決議
  • 慣性
  • 沒法取消的Promise

第四章 生成器

用回調錶達異步控制流程的兩個關鍵缺陷:

  1. 基於回調的異步不符合大腦對任務步驟的規劃方式;
  2. 因爲控制飯莊,回調並非可信任或可組合的。

只有控制生成器的迭代器具備恢復生成器的能力。

生成器爲異步代碼保持了順序,同步,阻塞的代碼模式。

4.1 生成器的定義

生成器是一種特殊的函數。

yield..next(..)這一對組合起來,在生成器的執行過程當中構成了一個雙向消息傳遞系統。(啓動生成器,即第一個next,不傳值)

每次構建一個迭代器,實際上就隱式構建了生成器的一個實例,經過這個迭代器來控制的是這個生成器的實例。同一個生成器的多個實例能夠同時運行,甚至能夠彼此交互。

相互交替執行的生成器內部具備同名變量,但這些變量是彼此獨立,相互之間沒有聯繫。

4.2 生成器的產生值

生成器做爲一種產生值的方式。

var something = (function(){
    var nextVal;
    
    return {
        //for...of循環須要
        //計算屬性名:指定一個表達式並用這個表達式的結果做爲屬性的名稱
        [Symbol.iterator]: function(){return this;},
        
        //標準迭代器接口方法
        next: function() {
            if(nextVal === undefined){
                nextVal = 1;
            }else{
                nextVal = (3*nextVal) + 6;
            }
            return { done: false, value: nextVal };
        }   
    };
})();

something.next().value; //1
something.next().value; //9
something.next().value; //33
something.next().value; //105
複製代碼

Object.keys(..)並不包含來自於[[Prototype]]鏈上的屬性。而for..in 會包含。

從ES6開始,從一個iterable中提取迭代器的方法是: iterable必須支持一個函數,其名稱是專門的ES6符號值Symbol.iterator。調用這個函數時,它會返回一個迭代器。一般每次調用會返回一個全新的迭代器,雖然這一點並非必須的。

嚴格說來,生成器自己不是iterable,當你執行一個生成器,就獲得了一個迭代器。

function *foo() { .. }
var it = foo()
複製代碼

生成器把while..true帶回了JavaScript編程的世界。

生成器會在每個yield處暫停,這意味着不須要閉包也可在調用之間保持變量狀態。

生成器內有try..finally語句,它將老是運行,即便生成器已經外部結束。若是須要清理資源的話,這一點很是有用。

function *something() {
    try{
        var nextVal;
        while (true) {
            if(nextVal === undefined){
                nextVal = 1;
            }else{
                nextVal = ( 3*nextVal ) + 6
            }
            
            yield nextVal;
        }
    }
    //清理
    finally{
        console.log( "clean up!");
    }
}
複製代碼

4.3 異步迭代生成器

function foo(x, y) {
    ajax(
        "http://some.url.1/?x=" + x + "&y=" + y,
        function (err, data) {
            if(err) {
                //向*main()拋出一個錯誤
                it.throw( err );
            }else{
                //用收到的data回覆*main()
                it.next();
            }
        }
    );
}

function *main() {
    try{
        //這裏的yield有一個暫停,會等待foo()的完成再賦值給text
        var text = yield foo(11, 31);
        console.log(text);
    }
    catch(err) {
        console.error( err );
    }
}

var it = main();

//這裏啓動
it.next();
複製代碼

把異步做爲實現細節抽象了出去,使得咱們能夠以同步順序的形式追蹤流程控制:「發出一個Ajax請求,等它完成以後打印出響應結果。」

生成器的yield暫停的特性意味着咱們不只可以從異步函數調用獲得看似同步的返回值,還能夠同步捕獲(try..catch)來自這些異步函數調用的錯誤。

function *main() {
    var x = yield "Hello World";
    
    //永遠不會到達這裏
    console.log( x );
}

var it = main();
it.next();

try{
    //*main()會處理這個錯誤嗎?看看吧
    it.throw("oops");
}catch(err) {
    //不行,沒有處理!
    console.log( err ); //oops
}
複製代碼

4.4 生成器+Promise

ES6中最完美的世界就是生成器(看似同步的異步代碼)和Promise(可信任可組合)的結合。

得到Promise和生成器最大效應的最天然的方法就是yield出來一個Promise,而後經過這個Promise來控制生成器的迭代器。

function foo(x, y){
    return request{
        "http://some.url.1/?x=" + x + "&y=" + y
    };
}

function *main() {
    try {
        var text = yield foo(11, 31);
        console.log( text );
    }
    catch(err){
        console.error( err );
    }
}

var it = main();

var p = it.next().value;
//等待Promise p決議
p.then(
    function(text) {
        console.log( text);
    },
    function(err) {
        it.throw( err )
    }
)
複製代碼
  • 支持PromiseGenerator Runner
function run(gen) {
    //...
}

function *main() {
    //..
}

run( main );
複製代碼

ES7: asyncwait

function foo(x, y){
    return request{
        "http://some.url.1/?x=" + x + "&y=" + y
    };
}

//不是生成器函數,而是async函數
async function main() {
    try {
        //再也不yield出Promise,而是await等待它決議
        var text = await foo( 11, 31 );
        console.log( text );
    }
    catch( err ) {
        console.error( error );
    }
}

main();
複製代碼
  • 生成器中的Promise併發
function *foo() {
    //var r1 = yield request( "http://some.url.1" );
    //var r2 = yield request( "http://some.url.2" );
    
    <!--併發模式-->
    //var p1 =  request( "http://some.url.1" );
    //var p2 =  request( "http://some.url.2" );
    
    //var r1 = yield p1;
    //var r2 = yield p2;
    
    var results = yield Promise.all( [
        request( "http://some.url.1" );
        request( "http://some.url.2" );
    ] );
    
    //數組結構
    var [r1, r2] = results;
    
    var r3 = yield request( "http://some.url.3?v=" + r1 + "&w=" + r2 );
    
    console.log( r3 );
}

//使用前面定義的工具run(..)
run( foo );
複製代碼

若是要實現一系列高級流程控制的話,那麼很是有用的作法是:把你的Promise邏輯隱藏在一個只從生成器代碼中調用的函數內部。如:

function bar() {
    Promise.all( [
        baz(..)
        .then(..),
        Promise.race( [..] )
    ] )
    .then(..)
}
複製代碼

4.5 生成器委託

yield * 暫停了迭代控制,而不是生成器控制。

  • 爲何用委託

yield委託的主要目的是組織代碼,以達到與普通函數調用的對稱。

  • 消息委託

yield不僅用於迭代器控制工做,也用於雙向信息傳遞工做。

...
複製代碼

和yield委託透明的雙向傳遞信息的方式同樣,錯誤和異常也是雙向傳遞的。

  • 異步委託
  • 遞歸委託

4.6 生成器併發

// Request(..)是一個支持Promise的Ajax工具
var res = [];
function *reqData(url) {
    var data = yield request( url );
    
    //控制轉移
    yield;
    res.push( data );
}

var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );

var p1 = it1.next();
var p2 = it2.next();

p1.then( function(data){
    it1.next(data);
});

p2.then( function(data){
    it2.next(data);
});

Promise.all( [p1,p2] )
.then( function(){
    it1.next();
    it2.next();
} );
複製代碼

4.7 形實轉換程序(thunk

狹義表述: thunk是指一個用於調用另一個函數的函數,沒有任何參數

換句話說,你用一個函數定義封裝函數調用,包括須要的任何參數,來定義這個調用的執行,那麼這個封裝函數就是一個形實轉換程序。

function foo(x,y) {
    return x + y;
}

function fooThunk() {
    return foo( 3, 4 );
}

//未來
console.log( fooThunk() ); //7
複製代碼

thunkory (thunk+factory) --- 生成thunk的工廠模式

var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory(3,4);
var fooThunk2 = fooThunkory(5,6);

//未來
fooThunk1 (function(sum){
    console.log( sum ); //7
})
fooThunk2 (function(sum){
    console.log( sum ); //11
})
複製代碼

Promise要比裸thunk功能更強,更值得信賴。

4.8 ES6以前的生成器

第5章 程序性能

5.1 Web Worker

JavaScript當前並無任何支持多線程執行的功能。

可是,像瀏覽器這樣的環境,很容易提供多個JavaScript引擎實例,各自運行在本身的線程上,這樣你就能夠在每一個線程上運行不一樣的程序。程序中每個這樣獨立的多線程部分被成爲一個(Web)Worker。這種類型的並行化被成爲任務並行,由於其重點在於把程序劃分爲多個塊併發運行。

//專用Worker
var w1 = new Worker( "http://some/url.1/mycoolworker.js" )
複製代碼

除了這樣指向外部文件的URL的專用Worker,還能夠建立一個在想Worker,本質上就是一個存儲在單個(二進制)值中的在線文件。

Worker之間以及它們和主程序直接,不會共享任何做用域資源,那會把全部多線程編程的噩夢帶到前端領域,而是經過一個基本的事件消息機制相互聯繫。

w1偵聽事件

w1.addEventListener( 'message', function(evt){
    //evt.data
})
複製代碼

w1發送事件

w1.postMessage( 'sth cool to say');
複製代碼

要在建立Worker的程序中終止Worker,能夠調用Worker對象上的terminate()。忽然終止Worker線程不會給它任何計劃完成它的工做或者清理任何資源。相似於經過關閉瀏覽器標籤頁來關閉頁面。

  • Worker環境

在Worker內部是沒法訪問主程序的任何資源的。這意味着你不能訪問它的任何全局變量,也不能訪問頁面的DOM或者其餘資源。但能夠執行網絡操做(Ajax,WebSockets)以及設定定時器。並且Worker、能夠訪問幾個重要的全局變量和功能的本地複本。如navigator, location, JSON和applicationCache。

能夠經過importScripts(..)向Worker同步(阻塞餘下的Worker執行,直到文件加載和執行完成)加載額外的JS腳本:

//在Worker內部
importScripts('foo.js', 'bar.js');
複製代碼

Web Woker一般應用有:

處理密集型數據計算
大數據集排序
數據處理(壓縮,音頻分析,圖形處理)
高流量網絡通訊
複製代碼
  • 數據傳遞

    使用解構克隆算法
      使用Transferable對象(對象全部權的轉移)
    複製代碼
  • 共享Worker(shareWorker,下降系統的資源使用)

  • 模擬Web Worker(兼容老式瀏覽器)

5.2 SIMD(單指令數據)

單指令數據是一種數據並行方式,與Web Worker的任務並行相對,由於這裏的重點實際上再也不是把程序邏輯分紅並行的塊,而是並行處理數據的多個位。

var v1 = SIMD.float32*4(3.14159, 21.0, 32.3, 55.55);
var v2 = SIMD.float32*4(2.1, 3.2, 4.3, 5.4);

SIMD.float32*4.mul(v1, v2);
//[6.597339, 67.2, 138.89, 299.97]

複製代碼

5.3 asm.js

asm.js這個標籤是指JavaScript語言中能夠高度優化的一個子集。

經過當心避免某些難以優化的機制和模式(垃圾收集、類型強制轉換,等),asm.js風格的代碼能夠被JavaScript引擎識別並進行特別幾斤的底層優化。

var a = 42;
//..
var b = a | 0;
//b應該老是被看成32位整型來處理,這樣就能夠省略強制類型轉換追蹤。
複製代碼

對JavaScript性能影響最大的因素是內存分配,垃圾收集和做用域訪問。

相關文章
相關標籤/搜索