js的setTimeout和Promise—同步非同步和微任務巨集任務

千鳥語發表於2019-02-16

久經前端開發沙場,會經歷各式各樣的需求,處理這些需求時候,會使用各種各樣的api和功能,這裡集中對setTimeout和Promise的非同步功能進行討論一下。


單獨使用的執行模式

這裡就使用Promise作為例子,來探究一下單獨使用它,會有哪些注意點。

1.最初的試探

執行程式碼,Promise的基本使用:

let fn = () => {
    console.log(1)
  let a = new Promise((resolve, reject) => {
      console.log(2)
    resolve(3)
  })
  console.log(4)
  return a
}
// 執行
fn().then(data => console.log(data))

以上程式碼,輸出結果為:

1 // 同步
2 // 同步
4 // 同步
3 // 非同步

注意 new Promise() 是同步方法,resolve才是非同步方法。
此外,上面的方法,可以有下面這種寫法,效果等同,主要是把Promise精簡了一下:

let fn = () => {
  console.log(1)
  console.log(2)
  let a = Promise.resolve(3)
  console.log(4)
  return a
}

// 執行
fn().then(data => console.log(data))

因為現在討論的是Promise的非同步功能,所以下面均使用第二種寫法的Promise

2.多個同級Promise

編輯器中,輸入以下程式碼,多個同級的單層的Promise

console.log(`同步-0.1`)
Promise.resolve().then(() => {
  console.log(`P-1.1`)
})
Promise.resolve().then(() => {
  console.log(`P-1.2`)
})
Promise.resolve().then(() => {
  console.log(`P-1.3`)
})
console.log(`同步-0.2`)

則會依次輸出以下列印,毫無疑問的結果:

同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3

3.PromisePromise

複雜一下,新增行有註釋說明:

console.log(`同步-0.1`)
Promise.resolve().then(() => {
  console.log(`P-1.1`)
  Promise.resolve().then(() => { // 新加行
    console.log(`P-2.1`) // 新加行
  }) // 新加行
})
Promise.resolve().then(() => {
  console.log(`P-1.2`)
  Promise.resolve().then(() => { // 新加行
    console.log(`P-2.2`) // 新加行
  }) // 新加行
})
Promise.resolve().then(() => {
  console.log(`P-1.3`)
  Promise.resolve().then(() => { // 新加行
    console.log(`P-2.3`) // 新加行
  }) // 新加行
})
console.log(`同步-0.2`)

輸出結果如下:

同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3
P-2.1
P-2.2
P-2.3

可見,多層Promise是一層一層執行的。

4.為了最終確認,進行最後一次驗證,在第一個Promise裡面多加一層:

console.log(`同步-0.1`)
Promise.resolve().then(() => {
  console.log(`P-1.1`)
  Promise.resolve().then(() => {
    console.log(`P-2.1`)
    Promise.resolve().then(() => { // 新加行
      console.log(`P-3.1`) // 新加行
    }) // 新加行
    Promise.resolve().then(() => { // 新加行
      console.log(`P-3.2`) // 新加行
    }) // 新加行
  })
})
Promise.resolve().then(() => {
  console.log(`P-1.2`)
  Promise.resolve().then(() => {
    console.log(`P-2.2`)
  })
})
Promise.resolve().then(() => {
  console.log(`P-1.3`)
  Promise.resolve().then(() => {
    console.log(`P-2.3`)
  })
})
console.log(`同步-0.2`)

輸出結果如下:

同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3
P-2.1
P-2.2
P-2.3
P-3.1
P-3.2

確認完畢,的確是一層一層的執行。

而且這裡可以告訴大家,setTimeoutsetInterval在單獨使用的時候,和Promise是一樣的,同樣是分層執行,這裡不再貼程式碼了(友情提醒:setInterval的話,需要第一次執行就把這個定時器清掉,否則就無限執行,卡死頁面秒秒鐘的事兒),


混合使用的執行模式

接下來才是重點,下面將setTimeoutPromise進行混合操作。

console.log(`同步-0.1`)
Promise.resolve().then(() => {
  console.log(`P-1.1`)
})
setTimeout(() => {
  console.log(`S-1.1`)
});
Promise.resolve().then(() => {
  console.log(`P-1.2`)
})
setTimeout(() => {
  console.log(`S-1.2`)
});
console.log(`同步-0.2`)

執行結果如下。。。問題暴露出來了:

同步-0.1
同步-0.2
P-1.1
P-1.2
S-1.1
S-1.2

為什麼,在同級情況下,是Promise執行完了setTimeout才會執行?

是人性的泯滅,還是道德的淪喪?

是因為JavaScript任務型別!

JavaScript的微任務和巨集任務

敲黑板,標重點。

JavaScript的任務分為微任務(Microtasks)和巨集任務(task);

  • 巨集任務是主流,當js開始被執行的時候,就是開啟一個巨集任務,在巨集任務中執行一條一條的指令;
  • 巨集任務可以同時有多個,但會按順序一個一個執行;
  • 每一個巨集任務,後面都可以跟一個微任務佇列,如果微任務佇列中有指令或方法,那麼就會執行;如果沒有,則開始執行下一個巨集任務,直到所有的巨集任務執行完為止,微任務相當於巨集任務的小尾巴;
  • 為什麼有了巨集任務,還會有微任務存在?因為巨集任務太佔用效能,當需要一些較早就準備好的方法,排在最後才執行的時候,又不想新增一個巨集任務,那麼就可以把這些方法,一個一個的放在微任務佇列裡面,在這個巨集任務中的程式碼執行完後,就會執行微任務佇列。

Promise是微任務,setTimeout是巨集任務。

所以上面的程式碼中,程式碼執行時會是如下場景:

開始執行當前巨集任務程式碼!

遇到了Promise?好嘞,把它裡面的非同步程式碼,放在當前這個巨集任務後面微任務裡面,然後繼續執行我們的;

咦,有個setTimeout?是個巨集任務,那在當前這個巨集任務後面,建立第二個巨集任務,然後把這個setTimeout裡面的程式碼塞進去,我們繼續執行;

咦,又一個Promise?把他塞進後面的微任務裡。。。什麼?已經有程式碼了?那有啥關係,繼續往裡塞,放在已有程式碼的後面就行,我們繼續執行;

天啊,又來一個setTimeout,現在後面已經有第二個巨集任務了對吧?那就建立第三個巨集任務吧,後面再遇到的話,繼續建立;

報告!程式碼執行到底了,當前這個巨集任務執行完畢!
行,看一下我們的小尾巴—我們的微任務裡面有程式碼嗎?有的話直接執行;

報告,微任務裡面,那兩個Promise的非同步程式碼執行完了!
乾的漂亮。。。對了,剛剛微任務裡面,有沒有新的Promise微任務?有的話,繼續在現在這個微任務後面放!對對,只看執行到的程式碼,有多少放多少,一會兒直接就執行了。。。如果遇到了setTimeout知道該怎麼做吧?繼續開巨集任務!

報告,微任務全部執行完畢!

好!開始執行下一個巨集任務!

所以,現在如果執行下面的程式碼,結果也顯而易見吧:

console.log(`同步-0.1`)
Promise.resolve().then(() => {
  console.log(`P-1.1`)
  Promise.resolve().then(() => { // 新加行
    console.log(`P-2.1`) // 新加行
    Promise.resolve().then(() => { // 新加行
      console.log(`P-3.1`) // 新加行
    }) // 新加行
  }) // 新加行
})
setTimeout(() => {
  console.log(`S-1.1`)
});
Promise.resolve().then(() => {
  console.log(`P-1.2`)
})
setTimeout(() => {
  console.log(`S-1.2`)
});
console.log(`同步-0.2`)

執行結果如下:

同步-0.1
同步-0.2
P-1.1
P-1.2
P-2.1
P-3.1
S-1.1
S-1.2

無論Promise套用多少層,都會在下一個setTimeout之前執行。


Dom操作到底是同步,還是非同步?

這裡出現一個說不清道不明的疑問,Dom操作到底是同步操作還是非同步操作?

如果是同步操作,那vuenextTick方法是做什麼用的?不就是在Dom更新完之後的回撥方法嗎?

如果是非同步操作,那在劇烈操作Dom後面的程式碼,為什麼會被阻塞?而且程式碼看上去,也的確是按順序執行的?

這裡直接說明:js裡面的Dom操作程式碼,是同步執行,但瀏覽器進行的Dom渲染,是非同步操作。

瀏覽器渲染Dom和執行js,同時只能二選一,渲染一次Dom的時機是,當前巨集任務和小尾巴微任務執行完,下一個巨集任務開始前

vuenextTick方法,則是使用H5的Api—MutationObserver,監聽瀏覽器將Dom渲染完成的時機。

若瀏覽器不支援此方法,則會使用setTimeout,把nextTick回撥函式的執行時機,作為一個巨集任務;

上面也說了,瀏覽器渲染一次Dom,是下一個巨集任務開始前,這樣使用了setTimeout,保證了Dom確實渲染完成。

這裡也需要稍作提醒,js操作Dom是同步的,但操作Dom,畢竟超出了js本身語言的Api,每操作一次Dom,都需要消耗一定的效能,所以,在適合的情況下,最好先把要修改的Dom的內容,以字串或者虛擬Dom的形式拼接好,然後操作一次Dom,把組裝好的Dom字串或虛擬Dom,一次性的塞進HTML頁面的真實Dom中。

相關文章