---
# System prepended metadata

title: JavaScript核心篇 - 同步語言、非同步行為、Promise
tags: [' Promise', ' 非同步行為', 同步語言]

---

---
title: JavaScript核心篇 - 同步語言、非同步行為、Promise
tags: 同步語言, 非同步行為, Promise
description:
---

JavaScript核心篇 -  同步語言、非同步行為、Promise
===

### 同步語言的概念

:::success
JavaScript是**同步語言**，其程式碼是**逐行執行**，由上而下的執行，執行完這一行再執行下一行。
:::

```javascript=
console.log('a');    // a
console.log('b');    // b
console.log('c');    // c
```

<br>

### 非同步的例子
:::success
JavaScript是**同步語言**，但又**有執行非同步**的能力。
:::

#### ex1：`console.log(1)~(4)`執行的順序為何？

- 一開始會執行**立即函式**的部分。所以先執行`console.log(1)`。

- `setTimeout(() => {}, 0)`是**非同步行為**。不管`setTimeout`延遲多久的時間，==只要是非同步行為就是**最後**才執行==。

- 接著執行**立即函式以外**的`console.log(4)`。

- 最後執行`setTimeout(() => {}, 0)`內的`console.log(2)`、立即函式的`console.log(3)`。

  #### 執行的順序：console.log(1)、(4)、(3)、(2)。

    ```javascript=
    (() => {
      console.log(1);

      setTimeout(() => {
        console.log(2);
        (() => {
          console.log(3);
        })();
      }, 0);

    })();

    console.log(4);
    ```

#### ex2：執行的順序為何？

- `setTimeout`非同步行為，所以**最後**執行。

- 先執行函式`getName()`，所以`console.log()`會先印出`小明`。

- 最後`setTimeout`的部分，`myName`賦予字串`小王`，再執行函式`getName()`，所以`console.log()`會先印出`小王`。

    ```javascript=
    function getName() {
      console.log(myName);
    };

    let myName = '小明';

    setTimeout(() => {
      myName = '小王';
      getName();
    }, 0);

    getName();
    ```

#### ex3：`setTimeout`所需花費的時間為100ms？

- JavaScript是**逐行執行**的。會先等`for`迴圈**執行完成**後，再回頭執行`setTimeout`的部分。

- 所以在迴圈**執行完之前**，就算`setTimeout`**延遲時間到**，==也不會立即執行作用域內的程式碼==。

  ![](https://i.imgur.com/VlD6URo.png =50%x)


    ```javascript=
    console.time('setTimeout時間');

    setTimeout(() => {
      console.timeEnd('setTimeout時間');
    }, 100);

    let id = 0;

    for (let i = 0; i < 100000000; i++) {
      id = parseInt(Math.random() * i);
    };
    ```    
<br>

### 認識逐行執行的 javaScript

#### 從範例了解==逐行執行==的JavaScript

```javascript=
console.time('執行時間');
function planA (){
  console.log('planA');
};

function planB() {
  console.log('planB');
};

function planC() {
  console.log('planC');
};

(function planOrder () {
  planA();
  planB();
  planC();
})();

console.timeEnd('執行時間');
```
#### 狀況一：把`planB`作用域加上`a`
  - 因為`a is not defined`，所以立即函式`planOrder`會中斷。
    ```javascript=
    function planB() {
      a;
      console.log('planB');
    };
    ```
    由此可知
    :::success
    JavaScript是逐行執行，執行過程中出錯，程式就會中斷。
    :::

<br>

#### 狀況二：把`planB`作用域加上`for`迴圈
- 因為立即函式`planOrder`作用域**逐行執行**，所以會等`planB`作用域的`for`迴圈結束後，再去執行`planC`。

- 所以`console.timeEnd('執行時間')`也會跟著變長。

  ![](https://i.imgur.com/MrPhkZ2.png =35%x)

    ```javascript=
    function planB() {
      console.log('planB');
      let id = 0;
      for (let i = 0; i < 100000000; i++) {
        id = parseInt(Math.random() * i);
      };
    };
    ```

<br>

#### 狀況三：把`planB`作用域的`console.log('planB')`外層加上`setTimeout`延遲500ms。請問立即函式`planOrder`內的執行順序會改變？

:::success
非同步行為，因為**非立即執行**。所以**非同步行為**先放置再==事件佇列==，等最後再執行。
:::

- 所以運行順序是：![](https://i.imgur.com/SMagFBS.png =30%x)

- 就算把`setTimeout`延遲**改0ms**，運行順序還是**不變**。

    ```javascript=
    function planB() {
      setTimeout(() => {
        console.log('planB');
      }, 500);
    };
    ```
    
<br>

### 執行堆疊（Call Stack）
:point_right: [執行堆疊](https://medium.com/魔鬼藏在程式細節裡/淺談-javascript-執行環境-2976b3eaf248)

- 在範例中第1、3、8、10、16、22、24、26、28行，加入`debugger`中斷點，
  觀察執行堆疊的變化。

- `function`沒有被呼叫，其**作用域的函式不會執行**。

- 範例主要是觀察**立即函式**`planOrder`**執行堆疊的變化**。


```javascript=
debugger
console.time('執行時間');
function planA (){
  debugger
  console.log('planA');
};

function planB() {
  debugger
  setTimeout(() => {
    debugger
    console.log('planB');
  }, 500);
};

function planC() {
  debugger
  console.log('planC');
};

(function planOrder () {
  debugger
  planA();
  debugger
  planB();
  debugger
  planC();
  debugger
})();
console.timeEnd('執行時間');
```

#### 執行堆疊的流程
1. 一開始在全域執行環境。
   ![](https://i.imgur.com/mEFMpdP.png)
   
3. 進入到`planOrder`

   ![](https://i.imgur.com/45vxaSk.png)

2. 一開始進入`planA`

   ![](https://i.imgur.com/jlAKKti.png)

3. 回到`planOrder`

   ![](https://i.imgur.com/uMlgLgw.png)

3. 再進入到`planB`

   ![](https://i.imgur.com/45vxaSk.png)
  
   此時`setTimeout`加入==事件佇列==
  
   ![](https://i.imgur.com/SSFzbdQ.png)


4. 回到`planOrder`

   ![](https://i.imgur.com/uMlgLgw.png)

5. 進入到`planC`
  
   ![](https://i.imgur.com/7lEB6Jy.png)

6. 回到`planOrder`

   ![](https://i.imgur.com/uMlgLgw.png)

7. **最後**執行`setTimeout`的部分（**async 非同步**）

   ![](https://i.imgur.com/atqOyLO.png)
   
![](https://i.imgur.com/IUp2fQw.png =40%x)

<br>

### setTimeOut 的作用
- JavaaScript**本身沒有**`setTimeout`的功能，JavaaScript把`setTimeout`交給瀏覽器。

- 呼叫`setTimeout`這個函式，JavaScript把**延遲計時**這件事交給瀏覽器處理。

- 瀏覽器會要求`setTimeout`**準備函式來接收**，這個函式就是`setTimeout`==裡面的callback function==。

-  `setTimeout`裡面會加上延遲時間，這個過程交給瀏覽器處理，JavaaScript是**逐行執行**，執行完就結束了。

- 瀏覽器處理完`setTimeout`後（**延遲計時**），把結果往回丟給`setTimeout`內的==callback function==去做處理。

    :::success
    JavaaScript執行`setTimeout`（非同步行為）時，先把`setTimeout`callback function放到**事件佇列**。

    ==（執行堆疊會進到`setTimeout`但不會執行內部callback function）==，

    等到JavaaScript逐行執行的**同步行為完成後**，**瀏覽器再呼叫**事件佇列的`setTimeout` **callback function**

    :::


  ![](https://i.imgur.com/FIrwrzj.png)
  
  <br>
  
### 為何要使用 promise

- `setTimeout`裡面的callback function，並延遲100ms，印出`這是非同步的函式`。

    ```javascript=
    function asyncFn () {
      setTimeout(() => {
        console.log('這是非同步的函式');
      }, 100);
    };

    asyncFn();
    ```

- 把`setTimeout`裡面的callback function**獨立出來**，**賦予到變數**上，符合==高階函式的寫法==。

    ```javascript=
    function asyncFn() {
      const fn = () => {
        console.log('這是一段非同步的函式');
      };
      setTimeout(fn, 100);
    };

    asyncFn();
    ```

- 把`setTimeout`裡面的callback function作為**參數代入**。

    ```javascript=
    function asyncFn(fn) {
      setTimeout(fn, 100);
    };

    asyncFn(() => {
      console.log('這是一段非同步的函式');
    });
    ```

- 現在也把`setTimeout()`裡面的延遲時間，拉出來作為參數。

    ```javascript=
    function asyncFn(fn, time) {
      setTimeout(fn, time);
    };

    asyncFn(() => {
      console.log('這是一段非同步的函式1');
    }, 100);

    asyncFn(() => {
      console.log('這是一段非同步的函式2');
    }, 300);

    asyncFn(() => {
      console.log('這是一段非同步的函式3');
    }, 100);
    ```
    可以看到執行印出的順序。
    
    ![](https://i.imgur.com/duElgN5.png)
    
    如果要把印出的順序，改成`這是一段非同步的函式1、2、3`**逐行執行**的方式。則要把呼叫`asyncfn()`的方式改成**巢狀**。
    
    ```javascript=
    function asyncFn(fn, time) {
      setTimeout(fn, time);
    };

    asyncFn(() => {
      console.log('這是一段非同步的函式1');
      asyncFn(() => {
        console.log('這是一段非同步的函式2');
        asyncFn(() => {
          console.log('這是一段非同步的函式3');
        }, 300);
      }, 600);
    }, 900);
    ```

<br>

### promise 是什麼?

:::success
Promise是函式、函式建構子、物件。
:::

```javascript=
console.dir(Promise);
```

![](https://i.imgur.com/TVwjFdK.png)

<br>

#### 建立自己的Promise函式

![](https://i.imgur.com/rvu4bMY.png =70%x)

- returnu 一個`new Promise`的實體（也可稱為函式、函式建構子、物件） ，裡面放入`callback Function`再代入兩個參數`resolve`、`reject`。

- Promise成功的話，`resolve`會用`then()`插入`callback Function`==接收資料==。 

- Promise失敗的話，`reject`會用`catch()`插入`callback Function`==接收資料==。 

- 在`new Promise`的實體加入`setTimeout`根據`promiseFn()`代入的參數`true`、`false`決定`then`或`catch`哪個能接收到資料。
```javascript=
function promiseFn (boolean) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (boolean) {
        resolve('成功');
      } else {
        reject('失敗');
      };
    }, 500);
  });
};

promiseFn(false)
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });
```

<br>

### 鏈接

:::success
以前非同步行為都用`callback Function`來接收。

如果**要求執行順序**的話，就用**巢狀**的撰寫方式來接收。

現在如果是用Promise，就可用**鏈接**的方式撰寫。
:::

- `promiseFn()`第一次執行之後，如果要鏈接下一個`promiseFn()`，在第一次**成功**執行的`.then()`的callback Fn作用域內，加上`return promiseFn()` 。

- 後續還需要鏈接下一個`promiseFn()`，以上一個例子類推。

```javascript=
function promisefn (boolean) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (boolean) {
        resolve('成功');
      } else {
        reject('失敗');
      }
    }, 500);
  });
};

promiseFn(true)
  .then((res) => {
    console.log('第一次', res); // 第一次執行
    return promiseFn(true);
  })
  .then((res) => {
    console.log('第二次', res); // 第二次執行
    return promiseFn(true);
  })
  .then((res) => {
    console.log('第三次', res); // 第三次執行
  });
```

#### 鏈接失敗

- 鏈接失敗會在**最後面**加上`.catch()`，用來==處理前面所有`.than()`的錯誤==。

- 在第4行**第二次**鏈接是**失敗的**，會直接跳到`.catch()`，裡面callback Fn的`console.log`會印出**失敗**。

- 第15行會**再次**`return promisefn(true)`，如果還是**鏈接失敗**瀏覽器console會顯示**Uncaught（in promise）失敗**

  ![](https://i.imgur.com/WOIfa4B.png =40%x)
  
  **通常不會再從`.catch()`裡面再接`.then()`**。

```javascript=
promisefn(true)
  .then((res) => {
    console.log('第一次', res); // 第一次執行
    return promisefn(false);
  })
  .then((res) => {
    console.log('第二次', res); // 第二次執行
    return promisefn(true);
  })
  .then((res) => {
    console.log('第三次', res); // 第三次執行
  })
  .catch((err) => {
    console.log(err);
    return promisefn(true);
  });
```

<br>

### axios的語法與取值

**axios**是**Promise**為基底。

```javascript=
console.dir(axios);
```
![](https://i.imgur.com/DSA1izW.png)

- axios本身是**函式**也是**物件**，用`get()`這個方法**取得遠端資料**。
(以random user API測試)

- 再用`.then()`的方式**接收成功回傳的資料**。

- 以及用`.catch()`的方式**接收失敗回傳的資料**。
```javascript=
axios.get('https://randomuser.me/ap/')
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  })
```

<br>

### 如何撰寫==連續性==請求(Request)

#### 連續性也可看做==依照順序==請求

**範例：**
在**random user** API用**seed**可以取得**相同一筆資料**，用==Promise鏈接==的手法，使用axios套件**連續取得兩筆**資料，第一筆是**隨機**取得，第二筆是用第一筆資料**seed**取得，最後印出兩筆資料的Email都是==相同的==。

- 先取得`.then()`接收的第一筆資料裡的email`res.data.results[0].email`。

  ![](https://i.imgur.com/OPAWlcu.png =55%x)

- 再用**物件解構**`const {seed} = res.data.info`得到第一筆資料的**seed**值。

  ![](https://i.imgur.com/XFTpt9u.png =50%x)

- 以**Promise鏈接**取得第二筆資料（要代入第一筆資料seed值）。
  
  `.get()`內，使用樣板字面值`https://randomuser.me/api/?seed=${seed值}`
  
  :point_right: [Random User文件](https://randomuser.me/documentation#seeds) 

  ![](https://i.imgur.com/k2s9lPC.png)

- 再取得`.then()`接收的第二筆資料裡的email`res.data.results[0].email`。

- 鏈接失敗會在最後面加上`.catch()`，用來處理前面所有`.than()`的錯誤，callback Fn內`console.log()`再印出錯誤資料。

```javascript=
axios.get('https://randomuser.me/api/')
  .then((res) => {
    console.log(res.data.results[0].email);
    const {seed} = res.data.info;
    return axios.get(`https://randomuser.me/api/?seed=${seed}`)
  })
  .then((res) => {
    console.log(res.data.results[0].email);
  })
  .catch((err) => {
    console.log(err);
  });
```

<br>

### 如何撰寫==同時發出==多個請求

- Promise的`.all()`方法，可**同時發出**多個請求。

- 在`.all()`方法裡代入**陣列**，**可放入多個axios請求**。 

- 同時發出多個請求，==接收資料也會收到多筆資料（陣列形式）==，接收資料同樣用`.then()`。
    :::success
    資料全部接收到才會執行`.then()`
    :::

  ![](https://i.imgur.com/aQn3yMt.png =85%x)

- **同時發出**多個請求，若==其中一個請求出錯==或==其中一個請求沒有回傳資料==，最後`.then()`接收的資料==也會全部出錯==。
  
  ![](https://i.imgur.com/MQ2mhMa.png =65%x)

```javascript=
Promise.all([
    axios.get('https://randomuser.me/api/'),
    axios.get('https://randomuser.me/ap/'),
    axios.get('https://randomuser.me/api/')
  ])
    .then((arrayRes) => {
      console.log(arrayRes);
    })
    .catch((err) => {
      console.dir(err);
    })
```

#### Promise.allSettled()

- Promise的`.allSettled()`方法，與`.all()`一樣，**也可同時**發出多個請求。

- 與`.all()`最大差異
    :::success
    **同時發出**多個請求，不管有沒有==請求出錯==或==請求沒有回傳資料==，最後`.then()`==都會接收到資料==。
    
    最後接收資料的資料（物件） ，每一筆都多了**status**的屬性，確認狀態。如果請求出錯**status**的屬性值會是`rejected`。
    
    :::
    ![](https://i.imgur.com/XUIwug4.png)


```javascript=
Promise.allSettled([
    axios.get('https://randomuser.me/api/'),
    axios.get('https://randomuser.me/ap/'),    // api位置錯誤
    axios.get('https://randomuser.me/api/')
  ])
    .then((arrayRes) => {
      console.log(arrayRes);
    })
```