這篇文章就再也不聊關於promise的各類好處和用法了,若是不瞭解請自行Google啦!javascript
我相信不少人在面試的時候遇到過這樣一道面試題:html
console.log(0)
let p = Promise.resolve()
setTimeout(()=>{
console.log(4);
setTimeout(()=>{
console.log(5);
},0);
},0);
p.then(data=>{
console.log(2);
setTimeout(()=>{
console.log(3);
},0);
})
console.log(6)
複製代碼
那麼你的答案是什麼呢? 粘貼到chrome的控制檯裏運行一下,結果以下vue
// 0
// 6
// 2
// 4
// 3
// 5
複製代碼
interesting的是,並非在全部瀏覽器裏都是這樣的打印順序的,例如,在safari 9.1.2中測試,輸出卻這樣的:java
// 0
// 6
// 4
// 2
// 5
// 3
複製代碼
再放到safari 10.0.1中卻又獲得了和chrome同樣的結果;node
固然,這只是這道面試題的一個簡單版本喲!git
那麼這道題到底在考察什麼呢?es6
其實,我相信不少同窗均可以一眼看出0和6會先輸出,可是setTimeout和promise哪一個先執行就有一丟丟小糾結了github
不再想爲這樣的執行順序所困擾?讓咱們先來了解一下js的event loop機制和promises的實現原理吧。web
咱們都知道promise是用來處理異步的,也知道js是單線程的,那麼js的異步是什麼呢? 這裏咱們先明確一批概念,是的沒看錯,一批面試
ECMAScript + DOM + BOM 咱們說js異步背後的「靠山」就是event loops。 其實這裏的異步準確的說應該叫瀏覽器的event loops或者說是javaScript運行環境的event loops,由於ECMAScript中沒有event loops, event loops是在HTML Standard定義的。
event loop也就是咱們常說的事件循環,能夠理解爲實現異步的一種方式,咱們來看看event loop在HTML Standard中的定義:
爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用本節所述的event loop。
咱們知道javascript在最初設計時設計成了單線程,爲何不是多線程呢? 進程是操做系統分配資源和調度任務的基本單位,線程是創建在進程上的一次程序運行單位,一個進程上能夠有多個線程。
以瀏覽器爲例
因而可知瀏覽器是多進程的,而且從咱們的角度來看咱們更加關心主進程,也就是瀏覽器渲染引擎
而單獨看渲染引擎,內部又是多線程的,包含兩個最爲重要的線程,即ui線程和js線程。並且ui線程和js線程是互斥的,由於JS運行結果會影響到ui線程的結果。
這裏也就回答了javascript爲何是單線程得問題,試想一下,若是多個線程同時操做DOM那豈不會很混亂?
固然,這裏所謂的單線程指的是主線程,也就是渲染引擎是單線程的,一樣的,在Node中主線程也是單線程的。
既然說js單線程指的是主線程是單線程的,那麼還有哪些其餘的線程呢?
單線程特色是節約了內存,而且不須要在切換執行上下文。並且單線程不須要管其餘語言如java裏鎖的問題;
ps:這裏簡單說下鎖的概念。例以下課了你們都要去上廁所,廁所就一個,至關於全部人都要訪問同一個資源。那麼先進去的就要上鎖。而對於node來講。 下課了就一我的去廁所,因此免除了鎖的問題!
一個event loop有一個或者多個task隊列。
當用戶代理安排一個任務,必須將該任務增長到相應的event loop的一個tsak隊列中。
每個task都來源於指定的任務源,好比能夠爲鼠標、鍵盤事件提供一個task隊列,其餘事件又是一個單獨的隊列。能夠爲鼠標、鍵盤事件分配更多的時間,保證交互的流暢。
task也被稱爲macrotask,task隊列仍是比較好理解的,就是一個先進先出的隊列,由指定的任務源去提供任務。
哪些是task任務源呢?
規範在Generic task sources中有說起:
DOM操做任務源: 此任務源被用來相應dom操做,例如一個元素以非阻塞的方式插入文檔。
用戶交互任務源: 此任務源用於對用戶交互做出反應,例如鍵盤或鼠標輸入。響應用戶操做的事件(例如click)必須使用task隊列。
網絡任務源: 網絡任務源被用來響應網絡活動。
history traversal任務源: 當調用history.back()等相似的api時,將任務插進task隊列。
總之,task任務源很是寬泛,好比ajax的onload,click事件,基本上咱們常常綁定的各類事件都是task任務源,還有數據庫操做(IndexedDB ),須要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來講task任務源:
每個event loop都有一個microtask隊列,一個microtask會被排進microtask隊列而不是task隊列。
有兩種microtasks:分別是solitary callback microtasks和compound microtasks。規範值只覆蓋solitary callback microtasks。
若是在初期執行時,spin the event loop,microtasks有可能被移動到常規的task隊列,在這種狀況下,microtasks任務源會被task任務源所用。一般狀況,task任務源和microtasks是不相關的。
microtask 隊列和task 隊列有些類似,都是先進先出的隊列,由指定的任務源去提供任務,不一樣的是一個 event loop裏只有一個microtask 隊列。
HTML Standard沒有具體指明哪些是microtask任務源,一般認爲是microtask任務源有:
task和microtask都是推入棧中執行的 來看下面一段代碼:
function bar() {
console.log('bar');
}
function foo() {
console.log('foo');
bar();
}
foo();
複製代碼
在規範的Processing model定義了event loop的循環過程: 一個event loop只要存在,就會不斷執行下邊的步驟:
主線程以外,還存在一個任務隊列,用來放置microtask。
簡單來講,event loop會不斷循環的去取tasks隊列的中最老的一個任務推入棧中執行,當次循環同步任務執行結束以後檢查是否存在microtasks隊列,若是有microtasks則先執行microtasks,執行結束清空microtasks棧,把下一個task放入執行棧內,如此循環。
說了這麼多關於event loop的東西,好像跟開篇的面試題並無什麼關係啊?
彆着急,下面咱們聊一下promise的實現; 咱們知道,promise是屬於es6的,在之前瀏覽器並不支持,也就衍生了各家諸如bluebird,q,when等promise庫,這些promise庫的實現方式不盡相同,但都遵循Promises/A+規範;
其中2.2.4就是:
onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
這就意味着,在實現promise時,onFulfilled和onRejected要在新的執行上下文裏才能執行;
而在3.1中說起了
This can be implemented with either a 「macro-task」 mechanism such as setTimeout or setImmediate, or with a 「micro-task」 mechanism such as MutationObserver or process.nextTick.
即promise的then方法能夠採用「宏任務(macro-task)」機制或者「微任務(micro-task)」機制來實現。有的瀏覽器將then放入了macro-task隊列,有的放入了micro-task 隊列。開頭打印順序不一樣也正是源於此,不過一個廣泛的共識是promises屬於microtasks隊列。
那麼咱們就來簡單看一下promise的「宏任務(macro-task)」機制實現:
class Promise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (data) => {
if (this.status === 'pending') {
this.value = data;
this.status = 'resolved';
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.reason = reason;
this.status = 'rejected';
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulFilled, onRejected) {
onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : y => y;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
let promise2;
if (this.status === 'resolved') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //「宏任務(macro-task)」機制實現
try {
let x = onFulFilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //「宏任務(macro-task)」機制實現
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
});
}
if (this.status === 'pending') {
promise2 = new Promise((resolve, reject) => {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulFilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0)
});
// 存放失敗的回調
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
})
}
return promise2; // 調用then後返回一個新的promise
}
// catch接收的參數 只用錯誤
catch(onRejected) {
// catch就是then的沒有成功的簡寫
return this.then(null, onRejected);
}
}
複製代碼
沒錯咱們看到了setTimeout; 這種就是經過macro-task機制實現的,打印出來的順序就是如在safari 9.1.2中同樣了。 測試了一下bluebird的promise的實現,輸出的結果又和上面的都不同:
// 0
// 6
// 4
// 2
// 5
// 3
複製代碼
因此到底哪一個先輸出,要看你所使用的promise的實現方式;
固然正如上面提到的一個廣泛的共識是promises屬於microtasks隊列,因此通常狀況下,promise.then並非上面的這種實現,而是mic-task機制;
那麼再來看開篇的題目
console.log(0) // 同步
let p = Promise.resolve();
setTimeout(()=>{ // 異步 macrotask
console.log(4);
setTimeout(()=>{
console.log(5); // 異步 macrotask
},0);
},0);
p.then(data=>{ // 異步 (經過macro-task實現則爲macrotask,經過micro-task實現則爲microtask)
console.log(2);
setTimeout(()=>{ // 異步 macrotask
console.log(3);
},0);
})
console.log(6) // 同步
複製代碼
這樣就很清晰了對吧
上面有列出microtask有
不知道用過vue1.0的同窗有沒有了解過vue1.0的nextTick是如何實現的呢?
有興趣能夠看一下源碼,就是經過MutationObserver實現的,只是由於兼容問題已經被取代了;
沒用過MutationObserver?不要緊,咱們舉一個簡單的例子 假如咱們要往一個id爲parent的dom中添加元素,咱們指望全部的添加操做都完成才執行咱們的回調 以下
let observe = new MutationObserver(function () {
console.log('dom所有塞進去了');
});
// 一個微任務
observe.observe(parent,{childList:true});
for (let i = 0; i < 100; i++) {
let p = document.createElement('p');
div.appendChild(p);
}
console.log(1);
let img = document.createElement('p');
div.appendChild(img);
複製代碼
That's all ,如上;