這裏是修真院前端小課堂,每篇分享文從前端
【背景介紹】【知識剖析】【常見問題】【解決方案】【編碼實戰】【擴展思考】【更多討論】【參考文獻】程序員
八個方面深度解析前端知識/技能,本篇分享的是:es6
【異步編程有哪幾種方法來實現?】web
你們好,我是IT修真院武漢分院web第16期的學員孟晨,一枚正直純潔善良的web程序員 今天給你們分享一下,修真院官網js(職業)任務五,深度思考中的知識點——異步編程有哪幾種方法來實現?編程
1.背景介紹
你可能知道,Javascript語言的執行環境是"單線程"(single thread)。
所謂"單線程",就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。設計模式
爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。瀏覽器
"同步模式"就是上一段的模式,後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的;"異步模式"則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。
"異步模式"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。服務器
2.知識剖析
經常使用的異步編程的幾種方法?
首先咱們來看一個基本的例子,在這個例子中輸出的順序是1,3,2,咱們想讓他按順序1,2,3輸出就須要用到異步編程的方法異步
function fn1() { async
console.log('Function 1')
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1()
fn2()
fn3()
// output =>
// Function 1
// Function 3
// Function 2
壹.回調函數
回調函數是異步編程的方法中最簡單也是最經常使用的一個方法
採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。
回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,並且每一個任務只能指定一個回調函數。
若是再嵌套多幾層,代碼會變得多麼難以理解 這個被稱之爲「回調函數噩夢」(callback hell)!!!
也是能夠看看例子,若是按照我們剛剛的寫法的話輸出順序會是3,2,1,因此把每一個函數中寫成回調函數的形式
就可讓執行完了前面的纔會執行後面的,而後標紅的部分就是被稱爲回調函數噩夢的緣由,
只是少數嵌套函數的話不明顯但多層嵌套代碼就會顯得很混亂
function fn1(f) {
setTimeout(() => {
console.log('Function 1')
f()
}, 1000)
}
function fn2(f) {
setTimeout(() => {
console.log('Function 2')
f()
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1(function () {
fn2(fn3)
})
// output =>
// Function 1
// Function 2
// Function 3
貳.事件發佈/訂閱
發佈/訂閱模式也是諸多設計模式當中的一種,剛好這種方式能夠在es5下至關優雅地處理異步操做。什麼是發佈/訂閱呢?以上一節的例子來講,fn1,fn2,fn3均可以視做一個事件的發佈者,只要執行它,就會發佈一個事件。這個時候,咱們能夠經過一個事件的訂閱者去批量訂閱並處理這些事件,包括它們的前後順序。下面咱們基於上一章節的例子,增長一個消息訂閱者的方法(爲了簡單起見,代碼使用了es6的寫法):
class AsyncFunArr {
constructor(...arr) {
this.funcArr = [...arr]
}
next() {
const fn = this.funcArr.shift()
if (typeof fn === 'function') fn()
}
run() {
this.next()
}
}
const asyncFunArr = new AsyncFunArr(fn1, fn2, fn3)
function fn1() {
console.log('Function 1')
asyncFunArr.next()
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
asyncFunArr.next()
}, 500)
}
function fn3() {
console.log('Function 3')
asyncFunArr.next()
}
// output =>
// Function 1
// Function 2
// Function 3
叄.PROMISE對象
romises對象是CommonJS工做組提出的一種規範,目的是爲異步編程提供統一接口。 簡單說,它的思想是,每個異步任務返回一個Promise對象,該對象有一個then方法,容許指定回調函數。好比,f1的回調函數f2,能夠寫成: f1().then(f2); 這樣寫的優勢在於,回調函數變成了鏈式寫法,程序的流程能夠看得很清楚,並且有一整套的配套方法,能夠實現許多強大的功能。 好比,指定多個回調函數: f1().then(f2).then(f3); 再好比,指定發生錯誤時的回調函數: f1().then(f2).fail(f3); 並且,它還有一個前面三種方法都沒有的好處:若是一個任務已經完成,再添加回調函數,該回調函數會當即執行。因此,你不用擔憂是否錯過了某個事件或信號。這種方法的缺點就是編寫和理解,都相對比較難。
標紅處就是所謂的鏈式寫法,這樣寫帶來告終構清晰的好處
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 1')
resolve()
}, 1000)
})
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 2000)
})
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1()
.then(fn2)
.then(fn3)
// output =>
// Function 1
// Function 2
// Function 3
肆.GENERATOR
若是說Promise的使用可以化回調爲鏈式,那麼generator的辦法則能夠消滅那一大堆的Promise特徵方法,好比一大堆的then()。
generator函數asyncFunArr()接受一個待執行函數列表fn,異步函數將會經過yield來執行。在異步函數內,經過af.next()激活generator函數的下一步操做。
這麼粗略的看起來,其實和發佈/訂閱模式很是類似,都是經過在異步函數內部主動調用方法,告訴訂閱者去執行下一步操做。可是這種方式仍是不夠優雅,好比說若是有多個異步函數,那麼這個generator函數確定得改寫,並且在語義化的程度來講也有一點不太直觀。
function fn1() {
setTimeout(() => {
console.log('Function 1')
}, 1000)
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
function* asyncFunArr(...fn) {
fn[0]()
yield fn[1]()
fn[2]()
}
const af = asyncFunArr(fn1, fn2, fn3)
af.next()
// output =>
// Function 1
// Function 2
// Function 3
伍.優雅的ASYNC/AWAIT
使用最新版本的Node已經能夠原生支持async/await寫法了,經過各類pollyfill也能在舊的瀏覽器使用。那麼爲何說async/await方法是最優雅的呢?
有沒有發現,在定義異步函數fn2的時候,其內容和前文使用Promise的時候如出一轍?再看執行函數asyncFunArr(),其執行的方式和使用generator的時候也很是相似。
異步的操做都返回Promise,須要順序執行時只須要await相應的函數便可,這種方式在語義化方面很是友好,對於代碼的維護也很簡單——只須要返回Promise並await它就好,無需像generator那般須要本身去維護內部yield的執行。
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 1')
resolve()
}, 3000)
})
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 2000)
})
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
async function asyncFunArr() {
await fn1()
await fn2()
await fn3()
}
asyncFunArr()
// output =>
// Function 1
// Function 2
// Function 3
3.常見問題
什麼時候使用異步
4.解決方案
在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。
5.代碼實戰
6.拓展思考
異步的好處: 一、異步流程能夠當即給調用方返回初步的結果。
二、異步流程能夠延遲給調用方最終的結果數據,在此期間能夠作更多額外的工做,例如結果記錄等等。
三、異步流程在執行的過程當中,能夠釋放佔用的線程等資源,避免阻塞,等到結果產生再從新獲取線程處理。
四、異步流程能夠等屢次調用的結果出來後,再統一返回一次結果集合,提升響應效率。
7.參考文獻
談一談幾種處理JavaScript異步操做的辦法
Javascript異步編程的4種方法
8.更多討論
鳴謝
感謝你們觀看
BY : 孟晨