javascript的執行分爲三個部分:運行時,事件循環,js引擎。運行時提供了諸如注入全局API(dom, setTimeout之類)這樣的功能。js引擎負責代碼編譯執行,包括內存管理。以前寫了一篇關於javascript內存管理的文章,具體可見 javascript內存管理以及三種常見的內存泄漏
javascript執行示意圖以下所示:
javascript
做爲前端工程師,咱們都知道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請求以同步的方式進行調用,在接口返回數據以前javascript線程都會處於被佔用的狀態,會致使當前頁面在success函數執行完成前不能響應用戶的任何操做。若是這個過程持續時間過長,就會直接形成頁面處於假死狀態
java
讓咱們來看以下這段代碼:jquery
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
複製代碼
執行這段代碼,咱們能夠看下調用棧和任務隊列中都發生了什麼git
隨着javascript語言的發展,針對異步流程控制也有了愈來愈多的解決方案,依照歷史發展的車轍,主要有四種:程序員
// 以jquery中的請求爲例
$.ajax({
url: 'xx1',
success: function () {
console.log('1');
$.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
}
})
複製代碼
在上述代碼中咱們經過在xx1請求完成的回調函數中發起xx2的請求這種回調嵌套的方式來實現兩個異步任務的執行順序控制。這種回調函數的方式在es6出現以前是應用最爲普遍的實現方案,可是其缺點也很明顯,若是咱們有多個異步任務須要依次執行,那麼就會致使很是深的嵌套層次,形成回調地獄,下降代碼可讀性。es6
var ajax1 = function () {
return new Promise(function (resolve, reject) {
$.ajax({
url: 'xx1',
success: function () {
console.log('1')
resolve()
}
})
})
}
ajax1().then(() => {
$.ajax({
url: 'xx1',
success: function () {
console.log('2')
}
})
})
複製代碼
promise經過then方法的鏈式調用將須要按順序執行的異步任務串起來,在代碼可讀性方面有很大提高。
究其實現原理,Promise是一個構造函數,它有三個狀態,分別是pending, fullfilled,rejected,構造函數接受一個回調做爲參數,在該回調函數中執行異步任務,而後經過resolve或者reject將promise的狀態由pending置爲fullfilled或者rejected。
Promise的原型對象上定義了then方法,該方法的做用是將傳遞給它的函數壓入到resolve或者reject狀態對應的任務數組中,當promise的狀態發生改變時依次執行與狀態相對應的數組中的回調函數,此外,promise在其原型上還提供了catch方法來處理執行過程當中遇到的異常。
Promise函數自己也有兩個屬性race,all。race,all都接受一個promise實例數組做爲參數,二者的區別在於前者只要數組中的某個promise任務率先執行完成就會直接調用回調數組中的函數,後者則須要等待所有promise任務執行完成。
一個mini的promise代碼實現示例以下所示:github
function Promise (fn) {
this.status = 'pending';
this.resolveCallbacks = [];
this.rejectCallbacks = [];
let _this = this
function resolve (data) {
_this.status = 'fullfilled'
_this.resolveCallbacks.forEach((item) => {
if (typeof item === 'function') {
item.call(this, data)
}
})
}
function reject (error) {
_this.status = 'rejected'
_this.rejectCallbacks.forEach((item) => {
if (typeof item === 'function') {
item.call(this, error)
}
})
}
fn.call(this, resolve, reject)
}
Promise.prototype.then = function (resolveCb, rejectCb) {
this.resolveCallbacks.push(resolveCb)
this.rejectCallbacks.push(rejectCb)
}
Promise.prototype.catch = function (rejectCb) {
this.rejectCallbacks.push(rejectCb)
}
Promise.race = function (promiseArrays) {
let cbs = [], theIndex
if (promiseArrays.some((item, index) => {
return theIndex = index && item.status === 'fullfilled'
})){
cbs.forEach((item) => {
item.call(this, promiseArrays[theIndex])
})
}
return {
then (fn) {
cbs.push(fn)
return this
}
}
}
Promise.all = function (promiseArrays) {
let cbs = []
if (promiseArrays.every((item) => {
return item.status === 'fullfilled'
})) {
cbs.forEach((item) => {
item.call(this)
})
}
return {
then (fn) {
cbs.push(fn)
return this
}
}
}
複製代碼
以上是我對promise的一個很是簡短的實現,主要是爲了說明promise的封裝運行原理,它對異步任務的管理是如何實現的。web
function* ajaxManage () {
yield $.ajax({
url: 'xx1',
success: function () {
console.log('1')
}
})
yield $.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
return 'ending'
}
var manage = ajaxManage()
manage.next()
manage.next()
manage.next() // return {value: 'ending', done: true}
複製代碼
在上述示例中咱們定義了ajaxManage這個generator函數,可是當咱們調用該函數時他並無真正的執行其內部邏輯,而是會返回一個迭代器對象,generator函數的執行與普通函數不一樣,只有調用迭代器對象的next方法時纔會去真正執行咱們在函數體內編寫的業務邏輯,且next方法的調用只會執行單個經過yield或return關鍵字所定義的狀態,該方法的返回值是一個含有value以及done這兩個屬性的對象,value屬性值爲當前狀態值,done屬性值爲false表示當前不是最終狀態。
咱們能夠經過將異步任務定義爲多個狀態的方式,用generator函數的迭代器機制去管理這些異步任務的執行。這種方式雖然也是一種異步流程控制的解決方案,可是其缺陷在於咱們須要手動管理generator函數的迭代器執行,若是咱們須要控制的異步任務數量衆多,那麼咱們就須要屢次調用next方法,這顯然也是一種不太好的開發體驗。
爲了解決這個問題,也有不少開發者寫過一些generator函數的自動執行器,其中比較廣爲人知的就是著名程序員TJ Holowaychuk開發的co 模塊,有興趣的同窗能夠多瞭解下。ajax
async function ajaxManage () {
await $.ajax({
url: 'xx1',
success: function () {
console.log('1')
}
})
await $.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
}
ajaxManage()
複製代碼
經過代碼示例能夠看出,async/await在寫法上與generator函數是極爲相近的,僅僅只是將*號替換爲async,將yield替換爲await,可是async/await相比generator,它自帶執行器,像普通函數那樣調用便可。另外一方面它更加語義化,可讀性更高,它也已經獲得大多數主流瀏覽器的支持。
// `rp` is a request-promise function.
rp(‘https://api.example.com/endpoint1').then(function(data) {
// …
});
// 使用await模式
var response = await rp(‘https://api.example.com/endpoint1');
// 錯誤處理
// promise的寫法
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/await處理
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e);
}
}
// 異步條件判斷
// promise處理
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/await改造
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;
}
}
複製代碼
// promise
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/await
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)
});
複製代碼
事件循環是宿主環境處理javascript單線程帶來的執行阻塞問題的解決方案,所謂異步,就是當事件發生時將指定的回調加入到任務隊列中,等待調用棧空閒時由事件循環將其取出壓入到調用棧中執行,從而達到不阻塞主線程的目的。由於異步回調的執行時機是不可預測的,因此咱們須要一種解決方案能夠幫助咱們實現異步執行流程控制,本篇文章也針對這一問題分析了當前處理異步流程控制的幾種方案的優缺點和實現原理。但願能對你們有所幫助。