Javascript異步編程

同步模式與異步模式


事件循環與消息隊列


??JavaScript 單線程指的是瀏覽器中負責解釋和執行 JavaScript 代碼的只有一個線程,即為JS引擎線程,但是瀏覽器的渲染進程是提供多個線程的,如下:

  • JS引擎線程
  • 事件觸發線程
  • 定時觸發器線程
  • 異步http請求線程
  • GUI渲染線程

??當遇到計時器、DOM事件監聽或者是網絡請求的任務時,JS引擎會將它們直接交給 webapi,也就是瀏覽器提供的相應線程(如定時器線程為setTimeout計時、異步http請求線程處理網絡請求)去處理,而JS引擎線程繼續后面的其他任務,這樣便實現了 異步非阻塞。

??定時器觸發線程也只是為 setTimeout(..., 1000) 定時而已,時間一到,還會把它對應的回調函數(callback)交給 任務隊列 去維護,JS引擎線程會在適當的時候去任務隊列取出任務并執行。

JS引擎線程什么時候去處理呢?消息隊列又是什么?

JavaScript 通過 事件循環 event loop 的機制來解決這個問題。

其實 事件循環 機制和 任務隊列 的維護是由事件觸發線程控制的。

事件觸發線程 同樣是瀏覽器渲染引擎提供的,它會維護一個 任務隊列。

??JS引擎線程遇到異步(DOM事件監聽、網絡請求、setTimeout計時器等...),會交給相應的線程單獨去維護異步任務,等待某個時機(計時器結束、網絡請求成功、用戶點擊DOM),然后由 事件觸發線程 將異步對應的 回調函數 加入到消息隊列中,消息隊列中的回調函數等待被執行。

同時,JS引擎線程會維護一個 執行棧,同步代碼會依次加入執行棧然后執行,結束會退出執行棧。

如果執行棧里的任務執行完成,即執行棧為空的時候(即JS引擎線程空閑),事件觸發線程才會從消息隊列取出一個任務(即異步的回調函數)放入執行棧中執行。

消息隊列是類似隊列的數據結構,遵循先入先出(FIFO)的規則。

  1. 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
  2. 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  3. 一但"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重復上面的第三步。
    只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復,這種機制就被稱為事件循環(event loop)機制。

異步編程的幾種方式


Promise異步方案 宏任務/微任務隊列


Promise

  • promise是一個類,在執行這個類的時候需要傳遞一個執行器進去 執行器,執行器會立即執行
  • promise中有三種狀態 分別為成功fulfilled 失敗rejected 等待pending
    一旦狀態確定就不可更改
  • resolve和reject函數是用來更改狀態的
  • then方法內部做的事情就是判斷狀態,成功就調用成功函數,反之調用失敗函數,then方法是被定義到原型對象中的
  • then成功回調有一個參數表示成功之后的值,同樣失敗也有參數
  • then方法是可以被鏈式調用的,后面的then拿到的值是,是上一個then的返回值
const PENDING='pending'
const FULFILLED='fulfilled'
const REJECTED='rejected'

class MyPromise{
  construtor(executor){
    try{
      executor(this.resolve,this.reject)
    }catch(e){
      this.reject(e)
    }
  }
  
  status=PENDING
//成功之后的值
  value=undefined
//失敗后的原因
  reason=undefined

//成功回調
successCallback=[]
//失敗回調
failCallback=[]

  resolve=(value)=>{  
  //如果狀態不是等待阻止程序向下執行
  if(this.status!==PENDING) return;
    this.status=FULFILLED
    //保存成功之后的值
    this.value=value
    while(this.successCallback.length){
      this.successCallback.shift()()
    }
  }
  reject=()=>{
    if(this.status!==PENDING) return;
    this.status=REJECTED
    //保存失敗后的原因
    this.reason=reason
    
    while(this.failCallback.length){
      this.failCallback.shift()()
    }
  }
  then(successCallback,failCallback){
    let promise2=new MyPromise((resolve,reject)=>{
      if(this.status===FULFILLED){
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            //判斷x的值是普通值還是promise對象
            //如果是普通值直接調用resolve
            //如果是promise對象查看promise對象的返回結果
            //再根據promise對象返回的結果決定調用resolve還是reject
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
      }
      if(this.status===REJECTED){
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        
      }else{
      //等待,存儲成功和失敗回調
        this.successCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
        this.failCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
      }
    })
    
    return promise2
  }
}

function resolvePromise(promise2,x,resolve,reject){
  if(x===promise2){
  reject(new TypeError('chaining cycle detected for promise'))
  }
  if(x instanceof MyPromise){
    //x.then(value=>resolve(value),reason=>reject(reason))
    x.then(resolve,reject)
  }else{
    resolve(x)
  }
}

macro-task(宏任務) 和 micro-task(微任務)。


所有任務分為 macro-task 和 micro-task:

  • macro-task:主代碼塊、setTimeout、setInterval等(可以看到,事件隊列中的每一個事件都是一個 macro-task,現在稱之為宏任務隊列)
  • micro-task:Promise、process.nextTick等

JS引擎線程首先執行主代碼塊。

每次執行棧執行的代碼就是一個宏任務,包括任務隊列(宏任務隊列)中的,因為執行棧中的宏任務執行完會去取任務隊列(宏任務隊列)中的任務加入執行棧中,即同樣是事件循環的機制。

在執行宏任務時遇到Promise等,會創建微任務(.then()里面的回調),并加入到微任務隊列隊尾。

micro-task必然是在某個宏任務執行的時候創建的,而在下一個宏任務開始之前,瀏覽器會對頁面重新渲染(task >> 渲染 >> 下一個task(從任務隊列中取一個))。同時,在上一個宏任務執行完成后,渲染頁面之前,會執行當前微任務隊列中的所有微任務。

也就是說,在某一個macro-task執行完后,在重新渲染與開始下一個宏任務之前,就會將在它執行期間產生的所有micro-task都執行完畢(在渲染前)。

這樣就可以解釋 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做為微任務加入到微任務隊列中,而 "timer over" 做為宏任務加入到宏任務隊列中,它們同時在等待被執行,但是微任務隊列中的所有微任務都會在開始下一個宏任務之前都被執行完。

在node環境下,process.nextTick的優先級高于Promise,也就是說:在宏任務結束后會先執行微任務隊列中的nextTickQueue,然后才會執行微任務中的Promise。
執行機制:

  1. 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  2. 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
  3. 宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
  4. 當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
  5. 渲染完畢后,JS引擎線程繼續,開始下一個宏任務(從宏任務隊列中獲取)

宏任務 macro-task(Task)

一個event loop有一個或者多個task隊列。task任務源非常寬泛,比如ajax的onload,click事件,基本上我們經常綁定的各種事件都是task任務源,還有數據庫操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來說task任務源:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任務 micro-task(Job)

microtask 隊列和task 隊列有些相似,都是先進先出的隊列,由指定的任務源去提供任務,不同的是一個 event loop里只有一個microtask 隊列。另外microtask執行時機和Macrotasks也有所差異

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

宏任務和微任務的區別

  • 宏隊列可以有多個,微任務隊列只有一個,所以每創建一個新的settimeout都是一個新的宏任務隊列,執行完一個宏任務隊列后,都會去checkpoint 微任務。
  • 一個事件循環后,微任務隊列執行完了,再執行宏任務隊列
  • 一個事件循環中,在執行完一個宏隊列之后,就會去check 微任務隊列

Generator異步方案 async/await語法糖

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。