js的setTimeout和Promise---同步異步和微任務宏任務

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


單獨使用的執行模式

這裏就使用Promise做爲例子,來探究一下單獨使用它,會有哪些注意點。vue

1.最初的試探 api

執行代碼,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性能

編輯器中,輸入如下代碼,多個同級的單層的Promisecode

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')

則會依次輸出如下打印,毫無疑問的結果:server

同步-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中。

相關文章
相關標籤/搜索