打工人!打工魂!前端才是人上人!此系列總結於大前端進擊之路過程中的學習,如果文章中有不對的地方,希望大家能進行批評改正,互相進步。
經典面試題我們先來看一道經典的面試題,讓我們的小腦袋瓜子思考起來~如果你對這道題有清晰的思路並且了解背後的原因,那麼請直接點讚評論加關注!!!!!
//請寫出輸出內容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');答案!你答對了嗎?沒對的不要跑,睜大你的小眼睛仔細看以下的內容
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
JS採用單線程模式工作的原因為了回答這個問題我們首先需要知道JS的執行環境是單線程的,是因為JS語言最早是運行在瀏覽器端的語言,目的是為了實現頁面上的動態交互。實現動態交互的核心就是DOM操作,因此決定了JS必須是單線程模式工作。我們來假設一下如果JS是多線程一起工作的,其中一個線程修改了一個DOM元素,另外的一個線程同時又要刪除這個DOM元素,那麼此時瀏覽器就懵逼了,無法明確以哪個工作線程為準。所以為了避免線程同步的問題,JS就被設計成了單線程的工作模式。
注意,我們這裡說的單線程是JS的執行環境是單線程,瀏覽器中是多線程的。
單線程的優勢和弊端採用單線程的工作模式可以節省內存,節約上下文切換時間,沒有鎖的問題。但弊端也很明顯,如果中間有一個任務需要花費大量的時間,那麼後面的任務就需要等待這個任務完成後才能執行,就會出現假死的情況,對用戶很不友好。為了解決這個問題JS給出了兩種執行模式:同步模式(Synchronous)和異步模式(Asynchronous)。
同步模式和異步模式同步模式同步模式其實很好理解,舉個慄子:
我們如果按照同步模式煮麵的話,首先先將鍋裡裝上水,打開火開始燒水,等待水燒開,再將面、雞蛋、火腿腸等材料拿出,材料準備好後放入鍋中進行煮,煮好後開始乾飯。
在這裡其實我們已經能夠看出來問題,我們必須等到水燒開後才去準備要煮的材料。回到概念裡就是在同步模式下我們的代碼是依次執行,後一個任務必須等待前一個任務結束才能開始執行。程序執行的順序和代碼編寫的順序是完全一致的。在單線程模式下,大多數任務都是以同步模式執行。
異步模式上個例子中我們在等待水燒開的過程中什麼都沒幹,很浪費時間,我們可以在燒水的過程中將食材都準備好,等到水燒開後直接放入。
我們在燒水的過程中去幹了別的事情,就屬於異步模式,異步模式中不會等待異步任務的結束才開始執行下一個同步的任務,都是開啟過後就立即執行下一個任務。
異步模式對於JS很重要,沒有異步模式的話我們就無法同時處理大量的耗時任務,就會給用戶帶來卡頓和假死的體驗。對於我們開發者來說,會給我們打開代碼執行的順序混亂的問題。
EventLoop事件循環和消息隊列EventLoop是一種循環機制,主線程從消息隊列中讀取任務並按照順序執行,這個過程是循環不間斷的。消息隊列是存放異步任務的地方,當我們的同步任務都執行完畢後,EventLoop會從消息隊列中依次取出異步任務放到調用棧中進行執行。宏任務和微任務宏任務可以理解為每次執行棧執行的代碼就是一個宏任務
瀏覽器為了讓JS內部宏任務與DOM操作能夠有序的執行,會在一個宏任務執行結束後,下一個宏任務執行開始前,對頁面進行重新渲染。
宏任務包括:script整體代碼、setTimeout、setInterval、I/O、UI交互事件、MessageChannel等。
微任務可以理解為每個宏任務執行結束後立即執行的任務,發生在宏任務後,渲染之前,執行微任務。
所以微任務的響應速度相比宏任務會更快,因為無需等待UI渲染
微任務包括:Promise.then、MutaionObserver、process.nextTick(Node.js環境下)等。
異步編程方案的本質—回調函數回調函數:由調用者定製,交給執行者執行的函數。
我們通過 callback 回調函數、事件發布/訂閱、Promise 等來組織代碼,本質都是通過回調函數來實現異步代碼的存放與執行。
// callback就是回調函數
// 就是把函數作為參數傳遞,缺點是不利於閱讀,執行順序混亂。
function foo(callback) {
setTimeout(function(){
callback()
}, 3000)
}
foo(function() {
console.log('這是回調函數')
console.log('調用者定義這個函數,執行者執行這個函數')
console.log('其實就是調用者告訴執行者異步任務結束後應該做什麼')
})
更優異步編程統一方案——PromisePromise概述Promise概念MDN傳送門
關於Promise概念性內容就不在贅述了,可直接點擊傳送門前往MDN查看。簡單來說如果我們是用傳統的回調函數方式來完成複雜的異步流程,就會無法避免大量的回調函數嵌套,產生回調地獄的問題。為了避免回調地獄讓我們開始愉快的Promise的學習時光吧!
// 我們想要執行完第一個再執行第二個再執行第三個
// 雖然我們使用同步的方式將異步的代碼學出來了,但是這樣的回調是不是讓我們的小腦袋瓜子嗡嗡的?
setTimeout(() => {
console.log('執行第一個');
setTimeout(() => {
console.log('執行第二個');
setTimeout(() => {
console.log('執行第三個');
setTimeout(() => {
console.log('執行第四個');
setTimeout(() => {
console.log('執行第五個');
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);回調地獄圖示,取自網絡,侵即刪。
Promise基本用法// Promise 基本示例
// promise的英文意思是承諾
// 在JS中Promise是一個對象,接收一個函數作為參數
const promise = new Promise(function (resolve, reject) {
// 這裡用於「兌現」承諾
// resolve(100) // 承諾達成
reject(new Error('promise rejected')) // 承諾失敗
})
promise.then(function (value) {
// 即便沒有異步操作,then 方法中傳入的回調仍然會被放入隊列,等待下一輪執行
console.log('resolved', value)
}, function (error) {
console.log('rejected', error)
})
Promise案例我們用Promise來封裝一個AJax
function ajax (url) {
return new Promise((resolve, rejects) => {
// 創建一個XMLHttpRequest對象去發送一個請求
const xhr = new XMLHttpRequest()
// 先設置一下xhr對象的請求方式是GET,請求的地址就是參數傳遞的url
xhr.open('GET', url)
// 設置返回的類型是json,是HTML5的新特性
// 我們在請求之後拿到的是json對象,而不是字符串
xhr.responseType = 'json'
// html5中提供的新事件,請求完成之後(readyState為4)才會執行
xhr.onload = () => {
if(this.status === 200) {
// 請求成功將請求結果返回
resolve(this.response)
} else {
// 請求失敗,創建一個錯誤對象,返回錯誤文本
rejects(new Error(this.statusText))
}
}
// 開始執行異步請求
xhr.send()
})
}
ajax('/api/user.json').then((res) => {
console.log(res)
}, (error) => {
console.log(error)
})
Promise的鏈式調用誤區嵌套使用的方式是使用Promise最常見的誤區。我們要使用promise的鏈式調用的方法儘可能保證異步任務的扁平化。// 嵌套使用 Promise 是最常見的誤區
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
})
})
})
})
})
鏈式調用的理解promise對象then方法,返回了全新的promise對象。可以再繼續調用then方法,如果return的不是promise對象,而是一個值,那麼這個值會作為resolve的值傳遞,如果沒有值,默認是undefined後面的then方法就是在為上一個then返回的Promise註冊回調前面then方法中回調函數的返回值會作為後面then方法回調的參數如果回調中返回的是Promise,那後面then方法的回調會等待它的結束Promise的異常處理ajax('/api/user.json')
.then(function onFulfilled(res) {
console.log('onFulfilled', res)
})
.catch(function onRejected(error) {
console.log('onRejected', error)
})
// 相當於
ajax('/api/user.json')
.then(function onFulfilled(res) {
console.log('onFulfilled', res)
})
.then(undefined, function onRejected(error) {
console.log('onRejected', error)
})簡單來說.catch是給整個promise鏈條註冊的一個失敗回調,推薦使用。