第一章 異步:如今與未來web
程序中如今運行的部分和未來運行的部分之間的關係就是異步編程的核心。ajax
場景:等待用戶輸入、從數據庫或文件系統中請求數據、經過網絡 發送數據並等待響應,或者是在以固定時間間隔執行重複任務(好比動畫)算法
1.1 分塊的程序數據庫
最多見的塊單位是函數。 從如今到未來的等待,最簡單的方法是使用一個一般稱爲回調函數的函數。編程
只要把一段代碼包裝成一個函數,並指定它在響應某個事件(定時器、鼠標點 擊、Ajax 響應等)時執行,你就是在代碼中建立了一個未來執行的塊,也由此在這個程序 中引入了異步機制。數組
1.2 事件循環promise
一個事件循環處理的示例瀏覽器
var eventLoop = []; var event; var reportError = function(err) { console.log('An error happened!'); console.log(err); } // 永遠循環 while(true){ // 一次tick if(eventLoop.length) { // 移出第一個事件 event = eventLoop.shift(); try{ event(); } catch(err){ reportError(err); } } }
諸如setTimeout()等方法,並不會直接將回調函數掛在事件循環隊列中,而是設置一個定時器,等定時器到時後,環境會把回調函數放在事件循環中。性能優化
1.3 並行線程websocket
並行是關於可以同時發生的事情。 並行最多見的就是進程和線程。進程和線程獨立運行,並可能同時運行:多個線程可以共享單個進程的內存。 JS因爲其單線程的特性,其不肯定性是在函數(事件)順序級別上,而不是多線程狀況下的語句順序級別。這種函數順序的不肯定性就是常說的競態條件(race condition)。
1.4 併發
1.4.1 非交互
若是進程間沒有互相影響的話,不肯定性是徹底能夠接受的。
1.4.2 交互
併發的操做須要相互交流,經過做用域或DOM間接交互。若是出現這樣的交互,就須要對它們的交互進行協調以免競態的出現。 一個可能的優化是
// 假設分頁狀況下 var res = []; function response(data) { res[data.page] = data.items; } ajax('url?page=0',response); ajax('url?page=1',response);
1.4.3 協做
併發協做:取到一個長期運行的進程,並將其分割成多個步驟或多批任務,使得其餘併發「進程」有機會將本身的運算插入到時間循環隊列中交替運行。 本質上是對於一個高性能消耗的操做,將其分割開進行處理。
var res = []; function response(data) { // 假設1000萬條數據,一次處理1000條 var chunk = data.splice(0, 1000); res = res.concat(chuck.map((value)=>{ return value * 2; })); // 若是有後續,異步觸發 if(data.length > 0) { setTimeout(function() { response(data); }, 0); } }
1.5 任務
任務隊列(job queue)。它是掛在事件循環隊列的每一個 tick 以後 的一個隊列。在事件循環的每一個 tick 中,可能出現的異步動做不會致使一個完整的新事件 添加到事件循環隊列中,而會在當前 tick 的任務隊列末尾添加一個項目(一個任務)。
第二章 回調
2.2 順序的大腦
2.2.1 執行與計劃
異步不是多任務,而是快速的上下文切換。 編寫異步代碼的困難之處就在於,這種思考/計劃的意識流對咱們中的絕大多數來講是不天然的。
2.3 信任問題
類型檢查/規範化的過程對於函數輸入是很常見的,即便是對於理論上徹底能夠信任的代碼。
function addNumbers(x, y) { // 爲了防止變成字符串拼接,檢查類型,若是不符合就拋出錯誤 if(typeof x !== 'number' || typeof y !== 'number') { throw new Error('Bad arguments!'); } // 另外一種作法是提早轉換類型 x = Number(x); y = Number(y); return x + y; }
永遠異步調用回調,即便就 在事件循環的下一輪,這樣,全部回調就都是可預測的異步調用了。
第三章 Promise
回調錶達程序異步和管理併發的兩個主要缺陷:缺少順序性 和可信任性
3.1什麼是Promise
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise(fn) { this._state = PENDING; this._value = null; // 用樹形結構去記錄then註冊的hanlder,而後等到主Promise決議了,在_excuteThen中操做then返回Promise的狀態和結果值 this._handlers = []; if (fn) { this._enqueue(fn.bind(null, this._resolve.bind(this), this._reject.bind(this))); } return this; } /** * Promise的then方法。 * 註冊新的handler,掛載到handlers樹上。 * 返回該hanlder的promise引用,用於鏈式調用。 * 返回的promise的_value取決於then傳入的onFulfilled或onRejected函數的返回值。 * @param {*} onFulfilled * @param {*} onRejected */ Promise.prototype.then = function (onFulfilled, onRejected) { var handler = { onFulfilled: onFulfilled, onRejected: onRejected, promise: new Promise(), }; // 先壓入堆棧,造成樹形結構 var index = this._handlers.push(handler) - 1; // 已經決議了,直接執行then if (this._state !== PENDING) { this._excuteThen(handler); } return this._handlers[index].promise; }; /** * Promise的resolve方法。 * 行爲包括狀態置位,修改結果,而後執行先前壓入堆棧的then * @param {*} result */ Promise.prototype._resolve = function (result) { // 只有PENDING狀況下才能修改狀態 if (this._state === PENDING) { this._state = FULFILLED; this._value = result; this._handlers.forEach(this._excuteThen.bind(this)); } }; /** * Promise的reject方法 * @param {*} result */ Promise.prototype._reject = function (result) { // 只有PENDING狀況下才能修改狀態 if (this._state === PENDING) { this._state = REJECTED; this._value = reason; this._handlers.forEach(this._excuteThen.bind(this)); } }; /** * 執行then,同時也通知then返回的promise去執行resolve或reject。 * then中的_value最終使用的值是then中onFulfilled或onRejected的返回值。 * @param {*} handler */ Promise.prototype._excuteThen = function (handler) { if (this._state === FULFILLED && typeof handler.onFulfilled === 'function') { // 異步觸發then this._enqueue( handler.promise._resolve.bind( handler.promise, handler.onFulfilled(this._value) ) ); // 同步觸發then /* handler.promise._reject(handler.onFulfilled(this._value)); */ } if (this._state === REJECTED && typeof handler.onRejected === 'function') { this._enqueue( handler.promise._resolve.bind( handler.promise, handler.onRejected(this._value) ) ); /* handler.promise._reject(handler.onRejected(this._value)); */ } }; /** * 將fn加入事件循環 * @param {Function} fn */ Promise.prototype._enqueue = function(fn){ // process.nextTick(fn); // NodeJS setTimeout(fn.bind(this), 0); };
3.2 設置超時:
// 用於超時一個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來了解是哪一種狀況 } );
3.3 promise一旦決議就不會改變:
then()註冊的回調中,出現了JavaScript異常錯誤怎麼辦?並不會執行then中的異常回調函數!!當捕獲到錯誤,會執行下一個then的reject回調函數,或被catch捕獲。
var p = new Promise( function(resolve,reject){ resolve( 42 ); } ); p.then( function fulfilled(msg){ foo.bar(); console.log( msg ); // 永遠不會到達這裏 :( }, function rejected(err){ // 永遠也不會到達這裏 } ); // p 已經完成 爲值 42,因此以後查看 p 的決議時,並不能由於出錯就把 p 再變爲一個拒絕。
3.4 promise鏈式流:
能夠把多個 Promise 鏈接到一塊兒以表示一系列異步 步驟,每一個 Promise 的決 議就成了繼續下一個步驟的信號。
兩個 Promise 固有行爲特性:
Promise. resolve(..) 會直接返回接收到的真正 Promise,或展開接收到的 thenable 值,並在持續展 開 thenable 的同時遞歸地前進。若是咱們向封裝的 promise 引入異步,一 切都仍然會一樣工做
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 建立一個promise並返回
return new Promise( function(resolve,reject){
// 引入異步!
setTimeout( function(){
// 用值42填充
resolve( v * 2 );
}, 100 );
} );
} ).then( function(v){
// 在前一步中的100ms延遲以後運行
console.log( v ); // 42
} );
構造ajax請求:
// 假定工具ajax({url}, {callback})存在 // Promise-aware ajax function request(url) { return new Promise(function(resolve, reject) { // ajax(...) 回調應該是咱們這個promise的resolve函數 ajax(url, resolve); }); } request('http://some.url.1/').then(function(response1) { return request('http://some.url.2/?v=' + response1); }).then( function(response2){ console.log(response2); })
3.5 錯誤處理
錯誤處理最天然的形式就是同步的 try..catch 結構,遺憾的是,它只 能是同步的,沒法用於異步代碼模式。
爲了不丟失被忽略和拋棄的 Promise 錯誤,一些開發者表示,Promise 鏈的一個最佳實踐 就是最後總以一個 catch(..) 結束
3.6 Promise模式
3.6.1 Promise.all([...])
完成順序並不重要,可是必須都要完成,門才能打開並讓流程控制 繼續。
Promise.all([...])容許傳入一組Promise對象,調用返回的promise會收到一個完成消息,這是一個由全部傳入promise的完成消息組成的數組,與指定的順序一致。 它會在全部成員的promise都完成(fulfilled)後纔會完成(fulfilled)。當其中任意一個被拒絕(rejected),它就馬上進入被拒絕(rejected)狀態,並丟棄來自其餘全部promise的所有結果。
3.6.2 Promise.race([...])
競態:只響應「第一個跨過終點線的 Promise」,而拋棄其餘 Promise。
第一個返回的promise爲完成,它就會進入完成(fulfilled)狀態;第一個返回的promise爲失敗,它就會進入被拒絕(rejected)狀態。
應用:1. 超時模式
// 前面定義的timeoutPromise(..)返回一個promise, // 這個promise會在指定延時以後拒絕 // 爲foo()設定超時 Promise.race( [ foo(), // 啓動foo() timeoutPromise( 3000 ) // 給它3秒鐘 ] ) .then( function(){ // foo(..)按時完成! }, function(err){ // 要麼foo()被拒絕,要麼只是沒可以按時完成, // 所以要查看err瞭解具體緣由 } );
finally:前面例子中的 foo() 保留了一些要用的資源,可是出現了超時,致使這個 promise 被忽略,finally就是用來:超時後主動釋放這些保留資源,或者取消任何可能產生的反作用。
Promise 須要一個 finally(..) 回調註冊,這個回調在 Promise 決議後總 是會被調用,而且容許你執行任何須要的清理工做
var p = Promise.resolve( 42 );
p.then( something ) .finally( cleanup ) .then( another ) .finally( cleanup );
3.6.3 其餘變體
另外一方面,Promise也有原型方法finally,在決議後忽略是否成功,老是會執行。
3 promise API 綜述
3.7.1 new Promise(..) 構造器
構造器 Promise(..) 必須和 new 一塊兒使用,而且必須提供一個函數回調,函數接受兩個函數回調,用以支持 promise 的決議。這兩個函數稱爲 resolve(..) 和 reject(..):
3.7.2 Promise.resolve(..) 和 Promise.reject(..)
建立成功和拒絕的Promise的快捷方式
3.7.3 then(..) 和 catch(..)
Promise 決議以後,當即會調用 這兩個處理函數之一,但不會兩個都調用,並且老是異步調用
3.8 Promise侷限性
3.8.1 順序錯誤處理
若是構建了一個沒有錯誤處理函數的Promise鏈,鏈中任何地方的任何錯誤都會在鏈中一直傳播下去。
3.8.2 單一值
Promise只能有一個完成值,或一個拒絕理由。能夠進行必定的值封裝,可是須要每一步都進行封裝和解封。
3.8.3 單決議
Promise只能被決議一次。
3.8.4 慣性
基於Promise對原有的異步回調進行改造依賴於經驗,沒有通用的實現手段。
3.8.5 沒法取消的Promise
單獨的Promise不該該可取消,可是取消一個序列(集合在一塊兒的Promise構成的鏈)是合理的。
3.8.6 性能
相比於不受信任的裸回調,Promise性能上更慢一點。
第四章 async await 異步處理
4.1 async-await 和 Promise 的關係
async-await是promise和generator的語法糖。簡單來講:async-await 是創建在 promise機制之上的。
基本語法
async function basicDemo() { let result = await Math.random(); console.log(result); } basicDemo(); // 0.6484863241051226 //Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
async
async用來表示函數是異步的,定義的函數會返回一個promise對象,可使用then方法添加回調函數。
async function demo01() { return 123; } demo01().then(val => { console.log(val);// 123 }); 若 async 定義的函數有返回值,return 123;至關於Promise.resolve(123),沒有聲明式的 return則至關於執行了Promise.resolve();
await
await 能夠理解爲是 async wait 的簡寫。await 必須出如今 async 函數內部,不能單獨使用。
function notAsyncFunc() { await Math.random(); } notAsyncFunc();//Uncaught SyntaxError: Unexpected identifier
await 後面能夠跟任何的JS 表達式。雖說 await 能夠等不少類型的東西,可是它最主要的意圖是用來等待 Promise 對象的狀態被 resolved。若是await的是 promise對象,會形成異步函數中止執行,而且等待 promise 的解決,若是等的是正常的表達式則當即執行。
function sleep(second) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(' enough sleep~'); }, second); }) } function normalFunc() { console.log('normalFunc'); } async function awaitDemo() { await normalFunc(); console.log('something, ~~'); let result = await sleep(2000); console.log(result);// 兩秒以後會被打印出來 } awaitDemo(); // normalFunc // VM4036:13 something, ~~ // VM4036:15 enough sleep~
實例
舉例說明啊,你有三個請求須要發生,第三個請求是依賴於第二個請求的解構第二個請求依賴於第一個請求的結果。若用 ES5實現會有3層的回調,若用Promise 實現至少須要3個then。一個是代碼橫向發展,另外一個是縱向發展。今天指給出 async-await 的實現哈~
//咱們仍然使用 setTimeout 來模擬異步請求 function sleep(second, param) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(param); }, second); }) } async function test() { let result1 = await sleep(2000, 'req01'); let result2 = await sleep(1000, 'req02' + result1); let result3 = await sleep(500, 'req03' + result2); console.log(` ${result3} ${result2} ${result1} `); } test(); //req03req02req01 //req02req01 //req01
錯誤處理
上述的代碼好像給的都是resolve的狀況,那麼reject的時候咱們該如何處理呢?
最好把 await 命令放在 try...catch 代碼塊中。
function sleep(second) { return new Promise((resolve, reject) => { setTimeout(() => { reject('want to sleep~'); }, second); }) } async function errorDemo() { let result = await sleep(1000); console.log(result); } errorDemo();// VM706:11 Uncaught (in promise) want to sleep~ // 爲了處理Promise.reject 的狀況咱們應該將代碼塊用 try catch 包裹一下 async function errorDemoSuper() { try { let result = await sleep(1000); console.log(result); } catch (err) { console.log(err); } } errorDemoSuper();// want to sleep~ // 有了 try catch 以後咱們就可以拿到 Promise.reject 回來的數據了。 // 另外一種寫法 async function myFunction() { await somethingThatReturnsAPromise().catch(function (err){ console.log(err); }); }
當心你的並行處理!!!
我這裏爲啥加了三個感嘆號呢~,由於對於初學者來講一不當心就將 ajax 的併發請求發成了阻塞式同步的操做了,我就真真切切的在工做中寫了這樣的代碼。await 若等待的是 promise 就會中止下來。業務是這樣的,我有三個異步請求須要發送,相互沒有關聯,只是須要當請求都結束後將界面的 loading 清除掉便可。
剛學完 async await 開心啊,處處亂用~
function sleep(second) { return new Promise((resolve, reject) => { setTimeout(() => { resolve('request done! ' + Math.random()); }, second); }) } async function bugDemo() { await sleep(1000); await sleep(1000); await sleep(1000); console.log('clear the loading~'); } bugDemo();
loading 確實是等待請求都結束完才清除的。可是你認真的觀察下瀏覽器的 timeline 請求是一個結束後再發另外一個的(若觀察效果請發真實的 ajax 請求)
那麼,正常的處理是怎樣的呢?
async function correctDemo() { let p1 = sleep(1000); let p2 = sleep(1000); let p3 = sleep(1000); await Promise.all([p1, p2, p3]); console.log('clear the loading~'); } correctDemo();// clear the loading~
恩, 完美。看吧~ async-await並不能取代promise.
await in for 循環
最後一點了,await必須在async函數的上下文中的。
// 正常 for 循環 async function forDemo() { let arr = [1, 2, 3, 4, 5]; for (let i = 0; i < arr.length; i ++) { await arr[i]; } } forDemo();//正常輸出 // 由於想要炫技把 for循環寫成下面這樣 async function forBugDemo() { let arr = [1, 2, 3, 4, 5]; arr.forEach(item => { await item; }); } forBugDemo();// Uncaught SyntaxError: Unexpected identifier
第五章 程序性能
5.1 Web Worker
JavaScript目前並無支持多線程執行的功能。 目前部分瀏覽器支持經過Web Worker的形式來實現任務並行。一般Web Worker只加載JS文件,瀏覽器爲它啓動一個獨立的線程,讓這個文件在這個線程中爲獨立的程序運行。
var w1 = new Worker('http://some.url.com/webworker.js');
Worker之間以及它們和主程序之間,不會共享任何做用域或資源,而是經過一個基本的事件消息機制相互聯繫。好比
w1.addEventListener('message', function(evt) { // evt.data }); w1.postMessage('something to say');
專用Worker和建立它的主程序是一對一的關係,這個message要麼來自這個Worker,要麼來自主頁面。 Worker能夠實例化它的子Worker,稱爲subworker。 要想關閉一個worker,只要在主程序中對Worker調用terminate()方法(相似瀏覽器標籤頁來關閉頁面)。
5.1.1 Worker環境
在Worker內部是沒法訪問主程序的任何資源的(變量、DOM)。 它能夠用來執行網絡操做(ajax,websocket)以及設置定時器,並能夠訪問幾個重要的全局變量和功能的本地複本,好比navigator、location、JSON和applicationCache。 還能夠經過importScript(...)方法向Worker加載額外的JavaScript腳本:
// Worker內部,該操做是同步的 importScript('foo.js', 'bar.js');
Web Worker的應用:
5.1.2 數據傳遞
若是要傳遞一個對象,可使用結構化克隆算法,把這個對象複製到一邊。 或者可使用Transferable對象,發生對象全部權的轉移,數據自己不作移動。一旦全部權發生轉移,它原來的位置上就會變爲null或不可訪問,消除多線程編程做用域共享帶來的混亂。好比使用postMessage()方法發送一個Transferable對象:
// 假設foo是一個Uint8Array postMessage(foo.buffer, [foo.buffer]);
5.1.3 共享Worker
使用SharedWorker能夠建立一整個站點或app的全部頁面實例均可以共享的中心Worker。
var w1 = new SharedWorker('http://some.url.com/sharedWorker.js');
SharedWorker使用端口來做爲不一樣程序的惟一標識符,調用程序必須使用Worker的port對象用於通訊。
w1.port.addEventListener('message', handleMessages); w1.port.postMessage('something'); // 端口鏈接初始化 w1.port.start();
同時在SharedWorker內部,還須要處理額外的connect事件,爲這個特定的鏈接提供了端口對象。保持多個鏈接獨立的最簡單辦法就是使用port上的閉包,把這個鏈接上的事件偵聽和傳遞定義在connect的處理函數內部:
// 在SharedWorker內部 addEventListener('connect', function(evt) { // 這個鏈接分配的端口 var prot = evt.ports[0]; port.addEventListener('message', function(evt) { // ... port.postMessage('something'); // ... }); port.start(); });
5.2 SIMD(單指令多數據)
單指令多數據是一種數據並行(data parallelism)的方式,這不是把程序邏輯分紅並行的塊,而是並行處理數據的多個位。現代CPU經過數字「向量」(特定類型的數組),以及能夠在全部這些數字上並行操做的指令,來提供SIMD功能。這是利用低級指令級並行的底層運算。
5.3 asm.js
5.3.1 如何使用asm.js優化
var a = 42; var b = a; // asm優化,確保b是32位整型 var b = a | 0;
5.3.2 asm.js模塊
對JS性能影響最大的因素是內存分配、垃圾收集和做用域訪問。可使用asm.js模塊來解決這些問題。 對於一個asm.js模塊來講,須要明確地導入一個嚴格規範的命名空間——stdlib,以導入必要的符號。同時還須要聲明一個堆(heap)並將其傳入var heap = new ArrayBuffer(0x10000); //64k堆,asm.js就能夠在這個緩衝區存儲和獲取值,不須要付出任何內存分配和垃圾收集的代價。 asm的目標是針對特定的任務處理提供一種優化的方法,好比數學運算和遊戲中的圖像處理。
第六章 性能測試與調優
6.1 性能測試
使用Benchmark.js、mocha、karma等測試框架。
6.5 微性能
若是某個變量只在一個位置被引用,而別處沒有任何引用,那麼它的值就會被在線化,即直接用值替換變量。 當遞歸能夠進行展開,引擎就會對其進行採用循環的方式實現。 非關鍵路徑上的優化沒有必要,而應該注重可讀性。關鍵路徑要注重性能優化。
6.6 尾調用優化
使用TCO能夠將尾遞歸轉爲普通的循環
function tco(f) { var value, active = false, accumulated = []; // 在f內部進行遞歸調用的時候,其實調用的是這個返回的accumulator // 這裏須要注意的是,要將f中的遞歸調用函數名使用accumulator被賦予的變量名 return function accumulator() { accumulated.push(arguments); if(!active) { active = true; /** * 因爲每次f.apply都會致使accumulated被push進一個新的arguments, * 因此這個while一直要到所有執行結束纔會跳出, * 但這種結構只會保持最多兩層的調用棧 */ while(accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } } } var sum = tco(function(x, y) { if (y > 0) { // 再次調用的實際上是閉包返回的accumulator return sum(x + 1, y - 1) } else { return x } }); st=>start: 開始 ed=>end: 結束 cond1=>condition: 檢查accumulator.length (若是是第一次進入, 或者上一次的遞歸調用中作了push, 這個循環就會繼續) cond2=>condition: 須要進行遞歸 opm0=>subroutine: 初始化閉包內的變量, 調用閉包返回的函數, 執行accumulated.push(arguments), 第一次的active判斷必然經過 op1-1=>operation: while循環 op1-2=>operation: 取出上次的運行結果, 傳參給f.apply op2-1=>operation: 執行真正的操做 op2-2=>operation: 進入第二層遞歸調用 op2-3=>operation: 執行 accumulated.push(arguments), 記錄這一次遞歸調用的結果 op2-4=>operation: 第二層中!active爲false,返回 op2-5=>operation: 回到第一層的while循環 op3-1=>operation: 遞歸調用條件已經不符合,能夠返回結果,再也不作push op4-1=>operation: 已經執行完畢,value獲得最後的結果 st->opm0->op1-1->cond1 cond1(yes,right)->cond2 cond1(no)->op4-1->ed cond2(yes,right)->op1-2->op2-1->op2-2->op2-3->op2-4->op2-5(left)->op1-1 cond2(no,right)->op3-1(right)->op1-1