一般前端了解eventloop的時候, 最想知道程式碼執行的先後順序,並非分析EventLoop。所以這裡先說總結。 為什麼?因為面試常考這個?,因為分析和聽懂分析都費勁。
##一、總結
- js 單執行緒缺點,容易出現"假死"(如alert()之後,dom不渲染)。優點:保證 dom 的渲染不易出錯。
- 解決"假死"的問題?其他語言,如java採用多執行緒去解決,避免佔用計算機空間太大,dom 渲染問題易出錯問題存在。所以js 採用單執行緒+非同步解決方案。
- 單執行緒 + 非同步的實現方式叫做——EventLoop
- 加入非同步佇列方式有兩種,第一種是巨集任務;第二種是微任務。微任務加入非同步佇列的優先順序巨集任務,且是先進先出原則,說得比較繞,簡而言之就是,微任務執行完,才會執行巨集任務。
- 微任務:Promise.prototype.then、async await、Process.nextTick(Node獨有)、Object.observe(廢棄)、MutationObserver
- 巨集任務:script全部程式碼、setTimeout、setInterval、setImmediate(瀏覽器暫時不支援,只有IE10支援,具體可見MDN)、I/O、UI Rendering。
- 執行的優先順序即,同步函式(主執行緒上) -> 非同步佇列·微任務 -> 非同步佇列·巨集任務
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
複製程式碼
分析:
- 2 和 4 是同步函式
- 1、5 和 6 是微任務,非同步佇列的順序是1、5、6
- 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 執行流程圖:
第 1 步 用到的設計模式 和 理解 promise 的簡易結構
瞭解程式碼第一步,一定要知道他的設計模式,能夠節約相當看程式碼時間。Promise 程式碼 最主要的模式,觀察者模式
封裝一個簡易的Promise的 resolve 和 then,便於理解 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等都是在此基礎上上進行二次封裝。下列數列梳理幾個點:
- then 接受的函式,是
Promise
時的處理方式 - then 接受的函式,是同步函式時的處理方式
- then 返回一個 Promise 例項,將該例項要執行的 _resolve 放到上一個 Promise 例項的非同步佇列中即將執行的非同步函式中
- _resolve 傳遞引數的value 是一個Promise 時的處理方式
- _resolve 放到 eventloop 中,即
setTimeout(run)
中 ,先進先出執行非同步佇列 - _status 狀態改變只在 _resolve 中改變
- _status 狀態判斷只在then中執行
- 利用自責鏈之後,每個Promise 的 _queues 只有一個元素,
_queues
是裡面是多個觀察者,這裡根據其他人說的,要實現一個 1 對 1 的觀察者模式。
第 3 步 從完整版的Promise提取關鍵程式碼
原始碼地址--> coderlt.coding.me/2016/12/04/…
uml 類圖
拿到原始碼,將原始碼錯誤檢測、不需要分析的方法通通幹掉,避免混淆視聽,得到如下程式碼:
// 判斷變數否為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.