EventLoop 的理解

無樣發表於2019-05-04

一般前端了解eventloop的時候, 最想知道程式碼執行的先後順序,並非分析EventLoop。所以這裡先說總結。 為什麼?因為面試常考這個?,因為分析和聽懂分析都費勁。

##一、總結

  1. js 單執行緒缺點,容易出現"假死"(如alert()之後,dom不渲染)。優點:保證 dom 的渲染不易出錯。
  2. 解決"假死"的問題?其他語言,如java採用多執行緒去解決,避免佔用計算機空間太大,dom 渲染問題易出錯問題存在。所以js 採用單執行緒+非同步解決方案。
  3. 單執行緒 + 非同步的實現方式叫做——EventLoop
  4. 加入非同步佇列方式有兩種,第一種是巨集任務;第二種是微任務。微任務加入非同步佇列的優先順序巨集任務,且是先進先出原則,說得比較繞,簡而言之就是,微任務執行完,才會執行巨集任務
  5. 微任務:Promise.prototype.then、async await、Process.nextTick(Node獨有)、Object.observe(廢棄)、MutationObserver
  6. 巨集任務:script全部程式碼、setTimeout、setInterval、setImmediate(瀏覽器暫時不支援,只有IE10支援,具體可見MDN)、I/O、UI Rendering。
  7. 執行的優先順序即,同步函式(主執行緒上) -> 非同步佇列·微任務 -> 非同步佇列·巨集任務

ps: 對純前端而言,掌握Promise.prototype.then、async await 和 setTimeout 的區別就可以了。另外,Promise、async await是可以轉換的,但是瀏覽器版本問題,async await 的優先順序可能高於 Promise(可以忽略這種情況)。

測試題目:

const fn1 = await function () {
	await fn2()
	console.log(1)
}

async function fn2 () {
	await console.log(2)
}

fn1()

setTimeout(function () {
	console.log(3)
})

new Promise(function (resolve, reject) {
	console.log(4)
	resovle()
}).then(function () {
	console.log(5)
}).then(function () {
	console.log(6)
})
// 答案
// 2
// 4
// 1
// 5
// 6
// 3
複製程式碼

分析:

  1. 2 和 4 是同步函式
  2. 1、5 和 6 是微任務,非同步佇列的順序是1、5、6
  3. 3 是巨集任務

程式碼轉換分析

function fn1 () {
	new Promise(function(resolve, reject) {
		console.log(2)
		resolve()
	}).then(function () {
		console.log(1)
	})
}

fn1()

setTimeout(function () {
	console.log(3)
})

new Promise(function (resolve) {
	console.log(4)
	resolve()
}).then(function () {
	console.log(5)
}).then(function () {
	console.log(6)
})
複製程式碼

二、記憶體分析和緣由

阮一峰eventloop www.ruanyifeng.com/blog/2013/1… 看記憶體分析的這篇 github.com/baiyuze/not…

瞭解過,可以跳過

三、封裝Promise原始碼分析

看這篇:juejin.im/entry/59996… 覺得分析有點囉嗦,沒有提取關鍵資訊

瞭解過,可以跳過

四、uml 類圖分析Promise

MDN文件 的 Promise 執行流程圖:

EventLoop 的理解

第 1 步 用到的設計模式 和 理解 promise 的簡易結構

瞭解程式碼第一步,一定要知道他的設計模式,能夠節約相當看程式碼時間。Promise 程式碼 最主要的模式,觀察者模式

EventLoop 的理解

封裝一個簡易的Promise的 resolvethen,便於理解 Pomise 的封裝解構

  • then 從語法結構上講是然後的意思,但在封裝上,是將函式加入非同步佇列,並返回一個 Promise 的一個類似物件
  • resolve 執行非同步佇列
class Que {
  _queueLit = []
  constructor (handler) {
    handler.call(this, this._resolve)
  }
  _queue (cb) {
    setTimeout(cb)
  }
  _resolve = () => {
    const { _queueLit, _queue } = this
    const resolve = function () {
      let cb
      while (cb = _queueLit.shift()) {
        cb()
      }
    }
    _queue(resolve)
  }
  then (cb) {
    this._queueLit.push(cb)
    return this
  }
}

// test
setTimeout(() => {
  console.log(4) 
});
new Que(function (resolve) {
  console.log(1)
  resolve()
}).then(function () {
  console.log(2)
}).then(function () {
  console.log(3)
})

// 結果
// 1
// 4
// 2
// 3
複製程式碼

後記:不然發現 setTimeout 執行的 callback 為什麼是全域性的,以及 await 為什麼不能在全域性環境下,只能在函式內?原因是為了加入非同步佇列

第 2 步 promise 處理8關鍵點(第 3 點 最為重要)

上面的封裝的程式碼,簡單理解 Promise 封裝的解構,現在梳理 Promise的解構,可知道 promise 的根本的兩個方法: then 和 _resolve , _queues、_status。reject 、catch等都是在此基礎上上進行二次封裝。下列數列梳理幾個點:

  1. then 接受的函式,是 Promise 時的處理方式
  2. then 接受的函式,是同步函式時的處理方式
  3. then 返回一個 Promise 例項,將該例項要執行的 _resolve 放到上一個 Promise 例項的非同步佇列中即將執行的非同步函式中
  4. _resolve 傳遞引數的value 是一個Promise 時的處理方式
  5. _resolve 放到 eventloop 中,即 setTimeout(run)中 ,先進先出執行非同步佇列
  6. _status 狀態改變只在 _resolve 中改變
  7. _status 狀態判斷只在then中執行
  8. 利用自責鏈之後,每個Promise 的 _queues 只有一個元素, _queues 是裡面是多個觀察者,這裡根據其他人說的,要實現一個 1 對 1 的觀察者模式。

第 3 步 從完整版的Promise提取關鍵程式碼

原始碼地址--> coderlt.coding.me/2016/12/04/…

uml 類圖

EventLoop 的理解

拿到原始碼,將原始碼錯誤檢測、不需要分析的方法通通幹掉,避免混淆視聽,得到如下程式碼:

// 判斷變數否為function
const isFunction = variable => typeof variable === 'function'

const StatusType = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
}

class MyPromise {
  _status = StatusType.PENDING // 新增狀態
  _value = undefined // 新增狀態
  _fulfilledQueues = [] // 新增成功回撥函式佇列
  constructor(handle) {
    handle(this._resolve.bind(this))
  }
  _resolve(val) {
    const run = () => {
      if (this._status !== StatusType.PENDING) return
      const runFulfilled = (value) => {
        let cb
        while (cb = this._fulfilledQueues.shift()) {
          cb(value)
        }
      }
      if (val instanceof MyPromise) {
        const resolvePromise = val
        resolvePromise.then(value => {
          this._value = value
          this._status = StatusType.FULFILLED
          runFulfilled(value)
        })
      } else {
        this._value = val
        this._status = StatusType.FULFILLED
        runFulfilled(val)
      }
    }
    setTimeout(run)
  }

  then(onFulfilled) {
    const {
      _value,
      _status
    } = this
    // 返回一個新的Promise物件
    return new MyPromise((onFulfilledNext) => {
      // 封裝一個成功時執行的函式
      let fulfilled = value => {
        let res = onFulfilled(value)
        if (res instanceof MyPromise) {
          res.then(onFulfilledNext)
        } else {
          // 下一個 promise 的 resolve 方法的執行
          onFulfilledNext(res)
        }
      }
      switch (_status) {
        case StatusType.PENDING:
          /* 至關重要的程式碼 */
          this._fulfilledQueues.push(fulfilled)
           /* 至關重要的程式碼 end */
          break
        case StatusType.FULFILLED:
          fulfilled(_value)
          break
      }
    })
  }
}
複製程式碼

總結

分析到這裡,基本瞭解的 promise.then 和 resolve 的實現。reject 相當於 promise 的 resolve 的翻版,catch、all、race 就不在話下,簡而言之,promise 原始碼最終重要的封裝是 promise.then 和 resolve.

相關文章