--- title: FRONT-END-TOPIC Discussion tags: appworks school --- # FRONT-END-TOPIC Discussion [TOC] :::info **題目自選** Anna:1 Alex:2 Sunny:3 每題報告時間:10 - 20 分鐘 ::: ## 1. Compare var, let, and const from the aspect of scope & hoisting. ### **前言** > 在 ECMAScript 2015 (簡稱 ES2015。又稱 ECMAScript 6,簡稱 ES6) 之前,JavaScript 使用 var 來宣告一個變數,以「函式」作為其作用域 (function scope)。 > > 在 ES6 中引入了兩個新的語法 let 和 const,使我們得已宣告以「區塊」作為其作用域 (block scope)的變數。 ### **Scope (作用域 / 範疇 / 有效範圍)** :::spoiler 維基百科怎麼說: 在電腦程式設計中,作用域(scope,或譯作有效範圍)是名字(name)與實體(entity)的繫結(binding)保持有效的那部分電腦程式。 不同的程式語言可能有不同的作用域和名字解析。 而同一語言內也可能存在多種作用域,隨實體的類型變化而不同。 **作用域類別影響變數的繫結方式**,根據語言使用**靜態作用域**還是**動態作用域**變數的取值可能會有不同的結果。 ::: :::info 1. Scope 是編譯器 (Compiler) 或 JavaScript engine 藉由識別字名稱 (identifier name) 查找變數的一組規則。 2. 可以理解為:一個「變數 (Variable)」與其「被賦予的值 (values and expressions)」在程式中可以作用、被存取、成立的範圍 (可及與可視範圍)。 ::: > 🤔 編譯器 (Compiler) 怎麼理解程式碼? 編譯器會在程式執行前將程式碼由上到下逐行轉為電腦可懂的命令,再執行這個編譯後的結果。 * 編譯三步驟: 1. 語法基本單元化與語彙分析 (tokenizing/lexing):將字串解析成 token。 2. 剖析或稱語法分析(parsing)。 3. 產生目的程式碼 (code-generation)。 > 🤔 作用域 (Scope) 的功用是? * JavaScript engine:負責整個編譯過程並執行程式碼。 * Compiler:負責編譯三步驟「語法基本單元化與語彙分析、剖析或稱語法分析、產生目的程式碼」。 * Scope:負責維護變數清單。 ```javascript= var name = "Anna"; ``` 1. 在編譯的時候,Compiler 會先到 Scope 詢問變數 name 是否存在,若存在就宣告這個 name 變數。 2. 接著,在執行階段,JavaScript engine 先到 scope 詢問變數 name 是否存在,若存在就將 "Anna" 設定給它;若 name 不存在就報錯。 --- ### Scope 主要分成兩種類型 :::info * Lexical Scope (語彙 or 靜態作用域):JavaScript 與大多數的語言相同是採用 Lexiacal Scope。 * Dynamic Scope (動態作用域) ::: * #### **Lexical Scope (語彙作用域)** 語彙作用域是在語彙分析時期 (編譯三步驟的步驟一) 所定義的 scope。 :::warning 提醒:Scope 的劃分在我們撰寫程式時就決定好了,不是 JavaScript 的 Compiler 產生的! ::: > 🤔 試著區分有幾個 Scope? ```javascript= function foo(a) { var b = a * 2; function bar(c) { console.log(a, b, c); } bar(b * 3); } foo( 2 ); // 2 4 12 ``` :::spoiler 公布答案! ![3 個 scope](https://i.imgur.com/B9pf1Gh.png) ::: Lexical Scope 代表著 Execution Context 的包裹關係,被包裹在內層的變數無法被外層取用,但外層宣告的變數內層可以使用。 > Children scope 可以使用 Parent scope 定義的變數,但 Parent scope 不行使用 Children scope 的。 * #### **Lexical Environment (詞彙環境)** :::info Javascript 在決定外部參考環境的時候是以「詞彙環境 (Lexical Environment)」為準則的。 ::: > 詞彙環境 (Lexical Environment) 就是程式碼實際上、物理上,到底寫在哪裡。 * Example 1 ```javascript= var name = 'Anna'; a(); function a (){ var name = 'Sunny'; b(); } function b (){ console.log(name) } ``` :::spoiler 公布結果! ```javascript= // Anna ``` func a 和 func b 的外部參考環境,都是全域,不管它是在什麼位置被呼叫的。 而 func b 這個往外找的這個機制,就是所謂的**範圍鏈 (Scope Chain)**。 ::: --- * Example 2 ```javascript= let a = 10; function foo (){ console.log(a); } function bar (){ let a = 20; foo(); } bar(); // 10 ``` :::info Scope 是變數的取用範圍,而程式的詞彙環境 (Lexical Environment) 決定了這個範圍,Javascript 引擎會依著範圍鏈 (Scope chain) 尋找可取用的變數。 ::: > **Lexical Scope** vs. **Lexical Environment** Lexical Scope:是在程式編譯時就已經決定好的作用域。 Lexical Environment:是在程式執行中存放環境資訊的地方。 --- ### 變數作用域 (Variable Scope) * #### **Global Scope (全域作用域)** JavaScript 在編譯階段 (compilation) 的最初,會產生一個全域執行環境 (Global Execution Context) 以執行 JavaScript 程式。而在全域的執行環境中,會存在一個全域的變數物件 (Global Variable Object)。 只要是在全域環境中被宣告的變數 (或者函式),就會被存放在這個 Global Variable Object 內,並且可在整個程式的任何地方被存取。 這種變數 (top-level code),我們可以說它的作用域是全域 (Global-level Scope),也就是所謂的全域變數 (Global Variable),而在 JavaScript document 中只會有一個 Global scope。 ```javascript= // Global scope const name = "Anna" // 全域變數 console.log(name) // Anna const printName = () => { console.log(name) // 變數 name 可作用 } printName() // Anna ``` * #### **Local Scope (區域作用域)** 每一個函式 (function) 會自成一個作用域,在函式內宣告的變數則稱為區域變數 (Local Variable),表示此變數只能作用於所屬的函式中,無法於其他函式中被呼叫並執行。 Local scope 可以被分為 **Function-level Scope (函式作用域)** 和 **Block-level Scope (區塊作用域)**。 ```javascript= // Global scope function foo1(){ // Local scope 1 function foo2(){ // Local scope 2 } } ``` * #### **Function-level Scope (函式作用域)** 在函式內定義的任何變數 (Local variable),作用範圍只有在該函式內。 **var** 宣告的一個變數,便是以「函式」作為其作用域 (function scope)。 > 範例一: ```javascript= { var name = "Anna"; } console.log(name); // Anna ``` 以 var 宣告的變數,作用範圍只有在定義它的函式內,但範例一沒有函式,故 name 是一個 global variable。 範例一等同於在最開始 (global scope) 宣告了一個 global variable "name",如下: ```javascript= var name; { name = "Anna"; } console.log(name); // Anna ``` 此處的**大括號 { }** 「沒有」任何作用,就算不寫結果也會一樣。 表示用大括號將一個以 var 宣告的變數包起來,無法讓它變成一個 local variable。 大括號對於 var 無法形成一個作用域,**只有函式才可以**。 > 範例二:將 var 變成 local variable 的方法 ```javascript= function printName(){ var name = 'Anna'; console.log(name); } printName(); // Anna console.log(name); // ReferenceError: name is not defined ``` 若在一個函式內以 var 宣告變數,那這個變數就變成了一個 local variable,只能在函式內才看得到。 > 範例三: ```javascript= if (true) { var name = "Anna"; } console.log(name); // Anna for (var i = 0; i < 3; ++i) { console.log(i) // 0, 1, 2 } console.log(i); // 3 ``` name 和 i 都是 global variable,範例三等同於如下: ```javascript= var name; if (true) { name = "Anna"; } console.log(name); // Anna var i; for (i = 0; i < 3; ++i) { console.log(i) // 0, 1, 2 } console.log(i); // 3 ``` * #### **Block-level Scope (塊級 / 區塊作用域)** 變數的作用範圍只存在**兩個大括號 (curly brackets) { }** 中,也就是區塊內才能作用。 常見的語法包含:if, switch conditions 或是 for, while loops。 **let** 和 **const** 宣告的變數,便是以「區塊」作為其作用域 (block scope)。 > 範例一: ```javascript= { let name = "Anna"; console.log(name); // Anna } console.log(name); // undefined ``` > 範例二: ```javascript= if (true) { let name = "Anna"; } console.log(name); // undefined for (let i = 0; i < 3; i++) { console.log(i); // 0, 1, 2 } console.log(i); // undefined ``` --- #### 1. 比較 var, let, const 的作用域。 ```javascript= function printName(){ if(true){ var name1 = 'Anna'; // 存在於 function scope const name2 = 'Alex'; // 存在於 block scope let name3 = 'Sunny'; // 存在於 block scope } console.log(name1); console.log(name2); console.log(name3); } printName(); ``` :::spoiler 公布結果! ```javascript= // Result: // Anna // ReferenceError: name2 is not defined // ReferenceError: name3 is not defined ``` ::: #### 2. 比較「是否允許重複宣告」及「是否在使用之後才被宣告」。 :::info | 宣告方式 | var | let | const | |:--------------------:|:---:|:---:|:-----:| | 允許重複宣告 | ✅ | ❌ | ❌ | | 允許使用之後才被宣告 | ✅ | ❌ | ❌ | ::: > 🤔 是否允許重複宣告? * 以 let, const 宣告變數,同一個大括號裡面只能宣告一次。 ```javascript= let name = "Anna"; let name = "Alex"; // SyntaxError: 'name' has already been declared ``` * let 或 const 在同一個執行環境中不能重複宣告變數。 ```javascript= function printName(){ const name = "Anna"; const name = "Alex"; let name = "Sunny"; console.log(number) }; printName(); // SyntaxError: Identifier 'name' has already been declared ``` * 用 let 宣告變數可以不斷重新賦值。但用 const 宣告變數並賦值後,就無法變更。 ```javascript= let name = 'Anna' name = 'Alex' console.log(name) // Alex ``` * 以 var 宣告一個已經被宣告過的 var 變數時,新的宣告會被忽略,但是賦值的動作還是會被執行。 ```javascript= var name = "Anna"; var name = "Alex"; var name; console.log(name); ``` :::spoiler 公布結果! ```javascript= // Alex ``` 第二行的 **var name;** 宣告會被忽略。 但第二行 **name = "Alex";** 的動作還是會被執行。 第三行的 **var name;** 宣告會被忽略。 但第三行並沒有賦予一個新值,所以不會讓值變成 undefined。 ::: --- > 🤔 是否允許使用之後才被宣告? * 用 var 宣告的變數可以「先使用,再宣告」。只要有宣告 var 變數,不管先使用或是先宣告都可以。但若完全都沒有宣告,則會拋錯。 > 範例一: ```javascript= function printName() { name = "Anna"; console.log(name); var name; } printName(); // Anna ``` > 範例二: ```javascript= function printName() { name = "Anna"; if (false) { var name; } console.log(name); } printName(); // Anna ``` * let 或 const,一定要先宣告才能使用。 > 範例一 ```javascript= function printName() { name = "Anna"; console.log(name); const name; } printName(); // SyntaxError: Missing initializer in const declaration ``` > 範例二 ```javascript= function printName() { name = "Anna"; console.log(name); let name; } printName(); // ReferenceError: Cannot access 'name' before initialization ``` ### **Hoisting (提升)** :::info 1. 在執行任何程式碼前,JavaScript 會把函式宣告放進記憶體裡面,這樣做的優點是「可以在程式碼宣告該函式之前使用它」。 2. JavaScript **僅提升宣告的部分**,而不是初始化。如果在使用該變數後才宣告和初始化,那麼該值將是 undefined。 3. 在編譯完執行時有 Creation phase 和 Execution phase,**hoisting** 在 **Creation phase** 時作用。 ::: * #### 函式的 Hoisting 即使在函式被定義之前就先呼叫它,程式碼仍然可以運作,是出於 JavaScript 內文執行 (context execution) 的運作原理。 優點:可以解決兩個函式需要互相呼叫彼此的狀態(遞迴狀況)。 > 理想:先定義完一個函式以後再呼叫它 ```javascript= function printName() { console.log("Anna"); } printName(); // Anna ``` > 函式的 Hoisting 特性 ```javascript= printName(); // Anna function printName() { console.log("Anna"); } ``` --- * #### Hoisting 也適用於其他型別和變數。 在 JavaScript 中,不管在函式中的哪一行用 var 宣告變數,一律視為在函式的第一行宣告。 意即不論宣告 var 變數的位置在哪,「宣告的動作」一律都會被「抬升」到函式的最頂端,這個特性就叫做 hoisting (提升)。 > 範例一:下面的程式碼執行上完全相等 ```javascript= function printName() { var name; name = "Anna"; console.log(name); } printName() // Anna ``` ```javascript= function printName() { name = "Anna"; console.log(name); var name; } printName(); // Anna ``` :::danger **只有「宣告」這個動作有 hoisting (提升) 的特性。 「賦值」(把值指定給變數) 的動作沒有 hoisting。** ::: > 範例二 ```javascript= function printName() { console.log(name); var name = "Anna"; console.log(name); } ``` :::spoiler 公布結果! ```javascript= function printName() { console.log(name); // undefined var name = "Anna"; console.log(name); // Anna } ``` 第三行宣告 name 的動作會被提升 (hoist) 至函式的最開始,剛宣告完的 name 的值會是 **undefined**,因為**沒有賦予初始值**。 第二行的結果是 undefined,因為第三行 name = "Anna" **賦值的動作沒有提升 (hoist)**,而是在原本的位置(第三行),所以第三行執行完值會變成 "Anna"。 第四行,name 印出的結果值就會是 "Anna"。 > 依照上述邏輯,下方程式碼等同於範例二 ```javascript= function printName() { var name; console.log(name); // undefined name = "Anna"; console.log(name); // Anna } ``` ::: > 範例三 ```javascript= const xx = () =>{ let a = b = 3 ; } xx() console.log(b) // 3 console.log(a) // error, ReferenceError: a is not defined ``` :::spoiler 因為 Hoisting 所以可以改寫成... ```javascript= let b; const xx = () =>{ b= 3 let a = b; } xx() console.log(b) // 3 console.log(a) // error, ReferenceError: a is not defined ``` ::: --- * #### 用調用函式表達式 (IIFE, Immediately-Invoked Function Expressions) 模擬區塊作用域 (block-level scope)。 IIFE 就是宣告一個函式並把你要做的事包起來,然後馬上執行。 ```javascript= (function() { var name = "Anna"; console.log(name); // Anna })(); console.log(name); // undefined ``` 因為 name 被包在一個函式裡面,所以 name 不是一個 global variable,它的 scope 僅限於函式內部。 所以外面的 console.log() 才會是 undefined --- ### TDZ > 參考:https://eddychang.me/es6-tdz ```javascript= let x = 'outer value' ;(function() { // 這裡會產生 TDZ for x console.log(x) // TDZ期間存取,產生ReferenceError錯誤 let x = 'inner value' // 對x的宣告語句,這裡結束 TDZ for x })() ``` * 以 **let/const** 宣告的變數,的確 **也是有提升(hoist)** 的作用。 * 這個是很容易被誤解的地方,實際上以 let/const 宣告的變數也是會有提升(hoist)的作用。 * 提升是 JS 語言中對於變數宣告的基本特性,只是因為 TDZ 的作用,並不會像使用 var 來宣告變數,只是會得到 undefined 而已,現在則是會直接拋出ReferenceError錯誤,而且很明顯的這是一個在執行期間才會出現的錯誤。 :::warning Hoisting 的結果: * var:**undefined** * let/const:**ReferenceError error** (因為 TDZ) * this 沒有 hoisting ::: --- ### **Reference** * [Shubo 的程式教學筆記](https://shubo.io/javascript-hoisting/) * [JavaScript: Introduction to Scope (function scope, block scope)](https://dev.to/sandy8111112004/javascript-introduction-to-scope-function-scope-block-scope-d11) * [圖解變數作用域(Scope)](https://ithelp.ithome.com.tw/articles/10204622) * [你懂 JavaScript 嗎?#11 語彙範疇(Lexical Scope)](https://cythilya.github.io/2018/10/18/lexical-scope/) * [JavaScript - Lexical Scope](https://ithelp.ithome.com.tw/articles/10194745) * [提升(Hoisting)- MDN](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting) --- ## 2. Explain the mechanism of the ""event loop"" in browser. ### 前言 >**JavaScript is a single threaded language that can be non-blocking. >JavaScript 是單線程的程式語言,所有的程式碼片段都會在堆疊中(stack)被執行,而且一次只會執行一個程式碼片段(one thing at a time)。** **Event Loop 在非同步行為中扮演重要的機制** 為了要理解 JavaScript 之所以能夠透過非同步的方式(asynchronous)「看起來」一次處理很多事情,我們需要進一步瞭解 Event Loop。我們之所以可以在瀏覽器中同時(concurrently)處理多個事情,是因為瀏覽器並非只是一個 JavaScript Runtime。 :::info JavaScript 的執行時期(Runtime)一次只能做一件事,但瀏覽器提供了更多不同的 API 讓我們使用,進而讓我們可以透過 event loop 搭配非同步的方式同時處理多個事項。 ::: ### 同步與非同步的簡單說明 ![](https://i.imgur.com/z5SKfkS.jpg) ![](https://i.imgur.com/4J9zUtp.jpg) ### *Event Loop 的簡單說明:* ::: info 一般來說,在大多數瀏覽器中,每個瀏覽器都有一個事件循環,讓每個進程獨立,避免網頁無限循環或繁重處理阻塞整個瀏覽器。 ::: >[[Flavio Copes] The JavaScript Event Loop](https://flaviocopes.com/javascript-event-loop/) >In general, in most browsers there is an event loop for every browser tab, to make every process isolated and avoid a web page with infinite loops or heavy processing to block your entire browser. :::info Event Loop(事件循環)有一項簡單的工作——監視 Call Stack 和 Callback Queue。如果 Call Stack 為空,它將從Callback Queue中取出第一個事件並將其推送到 Call Stack 運行。 ::: >[[SessionStack] How JavaScript works: Event loop and the rise of Async programming](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5) >The Event Loop has one simple job — to monitor the Call Stack and the Callback Queue. If the Call Stack is empty, it will take the first event from the queue and will push it to the Call Stack, which effectively runs it. ### Event Loop 與它的好朋友們 ![](https://i.imgur.com/alXoEOJ.png) JavaScript and Event Loop (from: [SessionStack](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5)) ### Memory Heap :::info JavaScript V8 引擎內負責處理記憶體配置(memory allocate)的地方 ::: - #### 值的初始化 為了不讓開發者對配置感到困擾,JavaScript 會在宣告值的同時完成記憶體配置 ```javascript= var n = 123; // 配置一記憶體空間給數字 var s = 'azerty'; // 配置記憶體空間給字串 var o = { a: 1, b: null }; //配置記憶體空間給內含值的物件 // (像物件一樣) 分配記憶體給一個陣列 // 該陣列包含一些值 var a = [1, null, 'abra']; function f(a) { return a + 2; } // 分配記憶體給一個函式 (一個可呼叫的物件) // function expressions also allocate an object someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false); Copy to Clipboard ``` - #### 藉由函式呼叫來配置 有些函式呼叫後產生物件配置。 ```javascript= var d = new Date(); // 配置一個日期物件 var e = document.createElement('div'); // 配置一個 DOM 物件 ``` - #### 值的使用 基本上使用值表示對已被配置的記憶體做讀寫。可藉由讀取或寫入變數的值或一個物件特性或甚至傳一個參數到函數中來完成此事。 - #### 釋放不再使用的記憶體 **若超過 memory heap 的上限會有 memory leak 的現象**,當我們談論到記憶體管理,問題通常出現在這個階段。最困難的工作是尋找「已不再被使用的記憶體配置空間」。當我們談論到記憶體管理,問題通常出現在這個階段。最困難的工作是尋找「已不再被使用的記憶體配置空間」。 高階的語言 (e.g. JavaScript) 有一個叫作垃圾回收器(garbage collector) 的系統,他的工作是追蹤記憶體分配的使用情況,以便自動釋放一些不再使用的記憶體空間。但這個垃圾回收器只是「儘量」做到自動釋放記憶體空間,因為判斷記憶體空間是否要繼續使用,這件事是「不可判定(undecidable)」的(不能用演算法來解決)。 ### Call Stack :::info **在 JavaScript 中的執行堆疊(called stack)會讀取、執行並記錄目前執行到程式的哪個部分。** ::: > 如果進入了某一個函式(step into),便把這個函式添加到堆疊(stack)當中的最上方;如果在函式中執行了 return ,則會將此函式從堆疊(stack)的最上方中抽離(pop off)。 ```javascript= function multiply(a, b) { return a * b } function square(n) { return multiply(n, n) } function printSquare(n) { let squared = square(n) console.log(squared) } printSquare(4) ``` 當我們在執行 JavaScript 的函式時,首先進入 stack 的會是這個檔案中全域環境的程式(這裡稱作 main);接著 printSquare 會被呼叫(invoke)因此進入堆疊(stack)的最上方;在 printSquare 中會呼叫 square() 因此 square() 會進入堆疊(stack)的最上方;同樣的,square 中呼叫了 multiply(),因此 multiply 進入堆疊的最上方。 因此目前的執行的堆疊(call stack)會長的像這樣: 接著執行到每一個函式中的 return 或結尾時,這個函式便會跳離(pop off)堆疊。 <br> <iframe width="838" height="501" src="https://www.youtube.com/embed/2ZH_1d8TYVg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <br> #### 什麼是 Stack Overflow? 下圖為一個簡單的recursion範例: ```javascript= function foo() { foo(); } foo(); ``` ![](https://i.imgur.com/i9yCeDq.png) #### 阻塞(blocking) :::info 當執行程式碼片段需要等待很長一段時間,或好像「卡住」的這種現象,被稱作 阻塞(blocking) ::: 假設請求資料的 AJAX Request 變成同步(Synchronous)處理的話,那麼每 request 一次,因為必需等這個函式執行完畢從堆疊(stack)中跳離開後才能往下繼續走,進而導致阻塞的情形產生 - #### 阻塞的情形導致瀏覽器停滯 <iframe width="838" height="501" src="https://www.youtube.com/embed/FyUP7GFCm38" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 從上面的影片中可以看出當堆疊中有未處理完的函式導致阻塞產生時(以同步的方式模擬發出一個 request 但尚未回應前),我們沒辦法在瀏覽器執行其他任何動作,瀏覽器也無法重新渲染(click 的 button 一直處於被按壓的狀態);必須要等到 request 執行結束後瀏覽器才會繼續運作。堆疊被阻塞(stack blocked)的情況會導致我們的瀏覽器無法繼續渲染頁面,導致頁面「停滯」。 ### Web APIs :::info 瀏覽器提供了很多不同的 API(讓我們能夠同時(concurrently)處理多項任務。當完成 Web APIs 的內部函式後(如 setTimeout()),便將任務傳遞至工作佇列。 ::: >DOM(document) AJAX(XMLHttpRequest) Timeout(setTimeout) ### Callback Queue :::info 這是一個**先進先出**( FIFO = First In, First Out)的工作佇列(callback queue) 。會接收從 Web APIs 來的任務,並透過 Event Loop 的監控,當堆疊中沒有執行項目時,便把佇列中的內容拉進堆疊中執行。 ::: ### 非同步處理與堆疊(Async Callback & Call Stack) 為了要解決阻塞的問題,我們可以透過非同步(Asynchronous)的方式搭配 callback ,在這裡我們以 setTimeout 來模擬非同步請求的進行,以下面的程式碼為例,先思考會如何執行: ```javascript= console.log('hi') setTimeout(function () { console.log('there') }, 5000) console.log('JSConfEU') ``` :::success 1. 'Hi' -> 'there' -> 'JSConfEU' 2. 'Hi' -> 'JSConfEU' -> 'there' ::: :::spoiler 公布答案! The Answer is : 2. 'Hi' -> 'JSConfEU' -> 'there' ::: ### Concurrency and Event Loop 在下面的影片中,我們可以看到當我們在堆疊中執行 setTimeout 這個 function 時,**setTimeout 實際上是一個瀏覽器提供的 API ,而不是 JS 引擎本身的功能**;於是瀏覽器提供一個計時器給我們使用, **setTimeout 中的 callback function會被放到 WebAPIs 中,這時候,setTimeout 這個 function 就已經執行結束,並從堆疊中脫離。** 當計時器的時間到時,會把要執行的 cb 放到一個叫做工作佇列(task queue)的地方。 這時候就輪到事件循環(event loop)的功能,它的作用很簡單—如果堆疊(stack)是空的,它便把佇列(queue)中的第一個項目放到堆疊當中;堆疊(stack)便會去執行這個項目。 <br> <iframe width="838" height="501" src="https://www.youtube.com/embed/N0Au8yc5IOw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <br> :::info event loop 的作用是去監控堆疊(call stack)和工作佇列(task queue),當堆疊當中沒有執行項目的時候,便把佇列中的內容拉到堆疊中去執行。 ::: ### setTimeout 0 的情況? 那麼如果我們使用 setTimeout 並且希望在 0 秒後馬上執行是什麼意思呢? ```javascript= console.log('hi') setTimeout(function () { console.log('there') }, 0) console.log('JSConfEU') ``` <br> <iframe width="838" height="501" src="https://www.youtube.com/embed/IoBc3Vy3s3I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <br> 如同剛剛的邏輯,即使我們使用 0 秒,它一樣會先將 cb 放到 WebAPIs 的計時器中,當時間到時,把該 cb 放到工作佇列(task queue)內,「等到」所有堆疊的內容都被清空後才會「立即」執行這個 cb。 也就是說會依序 console 出來的內容是 hi --> JSConfEU --> there , there 會是最後才輸入的。 #### 把同樣的觀念套到 AJAX request 同樣的道理,我們把剛剛利用 setTimout 來模擬非同步處理的 code,改成發出 AJAX Request: ```javascript= console.log('hi') $.get('url', function cb(data) { console.log(data) }) console.log('JSConfEU') ``` 和 setTimeout 一樣,AJAX Request 的功能並不在 JavaScript 引擎中,而是屬於 Web APIs(XHR)。當執行 AJAX 請求的時候,cb 的內容會被放在 Web APIs 中,因此原本的堆疊(stack)將可以繼續往下執行。 直到 AJAX Request 給予回應後(不論成功或失敗),便把 cb 放到工作佇列(queue)當中,當堆疊(stack)被清空的時候,event loop 便會把 cb 拉到堆疊中加以執行。 ### 自己玩玩看🥳 JSConfEU 講者 Philip Roberts 自己寫了一個方便視覺化瞭解 JavaScript Runtime, call stack, event loop, task queue 的工具 [Loupe](http://latentflip.com/loupe/) ,你可以把下面更多不同的例子,貼到他所提供的網站中執行: - #### Click Event 假設我們的程式碼長這樣,我們可以思考一下它會怎麼執行: ```javascript= console.log('Started') $.on('button', 'click', function onClick () { console.log('Clicked') }) setTimeout(function onTimeout () { console.log('Timeout Finished') }, 5000) console.log('Done') ``` 邏輯是一樣的。 首先 Started 會被放到堆疊中執行,再來 click 和 setTimeout 這兩個都是屬於 WebAPIs,當他們被放到 WebAPIs 後,就會從堆疊中脫離,最後執行 Done。 當 setTimeout 的 Timer 時間到時,或者是 click 事件被觸發時,WebAPIs 會將 cb 放到工作佇列中(task queue),當堆疊空掉的時候,event loop 就會把工作佇列中的內容搬到堆疊(stack)中加以執行。 因此,當我們點擊瀏覽器時,這個點擊事件的 cb 並不是立即被執行的,而是先被放到工作佇列(queue)中,直到堆疊(stack)空了之後,才會被執行: - #### Multiple setTimeout 一樣的邏輯,讓我們看下面這段程式碼,思考一下它會怎麼執行: ```javascript= setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) ``` 從運作的過程中我們便可以瞭解到,setTimeout 這個 timer 只能保證超過幾毫秒後 「即將會」 執行,但並不能保證在幾毫秒後會 「即刻」 執行。舉例來說,setTimeout(cb, 1000),表示在 1 秒後這個 function 「即將會」 被執行,但並不是說在 1 秒後這個 function 「立即會」 被執行。 <br> <iframe width="838" height="501" src="https://www.youtube.com/embed/VYeSpNVn6TQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <br> ### 結論 #### 為什麼學 Web Dev 需要知道 Event Loop? 究竟 Event Loop 好用在哪?為何網路應用程式的開發者(Web App Developer)必須認識 Event Loop 呢?主要原因有兩個: 1. 由於Event Loop Non-blocking的特性,協助了**非同步請求的實現,並產生連貫的畫面呈現(influent UI)。** 2. Event Loop 將「費時較久」或「須等待事件才能啟動」的任務往後安排,因而能打造**流暢的使用者體驗(Outstanding UX):** ### 參考資源 - [到底 Event Loop 關我啥事?. 初學者也能懂「為什麼 JavaScript 中存在事件循環(Event Loop)?」 | by 郭耿綸 Kaleb | 無限賽局玩家 Infinite Gamer | Publication](https://medium.com/infinitegamer/why-event-loop-exist-e8ac9d287044) - [[筆記] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)](https://pjchender.blogspot.com/2017/08/javascript-learn-event-loop-stack-queue.html?m=1&fbclid=IwAR32OM7pJOEH1PmG7YTYkeCxJzsnaM5VdeLgyd6LVliZ64tZV0mpP3C6My4) - [How Does Javascript Work? - Andrei Neagoie ](https://youtu.be/hGSHfObcVf4) - [所以說event loop到底是什麼玩意兒?| Philip Roberts | JSConf EU](https://www.youtube.com/watch?v=8aGhZQkoFbQ) --- ## 3. Introduce RESTful API. ### 什麼是 API ? :::info 全名叫做 Application Programming Interface,維基百科上的中文翻譯是:「應用程式介面」。 ::: 一般來說當我們提到 API,會是這樣子的場景: >你 API 串好了沒?你還沒串的話資料拿不到欸!!! > >我要來串 Google 登入的 API,讓我的網站可以用 Google 登入!!! > >請提供一個空房資訊的 API,我們才能顯示在網頁上面…!!! 從以上對話可以看出 API 背後隱含了 **「交換資訊」** 的目的。 需要串 API,就代表你需要別人的資料,或者是別人需要你的 **資料**。 換句話說,如果你今天一直是一個人單打獨鬥,自己做自己的,基本上就不太會有 API 這種事情。需要串 API,就代表你需要別人的資料,或者是別人需要你的資料。 這邊特別把 **「資料」** 兩個字 highlight 起來,就是因為 API 基本上只牽涉到資料的交換,這是很重要的一部分。 簡單來說,**API 就是個拿來交換資料的東西**。 「串 API」的時候,背後指的就是:「我要你的資料」或是「你要我的資料」,不過講資料其實有點侷限,更好的說法是:「我要用你的某個功能」或是「我要讓你用我的某個功能」。 **API 需要提供什麼?** 第一個就是網址,代表說:「這個 API 在哪裡」,第二個則是文件,告訴對方這個 API 應該如何使用。 :::danger API 在哪裡(網址)、API 怎麼用(文件),這就是不可或缺的兩大主角。 ::: ### 什麼是 CRUD? :::info CRUD 是 Create、Read、Update、Delete,對應到中文的新增、查詢、修改、刪除。 ::: ### RESTful API 是什麼? 來來看是誰發明的 ↓ >Roy Thomas Fielding,他是 HTTP 協議(1.0 版和 1.1 版)的主要設計者、Apache 服務器軟件的作者之一、Apache 基金會的第一任主席,REST 這個詞是他在 2000 年的博士論文中提出的。 :::info RESTful 它不是一個協定,只是一種「風格」/ 「設計規範」。 一種設計 API 架構的風格("建議" 使用的風格)。 ::: REST,全名 Representational State Transfer (表現層狀態轉移),他是一種設計風格,RESTful 只是轉為形容詞,像是 peace 和平這名詞,轉成形容詞是 peaceful,RESTful 則形容以此規範設計的 API,稱為 RESTful API。 RESTful API 是一種設計規範,並沒有規定 Web API 都必須使用 RESTful 風格,而是比較常被使用而已。畢竟前後端都使用同一種風格,這樣就不用花太多時間猜測怎麼使用。(當然更好的做法是做出詳細的 API 文件。) Representational State Transfer 表現層狀態轉換 * Representational:表現形式,格式,如 JSON,XML … * State Transfer:狀態變化,如 HTTP 動詞 * URL 定位資源,用 HTTP Method 動詞(GET,POST,DELETE…)描述操作 ### 為什麼我們要使用 Restful API? 用一般的 API 不行嗎? 一般的 API,可能會長這樣,可自行決定是否使用,但會有一些缺點: 比如:不同公司,不一樣的工程師,設計的名稱都會不一樣,沒有統一的命名方式,造成在引用各家 API 時,都需要詳讀 API 文件,理解所有設計命名規則後,才可使用。 ![](https://i.imgur.com/KEWD2SY.png) RESTful API 想成是建立在 HTTP 協定之上的設計模式,充分利用 http 協定的特性。 用一個唯一的 URL 定位資源,將動作藏在 HTTP 的 method 裡面。 所以使用 RESTful 風格設計的 API,就有了以下優點: :::info 有唯一的 URL 表示資源位置,統一的 API 接口。 (Uniform Interface) ::: ![](https://i.imgur.com/czNof9w.png) 用白話來說,以餐廳服務生為例,如果使用一般的 API 點菜,我要加點、查看已點菜色、修改已點菜色、取消已點菜色,都需要不同的服務生替我服務,可能加點的服務生是 A、查看已點菜色是 B,RESTful API,就是讓這些動作,都可以由同一位服務生完成。 :::danger 只需要一個接口就可以完成。 ::: ### 網址要長怎樣? 首先要決定這個 API 放哪裡。通常如果我們要架設新的服務,例如 kangaroo.noob.tw 的話,我們的 API 網址可能會是: ![](https://i.imgur.com/IzgHcCB.png) 另外為了方便未來 API 改版時不同版本的 Client 可以同時使用,會習慣在 URL 後面加上 API 版本: ![](https://i.imgur.com/jNwNN3B.png) ### Endpoints 決定了 API 的網址後,要先規劃一下 API 大概會分幾個部分。假設我們要做一個簡單的部落格系統,可能會分成 使用者 部分和 文章 的部分。Endpoints 可以說是 RESTful 的「名詞」。 那使用者的部分可能就會放在 /user 底下、文章的部分可能會放在 /post 底下。原則上這些名詞不使用大寫,若是由多個字組成,通常會使用底線(_)隔開。 ### HTTP Method ![](https://i.imgur.com/9UDJv8k.png) RESTful API 使用了 HTTP Method 來當作「動詞」。這些動詞分別代表幾個意思: ![](https://i.imgur.com/lHhqOGl.png) :::info * POST:新增 * GET:讀取 * PUT:修改(修改整份文件) * PATCH:修改(修改其中幾個欄位) * DELETE:刪除 ::: >透過這些動詞,我們就可以做不同的動作。像是新增使用者會使用 POST /user 而不會是 POST /user/new。 > 一般的 API: ![](https://i.imgur.com/ZHI6xCO.png) 使用 **RESTful API**: ![](https://i.imgur.com/taeZGzR.png) => 不同的 Method 就是對同一件事情做不同的操作。 ### REST 有以下設計要點: :::info * 統一介面 * 對資源的 增/刪/改/查,分別用 HTTP Method POST/DELETE/PUT/GET 對應 * 通過 URL 操作資源,改變資源狀態 * 無狀態 (stateless) ::: 1. **統一介面** 也就是說他只有一個接口,可以看到上述了例子 /api/files/,所有的動作僅透過唯一的 /api/files/ 進行。然而就不會像早期 API 有多的接口卻都在對同一個地方進行動作。 另外,通常在命名 URL 時都會使用 名詞,像範例中 files 或 Users 等等,不會出現動詞型態,如 get_files,因為操作上會使用 HTTP 中的動詞。 這樣不僅省時省力,設計人員也能方便閱讀、提高效率。 2. **對資源的「增/刪/改/查」,分別用「HTTP Method POST/DELETE/PUT/GET」對應** REST 風格所有的東西皆都在 HTTP 協議之下完成,所有動作(動詞)都會對應到 HTTP 當中 POST/DELETE/PUT/GET。這樣一來在設計時,就能避免多次上述所說的多 URL 卻都在對同一個地方進行動作等等問題。 常見的 Method: * POST:新增 * GET:讀取 * PUT:修改(修改整份文件) * PATCH:修改(修改其中幾個欄位) * DELETE:刪除 3. **通過 URL 操作資源,改變資源狀態** 簡單說就是我對 /api/files/ 進行 post (/put/delete等) 資料的動作。 4. **無狀態** (stateless) 就是說 Server 端不會存取 client 端之間的任何動作。 每次從客戶端(Client)對伺服器(Server)發出的請求都是獨立的 — 這一次的請求無法得知上一次請求的內容與資訊。 1. 不會記住之前的連線 2. 每個 request 都被視為是唯一且獨立的 RESTful 的狀態,意即 HTTP 的請求狀態,一般 Web 服務中,Server 端和 Client 端交互的資訊,會存在 Server 端的 Session (例如:已登入狀態),在 Client 端再次發送請求的時候,Server 端透過保存在 Server 端的 Session,去執行 request。 無狀態的意思,即 Client 端自行保存狀態,在請求 Server 的時候,一併附上給 Server 端,Server 端無保存 Client 端的狀態資訊。 使用者登入後,生成驗證信息 token,該 token 標記該用戶已經登陸的狀態,Server 端不保存該 token 信息,而是 token 返回給客戶端。 當使用者發生下一次的請求,使用者端把該 token 重新發送給 Server 端,於是 Server 端就根據該 token 知道用戶已經登陸的狀態。 > 具體實現是:第一次登入,token 信息在資料庫端緩存下來,第二次請求時候從資料庫緩存查詢該用戶 token。這樣使用者狀態的信息就不需要保存在 Server 端,保存了在資料庫中。 舉一個白話一點的例子:查詢員工工資: 第一步:登錄系統。 第二步:進入查詢工資的頁面。 第三步:搜索該員工。 第四步:點擊姓名查看工資。 這樣的操作流程就是**有狀態的**,查詢工資的每一個步驟都依賴於前一個步驟,**只要前置操作不成功,後續操作就無法執行**。 **如果輸入一個 URL 就可以直接得到指定員工的工資**,這種情況就是**無狀態的**,因為獲取工資不依賴於其他資源或狀態,這種情況下,員工工資是一個資源,由一個 URL 與之對應可以通過 HTTP 中的 GET 方法得到資源,這就是典型的 RESTful 風格。 :::info 所以照著 REST 的風格特點所設計出來的 API 就是RESTful 囉 ::: ### API 回傳資料 回傳資料時,也要注意幾件事情。首先由於 API 大多由 Ajax 的方式去呼叫,也就是說 Client 端(如網頁)是在背景再乎叫一次這個 API,得到資料後再顯示在前景,所以要回應不同的狀態讓 Client 知道狀況再去決定要幹麻,設計 API 時不要轉址。 而資料格式我們會採用 XML 或 JSON 等格式,不會使用純文字。 除了善用 HTTP Method 以外,API 也應該優先採用 HTTP Status Code 來表達狀態,而不是採用 {success: true} 或 {result: ok} 的狀態來回傳。原則上成功的話使用 2XX、失敗看情況,如果是使用者做錯就使用 4XX、伺服器出錯就使用 5XX,以下是幾個常用的狀態: * 200 OK:成功。通常我用在查詢(GET)的部分。 * 201 Created:資源新增成功。通常我用在新增(POST)的部分。 * 202 Accepted:請求已接受,但還在處理中。(換句話說可能失敗) * 204 No Content:請求成功,沒有返回內容。通常我用在刪除(DELETE)或修改(PUT)的部分。 * 也有人會把修改成功放在 201。 * 400 Bad Request:使用者做錯的通用狀態。通常我用在有必填欄位未填或資料錯誤的狀況。 * 401 Unauthorized:使用者沒有進行驗證。 * 403 Forbidden:使用者已經登入,但沒有權限進行操作。 * 404 Not Found:請求的資源不存在。 * 410 Gone:資源已經過期。 * 500 Internal Server Error:伺服器端錯誤。 * 502 Bad Gateway:通常是伺服器的某個服務沒有正確執行。 * 503 Service Unavailable:伺服器臨時維護或是快掛了,暫時無法處理請求。 ### 設計 API 文件 假設要寫簡單的文章系統: 大概想一下會有四支東西:新增文章、編輯文章、查看文章、刪除文章。對應到的 HTTP Method 分別是:POST、PUT、GET、DELETE。 再來決定一個文章會有多少欄位:大概想一想應該會有標題、內容、時間、作者,就先簡單四個就好。 所以新增文章應該除了時間以外三個欄位都要送,沒送的話就顯示錯誤(400 錯誤)、編輯亦同,多送一個文章 ID。讀取的時候只需要送文章 ID 就好,刪除的時候也只要送一個文章 ID 就好。 大概整理下來 API 文件可能會長這樣: ![](https://i.imgur.com/CZPEq76.png) ### !!! 優良範本在此 !!! [STYLiSH-API-Doc](https://github.com/AppWorks-School/API-Doc/tree/master/Stylish) ![](https://i.imgur.com/ye1SH2x.png) ### RESTful API 網站 1. [Reqres - A hosted REST-API ready to respond to your AJAX](https://reqres.in/): 一個提供 API 測試的網站 2. [The RESTful Pokémon API](https://pokeapi.co/) ### 結語 RESTful API 是一種設計風格,這種風格使 API 設計具有整體一致性,易於維護、擴展,並且充份利用 HTTP 協定的特點。列出一些較重要的部份,在最短時間對它有個初步了解。 May your API REST. (誤) ### 參考資源 * [API 是什麼? RESTful API 又是什麼?](https://medium.com/itsems-frontend/api-%E6%98%AF%E4%BB%80%E9%BA%BC-restful-api-%E5%8F%88%E6%98%AF%E4%BB%80%E9%BA%BC-a001a85ab638) * [API 實作(一):規劃 RESTful API 要注意什麼](https://noob.tw/restful-api/) * [API 實作(二):實作 RESTful API](https://noob.tw/koa-api/) * [從拉麵店的販賣機理解什麼是 API](https://hulitw.medium.com/ramen-and-api-6238437dc544) * [認識 Restful API](https://www.youtube.com/watch?v=gHCB0sd47Is) * [【Web Service】何謂 REST 表現層狀態轉換?](https://spicyboyd.blogspot.com/2018/10/web-service-rest.html) * [API 基礎 - RESTful API、JSON、curl 指令.. API 是什麼?](https://yakimhsu.com/project/project_w4_Network_API.html) * [Guidelines & Best Practices for Design RESTful API](https://bytenbit.com/best-guidelines-design-restful-api/) Stateless vs stateful [Stateful 與 Stateless](https://dotblogs.com.tw/jimmyyu/2010/10/16/difference-between-stateful-and-stateless)