LeetCode — JavaScript Challenge 30 Days — Asynchronous JavaScript

TH K
13 min readJun 8, 2023

--

每當開始寫作文都想在第一段發牢騷,今天接到振奮人心的電話了,我終於可以好好的睡覺了,也期待明天的兩場面試,累積一下經驗也審視自己。

這篇文章我寫了四個小時,實在誇張,有幸讓人看到的話,希望是有幫助的歡迎提供建議或指教~~

來到 Asynchronous JavaScript 我覺得整個挑戰對我來說比較難的部分,主要是過去大部分是直接用Asynchronous function method 而不是自己設計一個 Asynchronous function, 因此這次算是了解的比較徹底了。

在 Alpha Camp 學期 2–2 需要提交一份關於 event loop 的筆記整理,現在回去想寫歸寫腦子當時應該還是空的,JS 是 single-threaded 的特性,一次執行一項任務,當今天遇上一個任務如無限迴圈、取得大量資料這種任務 JS 會直接卡在那個任務,使其他事都無法執行,如果是網頁用的 JS 整個頁面直接卡住,導致使用者體驗非常糟糕。

Now, JavaScript is a single-threaded language, which means it has only one call stack that is used to execute the program.

Event Loop

Event Loop 就是來解決這個問題的,原來的 JS 任務是排定同步程式碼於 stack 現在可以將非同步程式碼排定於 queue ,那麼哪些東西屬於非同步程式碼呢?? 我不打算一一介紹,但我會舉一些例子,一開始去學習會看到下面這張圖以為只要是 web API 就是非同步其實並非如此,web API 是某些 method 才屬於要排進 queue 的 如 alert() 不會排進 queue 而使用 setTimeout( fn, time) 中的 fn 會被排進 queue ,那我們常聽到的 promise, async/await 屬於 JS 非同步程式碼,而非 web API 的 method。

Event Loop 的運作在於不斷去確認 stack 中是否還有需要執行的任務,如果 stack 空了就把 queue 中的任務放入 stack 執行。

https://medium.com/%40tina0757023.bt07/%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-node-js%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AD%98-58c84e551531

Promise

當我們想創建一個非同步程式碼時,可以使用

new Promise( ( resolve , reject ) => { } ) or Async function fn ( ){ }

兩個都會自動 return 一個 promise object ,這個 promise object 是用狀態的變化來跟同步程式碼接軌的, 在 promise object 內使用內建 method resolve(), reject() 或丟出 Error object 控制狀態,

pending

如果都不使用 resolve(), reject() 或丟出 Error object 那 promise object 狀態永遠 pending 什麼事都不會發生 。

let x = new Promise( ( resolve , reject ) => { } ) 
// Promise {<pending>}
x.then(()=>console.log('resolved')) //未執行
x.catch(()=>console.log('rejected')) //未執行
x.finally(()=>console.log('resolved or rejected (not a pending state )')) //未執行

fulfilled

遇到 resolve() 狀態會變成 fulfilled ,接下來用 then() 接 resolve() return 的value 執行下一步或是 promise chaining 繼續需要處理的值或任務傳遞到下一個 promise object 。

// promise chaining
new Promise( ( resolve , reject ) => { resolve ('傳下去')} )
.then ( e => { console.log ( e ) ; return e + e })
.then ( e => { console.log ( e ) })
// 傳下去
// 傳下去 傳下去

rejected

遇到 reject() , Error object 狀態會變成 rejected,接下來用 catch() 接,新手大地雷!!!以為遇到 catch 整個 Promise object 就不能再傳下去,實際上無論then(), catch(), finally() 哪個method 最後都會 return Promise object ,可以不斷 chaining 下去 只是遇到 error 會直接略過非 catch() method 去執行,並非傳到 catch(), finally() 就不能再繼續 chaining!

let z = new Promise( ( resolve , reject ) => { reject('error') } )
// Promise {<rejected>: 'error'}

let w = new Promise( ( resolve , reject ) => { throw Error('ERROR') } )
// Promise {<rejected>: Error: ERROR

z.then(()=> console.log('不被執行')).catch(()=> console.log('Oops!' , 'z'))
w.then(()=> console.log('不被執行')).catch(()=> console.log('Oops!' , 'w'))

z.catch(()=>'catch到error之後居然可以傳到then').then((e)=> console.log(e))
//catch到error之後居然可以傳下去

z.finally(()=>console.log('promise object 可以一直傳下去'))
.finally(()=> console.log('promise object 可以一直傳下去'))

觀察同步非同步執行以及承接的語法 then(), catch(), finally()

let something = ''

let x = new Promise( ( resolve , reject ) => { } )
// Promise {<pending>}

let y = new Promise( ( resolve , reject ) => { resolve('done') } )
// Promise {<fulfilled>: 'done'}

let z = new Promise( ( resolve , reject ) => { reject('error') } )
// Promise {<rejected>: 'error'}

let w = new Promise( ( resolve , reject ) => { throw Error('ERROR') } )
// Promise {<rejected>: Error: ERROR


// then 跟 catch
y.then(()=> something += 'Async') // 改變同步程式碼中 something 變數
console.log(something) //同步程式碼執行中 something 還是空字串!!因為要先清空stack
z.catch(()=> console.log(something)) // Async
w.catch((error)=> console.log(error.message)) // ERROR

//finally
x.finally(()=> console.log('finish x')) //未執行
y.finally(()=> console.log('finish y')) //finish y
z.finally(()=> console.log('finish z')) //finish y
w.finally(()=> console.log('finish w')) //finish y

理解這些概念後,去了解 async/await 語法就很快了。

進入正題

Sleep ➞ 製作一個 async function可以消耗指定的秒數後再執行下個任務

let t = Date.now() //時間點前

sleep(100).then(() => console.log( Date.now() - t ) ) // 時間點後 -時間點前 = 100

async function sleep(millis) {
return new Promise(resolve => {
setTimeout(() => resolve(), millis)
})
}

async function sleep(millis) {
await Promise.resolve(setTimeout(()=>null, millis))
}

一開始看到這題我是直接把 setTimeout 寫入就天真的以為結束了,雖然 setTimeout 是非同步程式碼 ,但他並非 promise 因此應該是要寫成他需要再promise 中去執行直到完成 promise 才可以 fulfilled ,.then() 才能夠被執行,有趣的是 async function 語法中其實你無論使用 return 或 await 都能夠達成任務 ,因為無論哪個關鍵字被使用 ,async function 就是回傳 promise object。

Promise Time Limit ➞ 製作一個 async function 監控任務執行時間如果超過指定時間就取消執行

const limited = timeLimit((t) => new Promise(res => setTimeout(res, t)), 100);

limited(150).catch(console.log) // “Time Limit Exceeded” at t=100ms

//chaining 
var timeLimit = function(fn, t) {
return async function(...args) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject("Time Limit Exceeded");
}, t);
fn(...args)
.then(resolve)
.catch(reject)
.finally(() => clearTimeout(timeout));
})
}
};
// async sytax suger
var timeLimit = function(fn, t) {
return async function(...args) {
return new Promise(async (resolve, reject) => {
const timeout = setTimeout(() => reject("Time Limit Exceeded"), t);

try {
resolve(await fn(...args));
} catch (err) {
reject(err);
}

clearTimeout(timeout);
});
}
};

// promise race 我自己寫的效能不太好

var timeLimit = function(fn, t) {
return async function(...args) {
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, t,'limit');
});
return Promise.race([promise1, fn(...args)])
.then((value) => {
if(value==='limit') throw "Time Limit Exceeded"
return value
});

}
};

當時解題方向就是要讓兩個程式碼同時開始運行,但我究竟該怎麼讓兩個程式碼同時開始運行,所以最後是用Promise.race 達成目標,看了editorial 才學到一個重要的概念 setTimeout 的執行在計時的時候,相當於按下計時器去做別的事,透過這個特性我就可以把 reject() 寫在 setTimeout ,並去執行 await fn(…args) ,promise object 就看先遇到 reject() 還是 resolve() 決定結果。

仔細看 code 會發現有用到 clearTimeout() ,這是當任務有在指定時間內做完就不再需要繼續計時了,繼續計時是浪費內存的行為,影響效能的東西當然要剔除。

Promise Pool ➞ 限制同步執行任務數量

這題是最有趣的也非常實用,假設你需要下載 100 部電影,你用Promise.all強制執行同時下載 100 部電影,大概率是整個程式當掉或是直接失去反應,任務太大除了效能不好最不好的是整個程式 shutdown 因此可以做出一個function 限制執行下載 100 部電影期間,每次同時在下載 2 部電影。

//functions 執行的任務們 ,n 限制同步執行任務數量
var promisePool = async function (functions, n){
let i = 0 //用來計算執行過的任務數量
async function recursion(){
if(i === functions.length) return
await functions[i++]() //執行functions[i]() then i++
await recursion()
}
const nPromises = []
while(i < n){
nPromises.push(recursion())
}
await Promise.all(nPromises)
}

利用 recursion 的不斷地將任務置入 Promise.all(nPromises) 真的是很棒的思路,我要不知羞恥的把我的講解影片放這兒

ref: MDN, Neetcode, Sleep, Promise Time Limit,
Promise Pool

以下閒聊

從去年六月萌生轉職想法,去年九月開始執行,今年一月正式休學全心的投入網頁後端跟程式語言的學習,內心一直很不安也有點自卑,在開發跟做基礎訓練中不斷轉換,寫這些技術題時,我居然有種重回考大學那種從早到晚都在算數學跟解物理的感覺,然後我覺得很好玩,已經走火入魔了嗎?不過同時又會懷疑自己實作能力,實做 side project 時,又擔心自己技術沒在練,我想告訴自己靜下心來慢慢練慢慢進步,加油。

今天終於得到口頭offer ,在這承認我對於工作跟薪水內心常常很掙扎,一方面覺得自己的能力加學歷應該拿多少,一方面又覺得自己技術可能太差勁,薪水太低覺得被看扁,薪水太高又怕被說不適任或是直接不被任用,現在大環境真的很差,我就把握機會先到工程師的世界且戰且走了,期待挑戰~

--

--