歡迎閱讀該系列文章的第4部分,該文章專門探討JavaScript及其構建組件。 在識別和描述核心元素的過程當中,咱們還共享一些在構建 SessionStack 時使用的經驗法則,SessionStack 是一個 JavaScript 應用程序,必須強大且高性能確保競爭力。javascript
您是否錯過了前三章? 您能夠在這裏找到它們:java
此次,咱們來擴展咱們第一章,經過回顧單線程環境中編程的缺點以及如何克服這些缺點來構建出色的 JavaScript UIs 。 照舊,在本文的最後,咱們將分享5條技巧,介紹如何使用 async / await 編寫更簡潔的代碼。git
在第一章中,咱們思考了如下問題:當在 Call Stack 中進行 function 的 調用須要大量時間來處理時,會發生什麼狀況。github
例如,假設瀏覽器中正在運行一種複雜的圖像轉換算法。web
當 Call Stack 中有 function 執行 ,此時瀏覽器沒法執行其餘任何操 做- 已被阻塞。這意味着瀏覽器沒法渲染,沒法運行任何其餘代碼,卡住了。 這是,你的 App UI 再也不高效,頁面交互再也不流暢。ajax
你的 app 卡住了 。算法
在某些狀況下,這可能不是一個致命問題。 可是在某些狀況下這會是一個更大的問題。 一旦瀏覽器開始處理 Call Stack 中的太多任務,它可能會長時間中止響應。 那時,許多瀏覽器會經過引起錯誤來詢問是否應該終止該頁面:shell
這很醜陋,而且徹底破壞了用戶體驗:編程
您可能會在單個.js文件中編寫 JavaScript 應用程序,可是您的程序幾乎能夠確定是由幾個塊組成的,其中只有一個要當即執行,其他的要稍後執行。 最多見的塊單位是 function。api
大多數剛接觸 JavaScript 的開發人員彷佛的問題在於什麼是 "之後並不必定要當即發生" 。 換句話說,根據定義,如今沒法完成的任務將異步完成,這意味着您將不會像在潛意識中指望或但願的那樣具備上述阻止行爲。
讓咱們看一下如下示例:
// ajax(..) is some arbitrary Ajax function given by a library
var response = ajax('https://example.com/api');
console.log(response);
// `response` won't have the response
複製代碼
您可能知道標準的 Ajax請
求 不會同步完成,這意味着在代碼執行時,ajax(...)
函數尚未任何值能夠返回以分配給響應變量。 一種 等待 異步函數返回其結果的簡單方法是使用一個稱爲回調的函數:
ajax('https://example.com/api', function(response) {
console.log(response); // `response` is now available
});
複製代碼
note:您實際上能夠發出同步
Ajax
請求。 永遠不要那樣作。 若是您發出同步Ajax
請求,則您的JavaScript
應用的用戶界面將被阻止。
用戶將沒法單擊,輸入數據,導航或滾動。 這將防止任何用戶交互。 這是一個可怕的操做。
看起來是這樣,可是請不要這樣作 – 不要破壞網頁體驗:
// This is assuming that you're using jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// This is your callback.
},
async: false // And this is a terrible idea
});
複製代碼
咱們僅以 Ajax 請求爲例。 您可讓任何代碼塊異步執行。
可使用 setTimeout(callback,msiseconds)
函數完成此操做。 setTimeout
函數的做用是設置一個事件(超時)以在之後發生。 讓咱們來看看:
function first() {
console.log('first');
}
function second() {
console.log('second');
}
function third() {
console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();
複製代碼
控制檯會輸出以下:
first
third
second
複製代碼
儘管容許異步JavaScript代碼(例如咱們剛剛討論的setTimeout),但咱們仍是從一個奇怪的說法開始,直到ES6爲止,JavaScript 自己實際上從未內置任何直接的異步概念。
除了在任何給定的時刻執行程序的單個塊以外,JavaScript引擎從未作過其餘任何事情。
要詳細瞭解JavaScript引擎的工做方式(特別是 Google 的 V8 ),請查看咱們以前關於該主題的文章之一。
那麼,誰告訴JS引擎去執行你的程序塊呢? 實際上,JS引擎不是孤立運行的,而是在託管環境中運行的,對於大多數開發人員來講,託管環境是典型的Web瀏覽器或Node.js。 實際上,現在,JavaScript已嵌入從機器人到燈泡的各類設備中。 每一個單個設備表明JS引擎的不一樣類型的託管環境。
全部環境中的共同點是一種內置機制,稱爲 event loop , 該機制每次調用 JS Engine 時都會處理程序中多個塊的執行。
這意味着對於任意 JS 代碼 來講,JS Engine 只是一個按需執行的環境。 安排 event(JS代碼執行)的執行時間是處決於周圍的環境。
所以,例如,當您的JavaScript程序發出 Ajax 請求以從服務器獲取某些數據時,您在函數中設置了「響應」代碼(「回調」),而後JS Engin e告知託管環境: 「嘿,我如今暫時暫停執行,可是隻要您完成該網絡請求,而且有一些數據,請回叫此函數。」
而後將瀏覽器設置爲偵聽來自網絡的響應,並在有須要返回的內容時,將經過將其插入 event loop 來安排要執行的回調函數。
如圖:
您能夠在上一篇文章中瞭解有關內存堆 和 call stack 的更多信息。
這些 Web API 是什麼? 本質上,它們是您沒法訪問的線程,您能夠對其進行調用。 它們是併發性所在的瀏覽器組件。若是您是Node.js開發人員,則這些都是 C ++ API。
那麼,什麼是 event loop 呢?
事件循環有一個簡單的工做:監視 call stack 和回調隊列。 若是 call stack 爲空,它將從隊列中獲取第一個事件,並將其推入 call stack,後者將有效地運行它。
在 Event loop 中這樣一個迭代的動做叫作一個 tick ,每一個 event 只是一個回調函數。
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
複製代碼
讓咱們執行如下上面的代碼,看看發生了什麼:
console.log('Hi')
被添加到了 call stack 中console.log('Hi')
被執行
console.log('Hi')
從 call stack 移除6.setTimeout(function cb1() { ... })
被執行,瀏覽器建立了一個定時器做爲 web apis 的一部分,它會處理倒計時。
setTimeout(function cb1() { ... })
執行後,從 call stack 中移除。
console.log('Bye')
被添加到 Call Stack。
console.log('Bye')
被執行。
console.log('Bye')
從 Call Stack 中移除。回調 callback
push 到回調的隊列中。cb1
從回調隊列中推入到 call stack。cb1
被執行,而且將 console.log('cb1')
推入 call stack。console.log('cb1')
被執行。console.log('cb1')
從調用堆棧移除。
cb1
從調用堆棧移除。
咱們來快速回顧一下:
有趣的是,ES6指定了事件循環的工做方式,這意味着從技術上講,它在JS引擎職責範圍以內,再也不僅充當託管環境角色。
進行此更改的一個主要緣由是在ES6中引入了Promises,由於後者須要訪問對事件循環隊列上的調度操做進行直接,細粒度的控制(咱們將在後面詳細討論)。
setTimeout(...)
怎麼工做的請務必注意,setTimeout(...)
不會自動將回調置於事件循環隊列中。 它設置一個計時器。 當計時器到期時,環境會將您的回調放入事件循環中,以便未來的 tick 將其拾取並執行,請看下面的代碼:
setTimeout(myCallback, 1000);
複製代碼
這並不意味着 myCallback
將在1000毫秒時恰好執行,而是在1000毫秒內將 myCallback
添加到隊列中。 可是,隊列中可能還包含其餘較早添加的事件 - 您的回調將不得不等待。
有不少關於 JavaScript async
入門的文章和教程,建議作 setTimeout(callback,0)
。 好了,如今知道了事件循環的功能以及 setTimeout
的工做方式:以0
做爲第二個參數調用 setTimeout
只會推遲迴調,直到清除 call stack。
看一下下面的代碼:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
複製代碼
儘管等待時間設置爲 0 ms,但在瀏覽器控制檯中的結果將是如下內容:
Hi
Bye
callback
複製代碼
Jobs
是什麼?ES6 中引入了一個稱爲 "Jobs Queue" 的新概念。 它是事件循環隊列頂部的一層。 在處理Promises的異步行爲時,您最有可能碰到它(咱們也將討論它們)。
咱們暫時在用 Promises 討論異步行爲時,只討論概念,稍後,你會明白這些動做是怎麼被執行和處理的。
想象一下:"Jobs Queue" 是一個附加到 Event Loop 中每一個刻度末尾的隊列。 在事件循環的一個 tick 中可能發生的某些異步操做不會致使將一個新事件添加到事件循環隊列中,而是將一個 job 添加到當前 tick 的Job隊列的末尾。
這意味着您能夠添加其餘功能以便之後執行,而且能夠放心,它會在以後當即執行。
job 還能夠致使將更多 jobs 添加到同一隊列的末尾。 從理論上講,job "循環"(一個job 一直在添加其餘的 jbs )可能無限期旋轉,從而使程序缺乏進入下一事件循環刻度的必要資源。 從概念上講,這相似於在代碼中表達長時間運行或無限循環(例如while(true)..)。
Jobs 有點像 setTimeout(callback,0)
的 "hack",可是實現的方式是,它們引入了更加明確和有保證的排序:稍晚,可是會盡快。
如您所知,到目前爲止,回調是在JavaScript程序中表達和管理異步的最經常使用方法。 實際上,回調是JavaScript語言中最基本的異步模式。 除了回調以外,無數的JS程序,甚至是很是複雜的程序,都在異步基礎之上編寫。
除了回調沒有帶來任何缺點以外。 許多開發人員正在嘗試尋找更好的異步模式。 可是,若是你不瞭解本質,既不能更好的明白和使用更抽象的模式。
在下一章中,咱們將深刻探討其中的兩個抽象概念,以說明爲何有必要甚至建議使用更復雜的異步模式(將在後續文章中進行討論)。
請看下面的代碼:
listen('click', function (e){
setTimeout(function(){
ajax('https://api.example.com/endpoint', function (text){
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
});
}, 500);
});
複製代碼
咱們將三個函數嵌套在一塊兒,每一個函數表明一個異步序列中的一個步驟。
這種代碼一般稱爲「回調地獄」。 可是「回調地獄」實際上幾乎與嵌套/縮進無關。 這是一個更深層次的問題。
首先,咱們在等待 "click" 事件,而後在等待計時器啓動,而後在等待Ajax響應返回,Ajax 的返回可能會重複出現。
乍一看,此代碼彷佛天然地將其異步映射到順序步驟,例如:
listen('click', function (e) {
// ..
});
複製代碼
而後:
setTimeout(function(){
// ..
}, 500);
複製代碼
再而後:
ajax('https://api.example.com/endpoint', function (text){
// ..
});
複製代碼
最後:
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
複製代碼
所以,這種順序的異步代碼表達方式彷佛更加天然,不是嗎? 必定有這種方法吧?
咱們看一下下面的代碼:
var x = 1;
var y = 2;
console.log(x + y);
複製代碼
一切都很是簡單:將x和y的值相加並將其打印到控制檯。 可是,若是x或y的值丟失而且仍待肯定怎麼辦?
假設咱們須要先從服務器檢索x和y的值,而後才能在表達式中使用它們。
假設咱們有一個函數loadX和loadY,它們分別從服務器加載x和y的值。 而後,假設咱們有一個函數求和,將x和y的值加載後將它們求和。
看起來可能像這樣(很是醜,不是嗎?):
function sum(getX, getY, callback) {
var x, y;
getX(function(result) {
x = result;
if (y !== undefined) {
callback(x + y);
}
});
getY(function(result) {
y = result;
if (x !== undefined) {
callback(x + y);
}
});
}
// A sync or async function that retrieves the value of `x`
function fetchX() {
// ..
}
// A sync or async function that retrieves the value of `y`
function fetchY() {
// ..
}
sum(fetchX, fetchY, function(result) {
console.log(result);
});
複製代碼
在代碼中有一個很是重要的內容,咱們將x和y視爲不肯定的值,而且咱們表示了一個操做 sum(…)
(從外部)不關心x或y或二者是否可用。
固然,這種基於回調的方法還有不少不足之處。 這只是邁出第一步,考慮不肯定值得好處就是你之後沒必要關心它們是否真正可用。
讓咱們簡要介紹一下如何使用 Promises 表達 x + y
:
function sum(xPromise, yPromise) {
// `Promise.all([ .. ])` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
return Promise.all([xPromise, yPromise])
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
.then(function(values){
// `values` is an array of the messages from the
// previously resolved promises
return values[0] + values[1];
} );
}
// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
sum(fetchX(), fetchY())
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...)` to wait for the
// resolution of that returned promise.
.then(function(sum){
console.log(sum);
});
複製代碼
此段代碼包含了兩個 Promise。
直接調用 fetchX()
和 fetchY()
,並將它們返回的 Promise
傳遞給 sum(...)
。 這些 Promise 表示返回多是立刻或者是之後,可是每一個承諾都將其行爲規範化爲相同。 咱們以與時間無關的方式推理 x 和 y 值。 它們是之後的值。
第二塊代碼是使用 .then 處理 sum(...)
返回的 promise(經過Promise.all([ ... ]) 中的 return 返回),當 sum(...)
的操做完成,咱們的值已經被計算完畢,並打印出來,咱們隱藏了等待 x 和 y 的返回的細節
注意:
在
sum(…)
內,Promise.all([…])
調用建立一個promise
(正在等待promiseX
和promiseY
解析)。 連接到.then(...)
的調用會建立另外一個promise
,即返回values [0] + values [1]
執行的結果。所以,咱們將
sum(...).then(...)
是執行的values [0] + values [1]
所產生的promise
使用Promises時,then(...) 調用實際上能夠具備兩個 回調函數,第一個用於實現(如前所示),第二個用於拒絕:
sum(fetchX(), fetchY())
.then(
// fullfillment handler
function(sum) {
console.log( sum );
},
// rejection handler
function(err) {
console.error( err ); // bummer!
}
);
複製代碼
若是在獲取 x 或 y 時出現問題,或者在加法過程當中因某種緣由失敗,則 sum(...)
返回的 promise
將被 reject
,傳遞給 then(...)
的第二個回調錯誤處理的回調。
由於 Promise
從外部封裝了時間相關的狀態(等待 value 的 resolve
或者 reject
),因此 Promise
自己是時間獨立的,所以 Promise
能夠以可預測的方式組合。
並且,一旦 Promise 被 resolve ,它就會永遠保持這種狀態 - 它會成爲一個不變的值
對 promise 的鏈式調用真的頗有用:
function delay(time) {
return new Promise(function(resolve, reject){
setTimeout(resolve, time);
});
}
delay(1000)
.then(function(){
console.log("after 1000ms");
return delay(2000);
})
.then(function(){
console.log("after another 2000ms");
})
.then(function(){
console.log("step 4 (next Job)");
return delay(5000);
})
// ...
複製代碼
.then 的執行必需要等到前一個 promise 的狀態變成 resolve/onFulfilled ,因此 after another 2000ms ,在等待了 2000ms 以後被執行。
關於 Promise 有一個很重要的細節是,須要瞭解清楚這個值是否須要被 promise,簡單地說就是,這個獲取這個值得行爲是否像一個 promise。
咱們知道 Promise 的建立是由 new Promise(…) 所得,那麼,你可能會認爲 p instanceof Promise
可以檢查一個 promise 實例,可是並非這樣的 。
主要緣由是由於你能夠從另一個環境(好比 iframe 擁有獨立的 Promise)接受到 Promise,該窗口具備本身的Promise,與當前窗口或框架中的Promise不一樣,而且該檢查將沒法識別Promise實例。
此外,庫或框架可能選擇使用其本身的 Promises,而不使用 ES6 Promise 實現。 實際上,您極可能將 Promises 與徹底沒有 Promise 的舊版瀏覽器中的庫一塊兒使用。
若是在建立 Promise 或在執行 promise 的任什麼時候候發生 JavaScript 異常錯誤(例如 TypeError 或 ReferenceError ),則會捕獲該異常,這將迫使 Promise 被 rejected。
舉例說明:
var p = new Promise(function(resolve, reject){
foo.bar(); // `foo` is not defined, so error!
resolve(374); // never gets here :(
});
p.then(
function fulfilled(){
// never gets here :(
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.
}
);
複製代碼
若是某個 promise 已經被 resolved ,但在 resolved 的回調中拋出了異常, 在當前 .then()
的二個回調中不會捕獲到錯誤。
var p = new Promise( function(resolve,reject){
resolve(374);
});
p.then(function fulfilled(message){
foo.bar();
console.log(message); // never reached
},
function rejected(err){
// never reached
}
);
複製代碼
從上面看出好像異常被吞噬了,可是實際上這是一個更深層級的錯誤,promise 拋出了另一個 rejected 的 promise ,須要在第二個 .then()
捕獲。
其實還有更多更好的方法
一個常見的作法是 Promises 末尾添加 done(...)
,這實際上將 Promise 鏈標記爲「完成」。done(...)
不會建立並返回 Promise ,所以將回調傳遞給 done(..)
,不會再拋出多餘的 promise 了 ( promise:undefined)。
done(...)
會全局處理未捕獲到的錯誤:
var p = Promise.resolve(374);
p.then(function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log(msg.toLowerCase());
})
.done(null, function() {
// If an exception is caused here, it will be thrown globally
});
複製代碼
JavaScript ES8 引進了 Async/await 讓 promise 變得更加容易,咱們將簡要介紹一下 Async/await 提供的可能性以及如何利用它們編寫異步代碼。
讓咱們來看一下 Async/await 怎麼工做的。
您可使用異步函數聲明來定義 async function。 此類函數返回 AsyncFunction 對象。 AsyncFunction
對象表示執行包含在該函數中的代碼的異步函數。
調用 async function ,它將返回Promise。 當異步函數返回的值不是 Promise 時,將自動建立 Promise 並將其與函數返回的值一塊兒解析。 當異步函數拋出異常時,Promise 將被拋出的值 rejected。
異步函數能夠包含 await 表達式,該表達式會暫停函數的執行並等待所傳遞的 Promise 的結果,而後恢復異步函數的執行並返回解析後的值。
您能夠將 JavaScript 中的 Promise 視爲 Java 的 Future 或 C#的 Task。
Async/await 的目的是簡化使用 promise 的行爲。
讓咱們看一下下面的例子:
// Just a standard JavaScript function
function getNumber1() {
return Promise.resolve('374');
}
// This function does the same as getNumber1
async function getNumber2() {
return 374;
}
複製代碼
其實兩個函數的做用是等價的,對於 promise 被 rejected 的狀況是這樣:
function f1() {
return Promise.reject('Some error');
}
async function f2() {
throw 'Some error';
}
複製代碼
await 關鍵字只能在 async function 中使用,並容許您在 Promise 上同步等待。 若是咱們在異步函數以外使用 Promise ,則仍然必須使用而後回調:
async function loadData() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
loadData().then(() => console.log('Done'));
複製代碼
您也可使用「 async 函數表達式」定義 async function。 async 函數表達式與async function 語句很是類似,而且語法幾乎相同。 他們之間主要區別是函數名稱,能夠在異步函數表達式中省略該名稱以建立匿名函數。 async 函數表達式可用做IIFE(當即調用函數表達式),該函數一經定義便當即運行。
就像這樣
var loadData = async function() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
複製代碼
更重要的是 Async/await 已經被全部的主流瀏覽器支持:
歸根結底,重要的是不要盲目地選擇「最新」方法來編寫異步代碼。 必須瞭解異步 JavaScript 的內部結構,瞭解爲何它如此重要並深刻理解所選方法的內部結構。 每種方法在編程中都有其優勢和缺點。
// `rp` is a request-promise function.
rp(‘https://api.example.com/endpoint1').then(function(data) {
// …
});
複製代碼
與:
// `rp` is a request-promise function.
var response = await rp(‘https://api.example.com/endpoint1');
複製代碼
function loadData() {
try { // Catches synchronous errors.
getJSON().then(function(response) {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch(function(e) { // Catches asynchronous errors
console.log(e);
});
} catch(e) {
console.log(e);
}
}
複製代碼
與:
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e);
}
}
複製代碼
function loadData() {
return getJSON()
.then(function(response) {
if (response.needsAnotherRequest) {
return makeAnotherRequest(response)
.then(function(anotherResponse) {
console.log(anotherResponse)
return anotherResponse
})
} else {
console.log(response)
return response
}
})
}
複製代碼
與:
async function loadData() {
var response = await getJSON();
if (response.needsAnotherRequest) {
var anotherResponse = await makeAnotherRequest(response);
console.log(anotherResponse)
return anotherResponse
} else {
console.log(response);
return response;
}
}
複製代碼
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then(() => {
throw new Error("boom");
})
}
loadData()
.catch(function(e) {
console.log(err);
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});
複製代碼
與:
async function loadData() {
await callAPromise1()
await callAPromise2()
await callAPromise3()
await callAPromise4()
await callAPromise5()
throw new Error("boom");
}
loadData()
.catch(function(e) {
console.log(err);
// output
// Error: boom at loadData (index.js:7:9)
});
複製代碼
調試:若是您使用了 Promise,則知道調試它們是一場噩夢。 例如,若是您在 .then 塊內設置斷點並使用調試快捷方式(如 「 stop-over」 ),則調試器將不會移至如下.then,由於它僅 「step」 經過同步代碼。 使用 async / await,您能夠徹底像正常的同步函數同樣逐步執行 await 調用。
編寫異步JavaScript代碼不只對應用程序自己很重要,對庫也很重要。
例如,SessionStack 庫記錄您的 Web 應用程序/網站中的全部內容:全部 DOM 更改,用戶交互,JavaScript 異常,堆棧跟蹤,失敗的網絡請求和調試消息。
這一切都必須在您的生產環境中進行,而不會影響任何用戶體驗。 咱們須要大量優化代碼,並使其儘量地異步,以便咱們能夠增長 event loop 正在處理的事件的數量。
不只是 librar! 當您在 SessionStack 中重播用戶會話時,咱們必須在問題發生時渲染用戶瀏覽器中發生的全部事情,而且咱們必須重構整個狀態,以容許您在會話時間軸中來回跳轉。 爲了實現這一點,咱們大量利用了 JavaScript async。
這裏有個免費的計劃讓你 開始 sessionStatck
資料: