今天我要來介紹JavaScript時常被拿來討論的非同步問題,首先我們要來先討論,為什麼我們需要在JavaScript處理非同步問題。

為什麼需要處理非同步問題

在開發專案的時候,當你設計好一個Http request時,你可能會遇到神奇的狀況。
例如:

  1. JSON server的資料未更新
  2. 後端未收到資料衍生的error
  3. 後端收到的資料,是未經過處理
  4. 已經寫了修改資料的程式,卻沒有任何動靜

舉例

這些舉例的問題,通常是初學時常碰到的,明明邏輯都沒問題,卻依然無法讓程式順利進行。
如果你未經歷過,無法共鳴上述的情境的話,以下再提供一段程式讓各位品嚐。
你覺得以下程式碼會執行出什麼樣的結果?:

1
2
3
4
5
6
7
8
9
10
function doSomething() {
const getUser = () => {
setTimeout(() => {
return { name: 'yy' }
}, 2000)
}

const user = getUser()
console.log(user.name)
}

你會發現他拋出了以下錯誤:

1
error: Uncaught TypeError: Cannot read property 'name' of undefined

這時,你就會體會到上面講的情境了,明明邏輯都沒問題,卻依然無法讓程式順利進行的狀況產生。
要知道為什麼,你需要先了解JavaScript是怎麼執行程式的。

原因

JavaScript是一個單執行緒的程式語言,因此無法做到多工的效果。
你以為執行順序應該是:

  1. 執行getUser()
  2. setTimeout執行2秒
  3. assign getUser() to user
  4. 輸出user

事實上是:

  1. 執行getUser()
  2. 輸出user(ERROR發生!!)
  3. setTimeout執行2秒
  4. assign getUser() to user

JavaScript執行同一層的function scope,會依序把任務放入CallStack。
需要等待執行的任務(setTimeout),會放入瀏覽器的APIs進行setTimeout的執行。
執行完setTimeout的程式會放入Message Queue(Task Queue),再從Message Queue取得任務(執行完setTimeout的任務)
再來Event Loop會等待與監聽任務的執行,並且依序將function scope拆解出來放進CallStack。

以下再來給一個範例:

1
2
3
4
5
6
7
8
9
10
const sayHi = () => {
console.log('Hi');
}

const oops = () => {
alert('oops!');
}

setTimeout(oops, 2000);
sayHi();

你預期要先alert再log,但是經由上面的解釋之後,會是先log再alert,你看出來了嗎?

如何解決?


那我們要如何解決這種執行順序的問題呢?
答案就是Callback function

Callbacks

Callback看似不容易理解,但是事實上原理很簡單,你可以理解成:如果這個事件發生了,麻煩幫我執行這個事件。
什麼意思呢?讓我們來看看以下範例:

1
2
3
4
5
6
7
8
9
10
11
function doSomething() {
const getUser = callback => {
setTimeout(() => {
callback({ name: 'yy' })
}, 2000)
}

getUser(user => {
console.log(user.name)
})
}

這個程式碼是一開始的改良版,原本console.log是在getUser的裡面
但是現在放到getUser裡面變成Anonymous function傳進去,為什麼要這樣做呢?
剛剛有提到JavaScript是一個單執行緒的程式語言,所以我如果要等待某個特定的任務執行完,再執行下個任務時
我可以把我想要之後執行的任務包裝成一個Anonymous function傳進去,並且在適當的時機使用它,我就能達到非同步的效果。
所以執行順序就會變成:

  1. 執行getUser(…)
  2. 執行非同步 setTimeout執行2秒
  3. 執行callback function
  4. 輸出user

是不是比較容易理解了呢?

Welcome to the Callback Hell

但是事情有時候並不是這麼的簡單,歡迎來到Callback Hell。
以下是一個簡單的加法範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function add (a, b, callback) {
const result = a + b
callback(null, result)
}

add(1, 2, (err, result) => {
if (err) {
console.log(err)
return
}
console.log(result)
add(result, 3, (err, result2) => {
if (err) {
console.log(err)
return
}
console.log(result2)
add(result2, 4, (err, result3) => {
if (err) {
console.log(err)
return
}
console.log(result3)
})
})
})

你有看出什麼問題嗎?
一般邏輯簡單的非同步也許可以用callback,但是當邏輯變複雜或是重複的事情變多時,callback就會越寫越精彩。
最後就變成大家喜歡惡搞的脈衝波圖片了。程式碼可想而知,也不好維護它了。

尤其在Error handling的時候,還要去思考callback callback了哪個callback又callback哪個callback……
這絕對不是你想要維護的程式碼吧…一點都不好玩!!
那該怎麼改善呢?JavaScript在ES6提出了一個Solution,Promise是我們的答案。

Promises


Promise就是字面上本身的意思。

JavaScript在ES6推出了Promise讓我們更方便去處理非同步。
在處理程式如果遇到非同步的問題,只要把希望之後再執行的程式包裝成Anonymous function放到then後面
然後再到程式裡面return 一個Promise去執行你放到then裡面的Anonymous function,就能達到我們要的目的。
以下是範例程式:

1
2
3
4
5
6
7
8
9
10
const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ name: 'yy' })
}, 2000)
})
}
getUser().then(user => {
console.log(user.name)
})

Promise也提供兩個方便的參數:resolve 和 reject
當你呼叫resolve(data),表示你視同這個Promise執行成功,並且回傳參數到then。
而如果你呼叫reject(err),表示你認為這個執行有問題,需要拋出異常。

以下是我改良上面的Callback Hell變成Promise版本的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
function add (a, b) {
const result = a + b;
console.log(result);
return new Promise((resolve) => {
resolve(result);
})
}

add(1, 2)
.then(event => add(event, 3))
.then(event => add(event, 4))

看完之後,你應該就會喜歡上用Promise了吧?而且程式碼更簡潔易懂。
當你有多筆任務的時候,你只需要不斷的then之後再then之後再then…即可。
不必再陷入多重callback的險境之中,更容易維護程式碼。
是不是和之前的程式碼差很多呢?

但是如果我們需要傳輸更多資料呢?
Promise有個小問題是,一次只能處理一次的資料。
這也是為什麼Observable會變得如此熱議的原因。
RxJS能解決我們的問題。

RxJS


RxJS提供Observable,用法不同於Promise。

Promise傳送一個Http request,server回覆response,並且解決Promise,最後結束程式。
但是RxJS並非如此,你可以handle streams of data,去監聽所有新的事件並且送出新的值,這是Promise無法辦到的事情。

詳細的內容可參考RxJS官方文件的Observable

分類 Single Multiple
Pull Function Iterator
Push Promise Observable

基本用法

RxJS不是本篇的重點,所以以下放個簡單的使用方式:

1
2
3
4
5
6
7
8
9
10
const button = document.querySelector('button')
const observable = Rx.Observable.fromEvent(button, 'click')
observable.subscribe(
event => {
console.log(event.target)
},
error => {
console.log(error)
}
)

RxJS使用方法不同於Promise,需使用Observable監聽,並再在透過subscribe去處理成功與失敗的問題。

等同於then的用法

你可以在subscribe裡面再放入一個Anonymous function。

1
2
3
4
5
6
7
8
9
10
const button = document.querySelector('button');
const observable = Rx.Observable.fromEvent(button, 'click');
observable.subscribe(
(event) => {
const secondObservable = Rx.Observable.timer(1000);
secondObservable.subscribe(
(data) => console.log(data);
);
}
);

這個時候你會不會心想:這不就又回來Callback Hell了嗎?
以下可以達到類似then的用法:

1
2
3
4
5
const button = document.querySelector('button')
const observable = Rx.Observable.fromEvent(button, 'click')
observable
.switchMap(event => Rx.Observable.timer(1000))
.subscribe(data => console.log(data))

透過switchMap將非同步的程式碼串在一起。

補充

  1. 由於RxJS6已經釋出了,所以使用上也比以往方便許多
    現在你可以這樣宣告了:

    1
    2
    3
    import { Observable, Subject } from 'rxjs'
    import { map, take } from 'rxjs/operators'
    import { of } from 'rxjs'
  2. 但是請注意!並不是Promise不好的意思,如果不需要處理大量的資料,那麼RxJS並非是一個好選擇。

Async/Await

當你在開發Promise和Observable的時候,相信你一定也會產生一點murmur。
為什麼我們要在這麼多blocks上寫程式?難道不能像以往一樣直接順著寫程式碼嗎?
那麼Async/Await可能會是你的選擇。
JavaScript在ES8推出了Async/Await給大家使用。
開發者只需要在function之前加入async,裡面的非同步函數加上await,即可輕鬆開發。

以下是範例程式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const button = document.querySelector("button");
const div = document.querySelector("div");

const setText = (text) => {
div.textContent = text
}

const checkAuth = () => {
return new Promise((resolve, reject) => {
setText('Checking Auth...')
setTimeout(() => {
resolve(true)
}, 2000)
})
}

const fetchUser = () => {
return new Promise((resolve, reject) => {
setText('Fetching User...')
setTimeout(() => {
resolve({ name: "yy" });
}, 2000)
})
}

button.addEventListener("click", async () => {
const isAuth = await checkAuth()
let user = null;
if (isAuth) {
user = await fetchUser()
}
setText(user.name)
});

只需要在想要達到非同步的程式碼加入,非常的方便。

結論

以上介紹這麼多東西,不管是哪一項技術,關鍵都在於你會不會用到它。
如果不需要用到,那麼挑選自己方便的技術只能即可。
除了開發好程式,也要注意是否影響到Development Experience。