『新手日記』Day-26 同步非同步
居然是因為生病中斷連載,難過!!!
最早在AC有一堂課是要我們學習分析什麼是 event loop。 在那邊第一次對同步跟非同步有了一絲的印象,那時候的文章:
https://www.notion.so/168538cd537243a18f89ba1dcca8b16d#49f4a62c06c7401084b31fb0f07df83e
之後陸陸續續在課堂或文章內容裡都有提到這個概念,但好像真的沒有認真的統整過一次,剛好在最新的課程裡講了Callback、Promises、Async/Await,又加上我第一次沒有聽懂決定再看一次,順便統整一次內容。
前言
我們都知道『JavaScript 是一種單線程(single threaded runtime)的程式語言』,因為作為瀏覽器的腳本(程式語言),他負責處理頁面的互動以及Dom的操作,讓你可以在網頁中實現出複雜的功能。但JavaScript 只是程式語言,因此我們需要一個『執行環境(runtime)』所提供的東西,例如說 setTiemout、document 等等,而這個 runtime可以是瀏覽器或是我們最近在學的Node.js。
blocking(阻塞)與non-blocking(非阻塞)
1 | let queue = [] |
我們用這段程式碼來解釋什麼是 blocking,大家可以順便猜一下會consloe.log出來什麼。
雖然setTimeout
的時間間距設定為 0,乍看之下是「馬上執行」的意思。但因為setTimeout
是屬於非同步事件,因此還是會在其他原始碼運行完以後才執行。
執行流程如下圖:
在執行queue.forEach
由於資料過於龐大程式就會停在queue.forEach
,要等執行完畢以後,才會去執行setTimeout
。換句話說,queue.forEach
「阻擋」了後續指令的執行,這時候我們就說這是一個 blocking(阻塞),因為程式的執行會一直 block 在這裡,直到執行完畢值為止。
如果後續指令跟現在在執行的程式有關那就只好認了,但如果現在執行的事情跟後續執行的事情一點關係都沒時,那把時間都浪費在這真的會奢侈。要解決這個狀況那我們就會運用到 non-blocking(非阻塞)。
用一個生活化的方式來解釋:去百貨公司美食街點餐,點完以後店家會給我一個呼叫器,等到餐點準備好的時候,呼叫器就會響,我就可以去店家領取餐點,而不用在原地傻傻地等。所以要解決這個問題我們就必須拿到那個『呼叫器』,而在 JavaScript 裡面,function 就很適合當作呼叫器!意思就是「當這個程序執行完畢時,請來執行這個 function,並且把結果傳進來」,而這個 function 又被稱作 callback function(回呼函式)
這樣我們就可以“看似”一次執行多個程式碼了,但請記住他只是“看似”而已,JavaScript 只是擅長用非同步的方式在不同事情間「切換」,它的本質仍然是一次執行一件事 (single thread)。
synchronous/asynchronous
了解了什麼是blocking(阻塞)與non-blocking(非阻塞)後,其實你已經懂了synchronous(同步) 與 asynchronous(非同步),為什麼這麼說呢?因為在Node.js 的官方文件是這麼說的:
Blocking methods execute synchronously and non-blocking methods execute asynchronously.
阻塞的方法會同步地(synchronously)執行,而非阻塞的方法會非同步地(asynchronously)執行
但我相信還是會有很多人跟我一樣,同步不就是同時進行嗎?為什麼我卻只能一次執行一件事。
這就要提到中文翻譯的問題了,但我英文很差這邊就用另一個方式跟大家解釋:
synchronous(同步)
Synchronous:餐廳老闆自己從帶位、點餐、煮菜等,一條龍完成所有作業,在完成一組客人後,才能接待下一位客人。
在JavaScript大部分功能都是同步的,而同步的定義是指:程式碼的執行順序,依照由上而下執行,最後才輸出。
優點當然是很好閱讀,但缺點就是效率變很差
asynchronous(非同步)
Asynchronous:餐廳老闆請了不同的人負責各自專門的事情,服務生負責點餐、廚師負責煮飯。在固定的時間內,可以藉由大家的合作,處理多組客人。
在這案例我們可以得知:
- 非同步機制能加速餐廳運作,更高效率的處理事情!
- 但非同步的狀況下,不同客人在不同階段需要的服務是不同的,因此員工間可能會產生「等待」的過程。
我們可以總結:所謂的「非同步處理」,可以說就是釐清各式各樣的「等待」流程,並且在因「等待」而複雜化的程式流程中,確保程式能正確運作。。
非同步處理的演進
接下來我會用同一個案例來練習同步處理:
虛擬碼
實際開發過程裡,收到專案規格後不會馬上開始撰寫程式碼,而是會用虛擬碼(pseudocode) 來釐清程式的邏輯。
專案目的:你需要為 2 位重要的客人安排一趟台北美食探索之旅,現在你手邊精選出 6 家優質餐廳,需要安排他們各自嘗試其中的 3 間餐廳,2 位客人分配到的餐廳沒有重覆。
先簡單拆解程式必要做的事情:
設定資料並連線資料庫:
- 從資料庫取得資料
- 將資料放進資料庫裡
推薦餐廳的邏輯:
- 先建立使用者資料、找到使用者與餐廳的關聯,並生成結果。
1 | // 資料描述 |
- 由於資料庫對這支 JavaScript 程式來說,是一個外部的 server,我們不確定是否資料庫會連接成功,因此必須等待資料庫回傳「連接成功」的確認信號後,才能繼續往下執行程式。
- 若忽略了這個「等待」就匆匆執行「塞資料」的程序的話,就會出現資料消失等不能理解的狀況。
callback
1 | db.once('open', () => { |
for (const [user_index, user] of users.entries())
首先,使用 for
拿出使用者的資料,並對每個使用者進行處理。因為要拿到資料庫裡的key/value pairs 我們使用entries 取出 user_index。
UserModel.create(user, (err, user) => {…}
接著,使用 mongoose 提供的 Model.create 建立使用者資料,這裡是一個典型的非同步操作:
- 若操作成功,資料庫就會回傳一個成功創建的
user
物件,也就是我們設定給 UserModel.create 的第一個參數 - 若成功取得 user 物件,就執行第二個參數中設定的 callback function,也就是
(err, user)=>{ })
這段
restaurants.forEach((restaurants, rest_index) => {…}
用 forEach
取出 data 中 restaurants 資料(包含 restaurant, rest_index),下一步用條件式建立使用者與餐廳資料的關聯。
條件式建立後,每個 restaurant 會有一個 userId
的參數,與創建使用者時自動生成的 user._id 做 mapping,可以用 restaurant.userId = user._id
表示,建立使用者跟餐廳資料的關聯。
接下來想要用 RestaurantModel.create
將餐廳資料一次性的塞進資料庫裡,需要先建立 userRestaurant = []
,並用 userRestaurant.push(restaurant)
,將匹配好的資料暫存到陣列中。
RestaurantModel.create(restaurants, (err, user) => {…}
程式終止的條件是:等待所有使用者的餐廳資料都建立完成。
在邏輯設計上,我們可以用 count 函式確認所有 users 與 restaurants 都成功創建完畢,來判定資料建立完成並結束執行程式。
此時可以用 node.js 提供的 process.exit
來終止程序。
Promises
Promise 是一個物件建構子 (constructor),使用時需要先從 Promise 物件產生物件實例 (instance),再使用繼承特性的 instance 去包裝程式碼的 callback 流程。
callback hell
https://www.notion.so/168538cd537243a18f89ba1dcca8b16d#2dd5a20abbf743cbb92bbed9f103796e
從 Callback 到 Promise
從邏輯上來說,程式流程有 4 段:
- 先創建 User 資料,也就是 for loop + UserModel.create 的段落
- UserModel.create 執行成功後,略為整理資料,進行 User x Restaurant 的配對
- 再用 RestaurantModel.create 創建資料,
- 兩個資料都創建好以後,就印個 console.log 然後終止程式
在 callback 的版本中,流程 3 包含在 2 的 callback 裡,然後 4 又包在 3 的 callback 裡。
至於在 Promise 版本裡,可以運用 then 的架構,把四段流程刻意拉出來,創造一種由上而下的閱讀體驗。
async/await
使用邏輯很簡單,如下圖示意:
原本當同步與非同步邏輯混搭時,我們會需要用一堆 then 來控制先後順序,而在 async/await 中,只要關鍵字放對了,就可以確保程式按照「視覺上由上而下的順序」來執行。例如上圖示意的 1~5。
使用時要把握三個原則:
1.要先確認有 Promise 物件實例,也就是已經定義好 resolve/reject,才能使用 async/await
2.在流程中正確設定關鍵字:
- 把後續流程用一個
async function
包裝起來 - 設定好
async function
之後,在要運用非同步處理的地方加上await
關鍵字
3.注意 async/await 和 then 不可以混搭使用
流程架構
仍然會有雙層的結構,因此兩個層級各需要一個 async
關鍵字,而 await
後面接的都是 Promise 物件,也就是需要非同步處理的區塊。