Skip to main content

Event Loop

· 6 min read

JS 是 single-threaded 語言


JS 是 single-threaded 程式語言,只有一個主執行緒(main thread)來執行程式碼,代表他只能同步地執行的任務。但是通常 JS 拿來寫網頁前端的時候,一定會處理到非同步程式碼,例如呼叫 API 就是個最常見的場景,此時就需要依靠瀏覽器這個 runtime 來提供這項能力。

Event loop 及 Task queue


瀏覽器提供了 web api,例如 setTimeout, fetch 以及 Event loop 這樣的機制來管理非同步事件。

JS 在執行時,執行 function 會產生 execution context,這項任務會被放進到 call stack 中,以先進後出的順序被執行。如果過程中遇到非同步任務,例如 setTimeout,會被丟進 callback (task) queue 當中,以先進先出的順序被執行。

Event loop 這個機制就是去幫你不斷監控目前 call stack 的同步任務是不是已經清空了,空了的話,就幫你去 task queue 拿非同步任務出來並放到 call stack 執行。他是會不斷重複直到所有任務都執行完畢的,因此稱作為 Event loop 事件循環。

實際上,非同步的 web api 對 task queue 來說,又可以區分成兩類,Microtask queue 與 Macrotask queue。 當 call stack 清空後,其實會優先去 Microtask queue 拿任務出來執行,等到 Microtask queue 空了之後,瀏覽器會進行畫面渲染,然後再執行 Macrotask queue 的東西。

這表示,如果在執行 Macrotask 過程中產生了 Microtask,那麼這些 Microtask 會優先於下一個 Macrotask 執行。換句話說,Microtasks 具有較高的優先級,並且會在下一個 Macrotask 開始之前執行。

通常會產生 Macrotask 的有 JS script 本身及setTimeout, setInterval 這種較大型的任務,而 Microtask 中常由 Promise 產生。有這樣的分別是為了最佳化非同步管理,讓 Microtask queue 管理可能會造成 UI 組塞(DOM 操作)這樣優先度較高的任務,先把畫面更新完之後再處理較不急迫的 Macrotask

範例


範例一:

console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
setTimeout(() => {
console.log(4)
}, 0)
console.log(5)
  1. Global execution context (GEC) 被放進 call stack。
  2. 執行第一行 console.log(1)console.log(1) EC 被放到 call stack。透過瀏覽器提供的 web api 取用 console 物件,呼叫方法 log,在 console 上印出 1。console.log(1) EC 從 call stack 被 pop out。
  3. 接著 setTimeout() EC 被放入 call stack, setTimeout() 這個 web api 被取用,瀏覽器將我們寫的 callback function () => {console.log(2)} 存起來不執行,並計時 0 ms 。setTimeout() EC 從 call stack 被 pop out。後續程式碼繼續執行。
  4. 執行下一行 console.log(3)console.log(3) EC 被放到 call stack。透過瀏覽器提供的 web api 取用 console 物件,呼叫方法 log,在 console 上印出 3。console.log(3) EC 從 call stack 被 pop out。
  5. 再次setTimeout() EC 被放入 call stack, setTimeout() 這個 web api 被取用,瀏覽器將我們寫的 callback function () => {console.log(4)} 存起來不執行,並計時 0 ms。setTimeout() EC 從 call stack 被 pop out。後續程式碼繼續執行。
  6. 執行下一行 console.log(5)console.log(5) EC 被放到 call stack。透過瀏覽器提供的 web api 取用 console 物件,呼叫方法 log,在 console 上印出 5。console.log(5) EC 從 call stack 被 pop out。
  7. 程式碼執行到底了,Global execution context 從 call stack 被 pop out。
  8. 至少過了 0 ms 後, callback function () => {console.log(2)} (簡稱 cb1) 就會被移到 callback queue 等待。
  9. 滿足 「callback queue 裡面有待執行任務」以及「目前 call stack 已清空」這兩個條件,Event loop 就會把 callback queue 裡面的任務一次拿一個出來 (先進來的先被拿出來 ),放到 call stack 裡面。
  10. cb1 execution context 從 callback queue 被拿出來塞到 call stack。
  11. 執行 cb1 console.log(2),透過瀏覽器提供的 web api 取用 console 物件,呼叫方法 log,在 console 上印出 2。
  12. cb1 execution context 執行完畢,從 call stack 被 pop out。
  13. 至少過了 0 ms 後, callback function () => {console.log(4)} (簡稱 cb2) 就會被移到 callback queue 等待。
  14. 再度滿足 「callback queue 裡面有待執行任務」以及「目前 call stack 已清空」這兩個條件。
  15. cb2 execution context 從 callback queue 被拿出來塞到 call stack。
  16. 執行 cb2 console.log(4),透過瀏覽器提供的 web api 取用 console 物件,呼叫方法 log,在 console 上印出 4。
  17. Cb2 execution context 執行完畢,從 call stack 被 pop out。

console:

1
3
5
2
4

範例二:

console.log('script start')

setTimeout(function () {
console.log('setTimeout')
}, 0)

Promise.resolve()
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})

console.log('script end')
  1. 印出 script start
  2. 把 setTimeout callback 放到 Macrotask queue
  3. 把 Promise resolve 因此,第一個 .then callback 放到 Microtask queue
  4. 印出 script end
  5. call stack 空了,去拿 Microtask queue 的任務,也就是第一個 .then 的 callback,放到 call stack,執行。印出 promise1
  6. 把 Promise 第二個 .then callback 放到 Microtask queue
  7. 同上流程,去執行第二個 .then 的 callback。印出 promise2
  8. Microtask queue 沒東西了,去拿 Macrotask queue 的任務,setTimout 的 callback 被放到 call stack,執行,印出 setTimeout

console

script start
script end
promise1
promise2
setTimeout

範例三:

console.log('begins')

// S1
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve().then(() => {
// P1
console.log('promise 1')
})
}, 0)

new Promise(function (resolve, reject) {
console.log('promise 2')
// S2
setTimeout(function () {
console.log('setTimeout 2')
resolve('resolve 1')
}, 0)
}).then(
// P2
(res) => {
console.log('dot then 1')
// S3
setTimeout(() => {
console.log(res)
}, 0)
}
)
  1. 印出 begins
  2. 把 S1 區塊 callback 放到 Macrotask queue
  3. 執行 new Promise,印出 promise 2
  4. 把 S2 區塊 callback 放到 Macrotask queue
  5. call stack 空了,去執行 Macrotask queue 第一個任務,S1 區塊 callback,印出 setTimeout 1
  6. 把 P1 區塊 callback 放到 Microtask queue
  7. call stack 空了,去執行 Microtask queue,P1 區塊 callback,印出 promise 1
  8. call stack 空了,去執行 Macrotask queue 第二個任務,S2 區塊 callback,印出 setTimeout 2
  9. resolve 執行使得 .then 區塊 P2 區塊 callback 放到 Microtask queue
  10. call stack 空了,去執行 Microtask queue,P2 區塊 callback,印出 dot then 1
  11. 把 S3 區塊 callback 放到 Macrotask queue
  12. call stack 空了,去執行 Macrotask queue 最後一個任務,S3 區塊 callback,印出 resolve 1

console

begins
promise 2
setTimeout 1
promise 1
setTimeout 2
dot then 1
resolve 1

Reference


請說明瀏覽器中的事件循環 (Event Loop)

JS 原力覺醒 Day15 - Macrotask 與 MicroTask

Tasks, microtasks, queues and schedules

最常見的事件循環 (Event Loop) 面試題目彙整

Cookie and Session

· 2 min read

HTTP 的無狀態性質


HTTP 是無狀態的,無法記住每個 request 之間的關聯。如果需要這樣的聯繫,例如登入登出功能記住使用者有無登入過,可以實作 session 機制來達成。

Session


需要關聯的 request,代表我們需要在一段時間內保有某種狀態,這樣一段時間內的狀態稱為 session。 例如:登入就開始了一段 session,經過某秒之後系統自動把你登出,session 結束。

session 具有幾個特性:

  1. 每個 session 都有開始與結束
  2. 每個 session 都是相對短暫的
  3. 瀏覽器或伺服器任何一方都可以終止這個 session

要實作 session 機制有許多不同方法,而因為一些好特性,其中最常用的為 Cookie。


Cookie 是 HTTP 中,實作(建立) session 的一個工具(容器),本質上是儲存在瀏覽器裡面的小份資料。

是用 cookie 去實作 session 機制的方法。

流程

  1. 前端發 req (ex: /login)
  2. 後端在 res 加上 set-cookie header,裡面會放加密過的資料(使用者資料)
  3. 瀏覽器看到後,會自動在下次的 req 帶上 cookie

缺點是,cookie 只能資料容量較小,且加密內容內容有可能被破解

cookie 的常用屬性

img

#備註:同網域是指 same domain,不是 same origin

cookie 種類

  • session cookie: 沒有特別設置 expire, Max-age,關掉瀏覽器就會被清掉了的這種
  • persistent cookie: 有設置 expire, Max-age,到你設定的值之前都可以存活

session data

如果想要存大分一點的資料,且不希望被破解,可以把 session 存在後端。

流程

  1. 前端發 req (ex: /login)
  2. 後端在 res 加上 set-cookie header,裡面會放 session id (不是實際機敏資料,而是一個對應那份資料的代號)
  3. 瀏覽器看到後,把這個 session id 儲存起來,會自動在下次的 req 帶上 cookie
  4. 後端看到這個 session id,拿著他去跟 session store (如:redis) 裡面紀錄的資料比對,取的相對應的資料返回給前端

若要安全性高的登入機制一般都會採用這個手法

Reference


淺談 Session 與 Cookie:一起來讀 RFC

成為看起來很強的後端系列

AWS EC2 + LAMP 網站部屬

· 6 min read

Part1: 設置 AWS EC2 主機


  1. 註冊 AWS 帳戶

  2. 登入 as root user

  3. 進入 EC2 dashboard

    img

  4. 將主機位置設置在離台灣較近的東京,以增加連線速度

    img

  5. 建立主機

    img

  6. 選擇 Ubuntu LTS (Long Term Support) AMI (映像檔,可以把它理解為光碟)

img

  1. 選擇免費版,然後 Next

    img

  2. 三到五步驟可不用修改設定,直接 NEXT

  3. AWS 防火牆預設用 SHH 連線,從 22 port,所有 ip 都能連到。我們要用新增 HTTP/ HTTP 連線,然後 NEXT

    img

  4. 最終 Review。確認無誤就可以按下 Launch。

    img

  5. 這時會被要求選擇金鑰,如事先有創立過了就直接選擇之前的,若沒有就建立一個新的,再替他取一個任意名稱。最後按 "download Key Pair" 來將金鑰檔下載下來

img

  1. 再來就可以看見自己的主機已經順利跑起來了

img

Part2: 連線到主機、基礎設置


  1. 這邊就是我們建立的主機,勾選後按下連線

img

開啟 CLI, cd 到金鑰所在的工作路徑,利用 SSH 來連線到主機

(註:如果出現金鑰權限太高的警示就要跑 chmod 400 <金鑰檔> ,以確保金鑰無法公開檢視

img

看到 ubuntu@ip-xxx.... 就表示成功連線到遠端主機囉

top 指令可以查看主機狀態,q離開

img

  1. 更新作業系統。利用管理員權限 (sudo),透過 apt 這個作業系統的套件管理工具來更新

    $ sudo apt update && sudo apt upgrade && sudo apt dist-upgrade

Part3. LAMP server (Linux + Apache + MySQL + PHP/Perl/Python) 安裝


  1. 首先我們可以安裝 tasksel 來協助 LAMP 的快速建置 (當然一個個分別裝也是可以的)

    sudo apt install tasksel

  2. 再來就可以用安裝好的 taskel 來把 LMAP server 裝起來

    sudo tasksel install lamp-server

    網站的預設根目錄是在 /var/www/html,當用網頁打開 server 預設會顯示根目錄底下的 index.html。安裝好之後開啟瀏覽器,輸入 public IPv4 經該就能看見以下的 default 頁面,表示安裝成功

img

Part4. 資料庫系統設置


以下分別介紹 phpmyadmin 和 sequel pro 的設置方式

phpmyadmin

  1. 下載 phpmyadmin

    sudo apt install phpmyadmin

  2. configuring phpmyadmin: 選擇連接 apache2

  3. 完成設置密碼

  4. 設定 root 帳號來設定密碼用以管理 MySQL 資料庫

    • 進入 MySQL shell

    sudo mysql -u root mysql

    • 啟用 mysql_native_password 插件,開啟使用密碼及使用密碼登入的功能

    UPDATE user SET plugin='mysql_native_password' WHERE User='root';

    • 重新載入特權表

    FLUSH PRIVILEGES;

    • 設定密碼。這時可以選擇由簡單到複雜 (0~2) 的密碼。

    sudo mysql_secure_installation

  5. 設置完成後在瀏覽器輸入:public IPv4/phpmyadmin 能夠進到這樣的畫面就代表設置成功

img

除錯的部分

如果在密碼設置完之後還沒有辦法順利進入步驟五的畫面,而出現錯誤畫面,可以參考以下排除。

  1. 用 nano 打開 apache2 config 檔

    sudo nano /etc/apache2/apache2.conf

  2. 將以下段加入到檔案的最後面 (這邊可以開啟允許滑鼠滑動就能快一點滑到底部)

    Include /etc/phpmyadmin/apache.conf

  3. 儲存退出後就可以重啟 apache

    sudo /etc/init.d/apache2 restart

Sequel pro

依照上述步驟我們有了 root 帳號及密碼可供管理資料庫,若是對 phpmyadmin 有安全上的顧慮的人也可以參考以下 sequel pro 的使用。

但由於預設只能由本機登入 MySQL,我們必須再做點設定修改

  1. 以 phpmyadmin 登入進到管理頁面
  2. 點選對帳戶 root 作「編輯權限」
  3. 進入登入選項將主機名稱下拉式選單選到 「任意主機」,此時會看到後面的欄位變為 "%"
  4. 再回到使用者帳號一覽,就能看見多了一組使用者名稱同為 root ,而主機名稱為 "%" 的帳戶。這樣表示設置成功,即準備好能夠從其他軟體進行連線

Sequel pro 設置

  1. 到官方網站 下載 Sequel pro
  2. 新增 SSH 連線如下:

img

參考文章:

https://magiclen.org/lamp/

Connecting to your AWS EC2 database with Sequel Pro

學長姐筆記

Part5. 上傳檔案到 Ubuntu Server


使用 FileZilla

  1. 開啟「站台管理員」,點選「新增站台」來新增一個連線。協定為 SSH,主機填上 Public IPv4,最後選擇以金鑰檔案登入,底下選取金鑰存放路徑即可點選連線。

img

  1. 成功連線後進到 /var/www/html,這個路徑為 apache server 預設的根目錄,因此我們網站的檔案都需傳到這路徑底下以順利跑起來。這邊可以開一個專案資料夾來管理所有相關的檔案,或將現存的檔案直接拖曳到路徑下。

    (若使用已有專案,要注意 conn.php 當中的連線資訊是否無誤,以及其他串接後端 api 的檔案的路徑也要一併修改過)

img

到這邊我們的網站已經順利部屬到虛擬主機,也能如其運行,接下來的步驟將要購買網域名來和虛擬主機串連。

其他方法

  1. 如果專案有上傳到 GitHub 或 GitLab,可以直接在虛擬主機 clone 下來

  2. 利用 scp 指令把檔案傳輸到虛擬機上面

    # 複製目錄到遠端
    scp -r ./dist/* myuser@192.168.0.1:/path/file2

Part6. 串連網域和虛擬主機、網域設定

網域由 Gandi 購得後,可進到 「區域檔紀錄」來進行網域設定

設定 A (Address):我們的 domain 對應到的 IP (Public IPv4)

這個設定連結了 domain 和虛擬主機的 IP,如此以後想要瀏覽主機管理的網頁就不必在辛辛苦苦地打上一長串 IP,也更容易分享給親朋好友。

img

img

this binding object

· 3 min read

this


  1. 代表函式的綁定物件,通常在函式當中使用
  2. 特別需要留意的是: 在不同 程式脈絡之下,this 所代表的東西可能不同

幾種常見情況底下的 this


1. 獨立的函式 (function):

this 會指向全域物件,例如,瀏覽器中會指向 window

例外情況:

  • 使用嚴格模式,會是 undefined
function test() {
console.log(this)
}

2. 物件的方法 (Object method):

this 代表物件本身

let Obj {
x: 3,
test: function() {
console.log(this);
}
}

3. 事件處理函式 (eventListener):

this 代表觸發事件的對象物件

document.addEventListener('mouseover', function () {
console.log(this)
})

4. 建構函式 (constructor):

this 會是指向實體化的物件

function Point() {
console.log(this)
}
new Point()

5. Arrow function

arrow function 沒有自己的 的 this,他的 this 是依據語彙環境的父層區域(parent scope)來綁定。

白話的說,arrow function 定義位置(不是呼叫順序)的上一層 this 代表誰,arrow function 內的 this 就代表誰

另外,arrow function 的 this 與一般 function declaration 的 function 不同,是不能自己重新綁定 this

自定義、重新設定 「綁定物件」 的三種方法 - bind, call, apply


1. functionName.bind(新綁定物件)

使用 bind() 將函式的綁定物件自定義

回傳一個新的 function

let newFunction = oldFunctionName.bind(新的綁定物件)
/*
回傳function with 新的綁定物件
因為式回傳新的function所以務必要宣告新變數存起來
*/

2. functionName.apply (新綁定物件, [參數…])

是一種特殊的呼叫函式的方式,藉由傳入新的自定義榜定物件,以陣列的形式傳入呼叫時欲傳入的參數。

function add(a, b) {
return a + b
}
// 一般的函式呼叫
add(1, 2)
// 傳入 document 物件當作新的綁定物件,傳入 [4, 5] 當作參數來呼叫這個 add
add.apply(document, [4, 5])

3. functionName.call(新綁定物件, 參數…)

基本上用法跟 apply 一樣,只是說他傳參數的方式是直接傳入,不是放在 array 裡面傳入。

function add(a, b) {
return a + b
}
// 一般的函式呼叫
add(1, 2)
// 傳入 document 物件當作新的綁定物件,傳入 4, 5 當作參數來呼叫這個 add
add.call(document, 4, 5)

Reference