手把手帶你弄懂JavaScript中的非同步程式設計

2021-06-03 13:00:52
本篇文章帶大家瞭解JavaScript中的非同步程式設計。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有所幫助。

非同步,就是非同步....

這節內容可能會有點枯燥,但是卻是 JavaScript 中非常重要的概念,非常有必要去學習。

目的

  • 提升開發效率,編寫易維護的程式碼

引子問題

  • 請求時候為什麼頁面卡死??
$.ajax({
  url: "www.xx.com/api",
  async: false, // true
  success: function(result) {
    console.log(result);
  },
});
  • 為什麼資料更新了,DOM 卻沒有更新??
// 非同步批次更新DOM(vue-nextTick)
// <p id="app">{{num}}</p>
new Vue({
  el: "#app",
  data: {
    num: 0,
  },
  mounted() {
    let dom = document.getElementById("app");
    while (this.num !== 100) {
      this.num++;
    }
    console.log("Vue num=" + this.num, "DOM num=" + dom.innerHTML);
    // Vue num=100,DOM num=0
    // nextTick or setTimeout
  },
});

產生非同步的原因

原因:單執行緒(一個時間點,只做一件事),瀏覽器的 JS 引擎是單執行緒導致的。

單執行緒是指在 JS 引擎中負責解釋和執行 IavaScript 程式碼的執行緒只有一個,不妨叫它主執行緒。

所謂單執行緒,就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成再執行後面一個任務。

先看看一下瀏覽器核心的執行緒圖:

1.jpg

其中,渲染執行緒和 JS 執行緒互斥

假設有兩個函數,一個修改一個刪除,同時操作一個 DOM 節點,假如有多個執行緒的話,兩個執行緒一起執行,肯定就死鎖了,就會有問題。

為什麼 JS 要設計為單執行緒,因為瀏覽器的特殊環境。

單執行緒的優缺點:

這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段 Javascript 程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。

常見的堵塞(死迴圈):

while (true) {}

JS 在設計之初就以執行在瀏覽器中的指令碼語言,所以也不想搞得這麼複雜,就設計成了單執行緒,也就是,一個時間點,只能做一件事。

為了解決單執行緒堵塞這個缺點:產生了非同步。

拿吃泡麵舉例:

  • 同步:買泡麵=》燒水(盯著)=》煮麵=》吃泡麵
  • 非同步:買泡麵=》燒水(水開了熱水壺響-回撥)=》看電視=》煮麵(面好了熱水壺響-回撥)=》看電視=》熟了叫我=》吃泡麵

看電視就是非同步操作,熱水壺響,就是回撥函數。

非同步程式設計

JS 中大多的程式碼都是同步執行的,只有極個別的函數是非同步執行的,非同步執行的程式碼,則需要非同步程式設計。

非同步程式碼

setTimeout(() => {
  console.log("log2");
}, 0);
console.log("log1");
// ?? log1 log2

非同步程式碼的特點:不是立即執行,而是需要等待,在未來的某一個時間點執行。

同步程式碼非同步程式碼
<script>程式碼網路請求(Ajax)
I/O 操作定時器(setTimeout、setInterval)
渲染操作Promise(then)

async/await

回撥函數

非同步程式碼最常見的寫法就是使用回撥函數。

  • HTTP 網路請求(請求成功、識別後執行 xx 操作)
  • DOM 事件繫結機制(使用者觸發事件後執行 xx 操作)
  • 定時器(setTimeout、setInterval)(在達到設定時間後執行 xx 操作)
// 注意到click方法中是一個函數而不是一個變數
// 它就是回撥函數
$("#btn_1").click(function() {
  alert("Btn 1 Clicked");
});
// 或者
function click() {
  // 它就是回撥函數
  alert("Btn 1 Clicked");
}
$("#btn_1").click(click);

回撥函數的缺點也很明顯,容易產生回撥地獄:

2.png

非同步程式設計的三種方式

  • callback
function getOneNews() {
  $.ajax({
    url: topicsUrl,
    success: function(res) {
      let id = res.data[0].id;
      $.ajax({
        url: topicOneUrl + id,
        success: function(ress) {
          console.log(ress);
          render(ress.data);
        },
      });
    },
  });
}
  • promise
function getOneNews() {
  axios
    .get(topicsUrl)
    .then(function(response) {
      let id = response.data.data[0].id;
      return axios.get(topicOneUrl + id);
    })
    .then((res) => {
      render(res.data.data);
    })
    .catch(function(error) {
      console.log(error);
    });
}
  • async/await
async function getOneNews() {
  let listData = await axios.get(topicsUrl);
  let id = listData.data.data[0].id;
  let data = await axios.get(topicOneUrl + id);
  render(data.data.data);
}

線上預覽

預覽地址:http://jsrun.net/s43Kp/embedded/all/light

問題??

如果多個非同步程式碼同時存在,那麼執行順序應該是怎樣的?那個先執行、那個後執行了?

宏任務和微任務

非同步程式碼的劃分,非同步程式碼分宏任務和微任務。

宏任務(不著急)微任務(著急)
<script>整體程式碼Promise
setTimeout/setInterval

事件迴圈(Event loop)

3.png

執行順序:

  • 執行整體程式碼<script>(宏任務)

  • 執行所有微任務

  • 執行一個宏任務

  • 執行渲染執行緒

  • 2->3->2->3...依次迴圈(在 2、3 步中又建立了新的宏、微任務)

重複從宏任務和微任務佇列裡拿出任務去執行。

總結

因為瀏覽器設計的原因,JS 執行緒和渲染執行緒互斥,所以 JS 執行緒被設計成了單執行緒。

因為單執行緒執行一些操作(如網路請求)時有堵塞的問題,所有產生了非同步。

因為有了非同步,所以產生了非同步程式設計,從而有了回撥函數。

因為回撥函數寫多了會產生回撥地獄,所有又有了解決回撥地獄的 Promise 寫法

自 ES7 標準後有了比 Promise 更加優雅的寫法 ———— async/await 寫法,也是非同步程式設計的最終解決方法。

因為 JS 的程式碼分為同步和非同步程式碼,同步程式碼的執行順序不必多說,自上而下的執行。

但是如果有多個非同步的程式碼,他的執行順序又是怎麼的呢??

為了解決多個非同步程式碼的執行順序問了,有了事件迴圈(EventLoop),將非同步任務區分為宏任務、微任務,依據規則依次執行。

至此 完!

練習

console.log("script start");
setTimeout(function() {
  console.log("timeout1");
}, 10);
new Promise((resolve) => {
  console.log("promise1");
  resolve();
  setTimeout(() => console.log("timeout2"), 10);
}).then(function() {
  console.log("then1");
});
console.log("script end");

寫出 log 的輸出結果,並說出理由。

更多程式設計相關知識,請存取:!!

以上就是手把手帶你弄懂JavaScript中的非同步程式設計的詳細內容,更多請關注TW511.COM其它相關文章!