JavaScript – 非同步的前世今生

歸去來兮-不如去兮發表於2018-08-16

​ 在開始接觸JavaScript的時候,書上有一句話我記憶深刻,JavaScript是一門單執行緒語言,不管從什麼途徑去獲取這個訊息,前端開發者都會記住,哦~~,JavaScript是一門單執行緒語言,所以alert()會卡住

為什麼JavaScript是一門單執行緒語言?因為什麼原因讓JavaScript出生就是單執行緒語言?

​ 提出問題,必然解決問題,從JavaScript出生說起,最開始JavaScript是配合html去完成對dom的控制,優化使用者互動,於是問題來了

  • js可以修改Dom結構
  • 瀏覽器渲染Dom結構

假如同時執行,會發生什麼,假如同時操作同一個Dom怎麼辦,

同理 兩段js都修改Dom結構,假如同時操作Dom會發生什麼

js沒有執行的時候,瀏覽器Dom渲染,js執行的時候,瀏覽器Dom停止渲染,html結構樹本來是就是從上往下渲染,也就是說,瀏覽器渲染Dom本身就是單執行緒,js為了避免發生Dom衝突,沒有辦法只能成為單執行緒語言

但是單執行緒的硬傷出現了,單執行緒意味著頁面載入會卡頓,執行緒被佔用瀏覽器卡死

  var sum = 0;
    console.log("start");
    
    for (let i = 0; i < 1000000000; i++) {  //單執行緒佔用
      sum++
    }
  console.log(sum);
  alert("100")   //單執行緒佔用
  console.log("我終於執行啦");

​ 例如上面的程式碼,瀏覽器資源類佔用,就會發生可怕的事情,瀏覽器卡死,也許這裡for迴圈不形式,但是現實開發裡面同步ajax請求慢的時候頁面卡死是正常事

同步解決方案 – 非同步

說到非同步,不得不談大名鼎鼎的event-loop(事件輪詢)

相信開發者一定看過阮一峰大神對Event loop的解釋,除了圖有點看不懂之外,其他都講的很請求

這裡要說一道比較老的面試題

 setTimeout(() => {
    console.log("我是定製器");
  });
  console.log("我是主程式");

很簡單 先執行console.log("我是主程式"); ,為什麼呢?即使因為事件輪詢

當瀏覽器從上往下解析,遇到setTimeout,setTimeout是非同步任務,瀏覽器就會將他放入非同步任務裡面,然後執行主執行緒的程式碼,當主執行緒的程式碼執行完畢,事件輪詢就是去檢視非同步任務,於是執行了setTimeout裡面的函式

這就是事件輪詢

這裡借閱阮一峰大神的一段部落格

單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒著的,因為IO裝置(輸入輸出裝置)很慢(比如Ajax操作從網路讀取資料),不得不等著結果出來,再往下執行。

JavaScript語言的設計者意識到,這時主執行緒完全可以不管IO裝置,掛起處於等待中的任務,先執行排在後面的任務。等到IO裝置返回了結果,再回過頭,把掛起的任務繼續執行下去。

於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

具體來說,非同步執行的執行機制如下。(同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。

(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步。

​ event-loop就像哨兵一樣,監視著非同步佇列,一旦有非同步程式碼,就會在主執行緒執行完畢將非同步方法,讀取到主執行緒,在回去監視非同步佇列,所以event-loop也叫事件輪詢

jQuery.deferred()

無法改變 JS 非同步和單執行緒的本質

只能從寫法上杜絕 callback 這種形式

他是語法糖,但是解構了程式碼

體現了程式設計思想 : 開放封閉原則

對擴充開放,對修改封閉, 越是深入模組化,越是體現這樣的實現

 $.ajax({
    url: "test.html",
    success: function(){
      alert("哈哈,成功了!");
    },
    error:function(){
      alert("出錯啦!");
    }
  });

老版本的使用的是回撥函式形式,那麼假如我們想修改程式碼,勢必要對$.ajax({})裡面的所有程式碼進行變成 這就成了

對擴充封閉,對修改封閉,這是對在開發上非常不友好的變成方式,牽一髮而動全身

當deferred出現後,ajax變成了鏈式呼叫

 $.ajax("test.html")

  .done(function(){ alert("哈哈,成功了!"); })

  .fail(function(){ alert("出錯啦!"); });

這裡明顯可以看到對修改封閉,對擴充開放

假如我們想擴充

.done(function(){ alert("第二個回撥函式!");} );

完全不影響之前的程式碼,程式碼耦合性低

在我最開始接觸jquery的時候,ajax有三種寫法

$.ajax({
    url,
    success: function (){
        //成功回撥
    },
    error: function (){
        //失敗回撥
    }
})
$.ajax({url})
.done(()=> {
    //成功回撥
})
.fail((0=> {
    //失敗回撥
})
$.ajax({url})
.then(()=> {
    //成功回撥
},
()=> {
    //失敗回撥
})

剛開始不解,為什麼第一種是淘汰的寫法.再後來漸漸深入才發現回撥的侷限性,

  • 耦合性太高,
  • 不支援鏈式呼叫,
  • 不利於維護,回撥裡程式碼過於複雜會產生風險

這讓我想探究一下,如何實現.done.fail,所以就不得不說jQuery的deferred

阮一峰大神對deferred的講解

在使用jq我們可以使用deferred幫助我們控制非同步

function waitHandle() {
      var dtd = $.Deferred()
      var wait = function (dtd) {
        var task = function () {
          console.log("執行完成");
          dtd.resolve()
        }
        setTimeout(task, 2000);
        return dtd.promise()  //返回pomise物件 防止在回撥裡面執行控制回撥的函式 例如reject()
      }
      return wait(dtd) //返回wait的執行結果
    }
    waitHandle()
      .then(function () {
        console.log(1);
      }, () => {
        console.log("hahah");

      })
      .then(() => {
        console.log(2);
      }, () => {
        console.log("hahah");
      })

這裡我就不贅述了,前人已經全面的總結了;這裡明顯看到,這是已經初步提出了promise的概念,對日後的標準建立的基礎

Promise

對於低版本瀏覽器,想要使用第三方庫,可以使用bluebird.js,對promise進行實現

老生長談了,開發必須要會

這是jq的deferred的進階版啊,,開發者卸磨殺驢,竟然說jq淘汰了,一葉障目

  function loadImg(src){
    return new Promise((resolve,reject)=> {
      var img = document.createElement(`img`)
      img.onload = ()=> {
        resolve(img)
      }
      img.onerror = ()=> {
        reject("圖片載入失敗")
      }
      img.src = src
    })
  }

 var load1 = loadImg("https://secure.gravatar.com/avatar/7337a05ac8210b3f1b522818cd31b90d?s=86")
 var load2 = loadImg("https://avatars2.githubusercontent.com/u/33681955?s=460&v=4")
 load1.then(res=> {
   console.log(res.width);
   return load2   //後面的是load2的回撥  對於請求串聯 可以這麼寫 
 }).then(res=> {
   console.log(res.width);
 }).catch(res=> {
   console.log(res);
 })

Promise.all

接受一個Promise物件的陣列,待全部完成後,統一執行then,失敗一個catch

Promise.all([load1,load2])
  .then(res=> {
    console.log(res);
  })
  .catch (res=> {
    console.log(res); //假如有一個失敗就會返回catch
  })

Pormise.race

接收一個包含多個Promise物件的陣列,主要完成一個就執行then,失敗一個catch

Promise.race([load1, load2])
    .then(res => {
        console.log(res); //成功一個立馬返回
    })
    .catch(res => {
         console.log(res); //假如有一個失敗就會返回catch
     })

在Peomise上還要說一點就是狀態變化

三種狀態: pending(待定的) fulfilled(滿足的) rejected(拒絕)

規則: 狀態變化不可逆

在初始狀態下,Promise是pending 只有兩種情況

  • Promise執行成功 pending 成功狀態變成fulfilled
  • Promise執行成功 pending 失敗狀態變成rejected

瞭解一下

async/await

then是將callback進行拆分了

關於async/await 其實網上資料也挺多的,將非同步程式碼同步進行,取消了.then這樣的寫法,迴歸本源

let load = async function() {
  let result1 = await loadImg(load1)
  console.log(result1);
  let result2 = await loadImg(load2)
  console.log(result2);
}
總結一下JavaScript非同步的前世今生
  • JavaScript是單執行緒,兩段js不可以同時進行
  • 原因是為了避免 DOM的渲染衝突
  • 非同步就是”無奈的解決方案”,但是初期有很多問題
  • 非同步是event loop實現的,事件輪詢 非同步方法 進入的非同步佇列,主執行緒執行完畢才會去執行非同步佇列裡面的程式碼
  • jQuery Deferred,改變了非同步只能由callback操作這個問題
  • deferred和Promise 有區別的 Promise對deferred進行優化 ,讓Promise物件只能被動監聽結果,避免衝突
  • Promise的出現成為了對非同步的最佳解決方案相對於callback,極大的降低了耦合性
  • async/await 配合Promise 成為終極解決方案,但是ES7為成為正式版,未來仍需參考

非同步的解決方案

  • 回撥函式 – callback
  • 初代鏈式 – deferred (我覺得這個有必要提出來,改變js非同步程式設計的關鍵)
  • 成熟方案 – promise
  • 終極方案 – async/await + promise
  • 迭代器 Generator(同樣實現非同步,但是被async/await替代)

相關文章