JavaScript 非同步大亂鬥
大家都知道 js 的非同步特性,這是一把雙面刃有時一些奇怪的 bug 也是因為非同步的問題,現在 es6 的語法已經讓我們可以很簡單的去操作避免掉非同步的一些問題,回過頭來還是必須要去深入理解基本功這件事情。
同步與非同步
同步(Synchronous) 與 非同步(Asynchronous) 這兩個名詞大家一定不陌生,以下用自己的認知來闡述這兩件事情,我們以一個餐廳的服務生當做例子。
非同步:餐廳內的一個服務生可以點餐完將菜單交給廚房的廚師,並且接著服務下一桌客人等到廚房將餐點準備好可以出菜時再回來將餐點送到顧客的桌上。
同步:餐廳內的一個服務生點餐完之後將菜單交給廚房,就在廚房等待廚師將餐點準備完畢將菜送到顧客桌上,才會再繼續服務下一桌的客人。
看出來了嗎?兩著的差別在於服務生點餐完之後是否有繼續服務下一桌客人,以 Node 來講就是 Single Thread 可以一次處理多個 Request 不須等待第一個 Request 處理完再去處理下一個 Request。
常遇到的問題範例
再來看看下面這些範例,這些也是面試時很常見到的考古題。
console.log('Befor')
setTimeout(() => {
console.log('loading data')
}, 2000)
console.log('After')
結果就會是
Befor
After
loading data
你會說欸~不是應該會照程式順序印出 log 嗎怎麼會最後才打印出 loading data
還記得我們提到的非同步特性嗎? setTimeout
並不會等到它執行完之後才繼續執行下一行,而是 javascript 會將它擺到背景的一個 queue 中,未來有時間將會再另外開一篇討論這個有趣的議題。
如果你將程式重構一下會遇到另一個問題,以下是簡單的範例
console.log('Befor')
const user = getUser(1)
console.log(user)
console.log('After')
function getUser(id) {
setTimeout(() => {
return {id:id,userName:'jimmy'}
}, 2000)
}
結果會是
Befor
undefined
After
程式並不會等待你兩秒跑完就繼續往下一行執行,所以就會得到 undefined
這個結果。
解決方式
- Callbacks
- Promises
- Async/await
Callbacks
Callbacks 是一開始 es6 之前的寫法,大致上的結構會像是這樣
console.log('Before')
getUser(1, (user) => {
console.log('user',user)
})
console.log('After')
function getUser(id, callback) {
setTimeout(() => {
console.log('get user....')
callback({
id: id,
userName: 'jimmy'
})
}, 2000);
}
執行結果會是
Before
After
get user....
user { id: 1, userName: 'jimmy' }
如果你要做的事情再更複雜一點,拿取使用者之後再拿取該使用者的 repository 再拿取該 repo 的 commit ,你的程式碼就會長得非常醜
console.log('Before')
getUser(1, (user) => {
console.log('user', user)
getRepositories(user.userName, (repo) => {
console.log('repo', repo)
getCommits(repo[0], (commits) => {
console.log('commits', commits)
})
})
})
console.log('After')
function getUser(id, callback) {
setTimeout(() => {
console.log('get user....')
callback({
id: id,
userName: 'jimmy'
})
}, 2000);
}
function getRepositories(username, callback) {
setTimeout(() => {
console.log(`get ${username} repositories....`)
callback(['repo1', 'repo2', 'repo3'])
}, 2000)
}
function getCommits(repo, callback) {
setTimeout(() => {
console.log(`get ${repo} commits....`)
callback(['init', 'add something', 'modify somthing'])
}, 2000)
}
打印結果
Before
After
get user....
user { id: 1, userName: 'jimmy' }
get jimmy repositories....
repo [ 'repo1', 'repo2', 'repo3' ]
get repo1 commits....
commits [ 'init', 'add something', 'modify somthing' ]
如此一來有先後順序性的流程就會造成所謂的 callback hell
Promises
如果使用了 promise 就簡單得多了整個程式碼的可讀性將會高很多
getUser(1).then(user => getRepositories(user.userName))
.then(repo => getCommits(repo[0]))
.then(commits => console.log('Commits', commits))
.catch((err) => {
console.log(err)
})
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('get user....')
resolve({
id: id,
userName: 'jimmy'
})
}, 2000);
})
}
function getRepositories(username) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`get ${username} repositories....`)
resolve(['repo1', 'repo2', 'repo3'])
}, 2000)
})
}
function getCommits(repo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`get ${repo} commits....`)
resolve(['init', 'add something', 'modify somthing'])
}, 2000)
})
}
差異上並不大只是改成每一個 function
返回一個 Promise
物件並且將 callback
改成 resolve
Async/await
最後是 async 的寫法,相比起 promise 又更簡單了一點,可是還是必須使用 try catch
來捕捉錯誤訊息,這是比較可惜的地方,不過實際上他只是語法糖而已實質上他還是 promise 的運作方式。
async function displayCommits() {
try {
const user = await getUser(1)
const repo = await getRepositories(user.userName)
const commits = await getCommits(repo[0])
console.log(commits)
} catch (error) {
console.log(error)
}
}
displayCommits()
// 以下同 promise
是不是看起來又更簡潔了一點而且又像同步的寫法,不過在使用上必須確保瀏覽器支援度。
結論
現在瀏覽器基本上大多數已經支援 es6 的語法了,除了萬惡的 IE 如果不需要考慮支援度可以放心地使用 es6 以上的寫法,如果必須相容舊版瀏覽器就可能要引入 Polyfill 或是 Babel