title: JS中操作DOM是"同步"還是"異步"? date: 2019-08-08 17:55:00 tags:
- JAVAScript categories: JavaScript
很多時候"不得已"使用js操作DOM,這個操作過程到底是"同步"的還是"異步"呢?
很多時候"不得已"使用js操作DOM,這個操作過程到底是"同步"的還是"異步"呢?
一、操作DOM的栗子
按理說,在js的執行中,對于DOM的操作都是同步執行的,
<body></body> <script> var body = document.querySelector('body'); console.log(`1`); var cDiv = document.createElement('div'); console.log(cDiv) console.log(`2`); body.AppendChild(cDiv) console.log(body); </script>
以上結果目前和我們預想的結果是一致的,自上而下依次同步執行,這里劃重點,js引擎線程。
接下來做一點修改
<style>
.easy {
width: 200px;
height: 200px;
background: lightgoldenrodyellow;
}
.hard {
background: lightsalmon;
transition: 2s all;
}
</style>
<body></body>
<script>
var body = document.querySelector('body');
console.log(`1`);
var cDiv = document.createElement('div');
console.log(cDiv);
console.log(`2`);
body.appendChild(cDiv)
console.log(body);
cDiv.classList.add('easy')
console.log(`3`);
// ======================
for(var i = 0;i<3000000000;i++);
cDiv.classList.add('hard')
console.log(cDiv)
// ======================
</script>
既然是同步執行,那我在添加第二個樣式hard之前阻塞一下,理論上在阻塞的情況下<div>應該的背景色是淡黃色吧?不過跑一下完全不對勁啊,出來的很慢不說,竟然直接就橘色了。這里劃重點,GUI渲染線程
二、捋一捋問題
- 有阻塞,在阻塞時沒有顯示已有樣式,究竟是不是同步執行的?
- console.log()的內容并不是空,只是返回的很慢,看著像異步執行?
- 過度樣式被忽略了,但背景色覆蓋執行了,是什么原因?
三、依次解題
- js執行順序不在這里細說,常見能夠改變執行隊列的Promise、setTimeout、<script>標簽等等都沒有在這里出現,所以確認是同步執行無疑。
- 既然同步執行為什么會有"異步"的效果,這里要說到上文劃重點內容: js引擎線程與GUI渲染線程。也就是說,js引擎線程與GUI渲染線程互斥,這是線程之間的"同步"造成的操作DOM時的"異步"效果。
- <div>的樣式為什么沒有生效呢?明明有一個過渡效果。原因是:瀏覽器的渲染時會執行優化策略,即將多個同一DOM下的樣式合并后渲染。
四、 總結
- js引擎線程與GUI渲染線程線程間的互斥,引起了對js操作DOM的"異步"問題。
- GUI渲染線程在能夠執行的情況下的優化策略,渲染出的是最終得到的樣式結果。
具體的渲染線程的內容,不在這次討論范圍之內嘛。
雖然原因找到了,不過問題好像還在。
五、 解決問題
如果產品一定要從js創建出來的div擁有炫酷的特效(比如上面的過度樣式)。 呵呵呵呵
直接整理一下來自知乎各方大佬的解題思路, 這里不僅僅是過度樣式,類似問題依然有效。
分析問題:
- 過度效果是至少由A變B,也就是至少存有兩個不同狀態;
- 由于上文所講的GUI渲染線程與js引擎的互斥會造成一種"同步"執行的效果,所以創建<div>本身已經被滯后了,缺少A。
- 又由于GUI渲染線程優化策略,最后結果B將覆蓋可以覆蓋的所有。缺少了A(被覆蓋),之后被渲染出現在document內。
- 本身已經是B,且沒有A狀態,過度效果無效。
解決方向就是使<div>擁有一個初始狀態A就搞定了。(提前將生成的DOM渲染到document上)
解決方法一:
cDiv.classList.add('easy') // for(var i = 0;i<3000000000;i++); setTimeout(() => { cDiv.classList.add('hard') }, 0)
思路: 利用setTimeout方法,改變執行隊列。也就是手動將js引擎滯后,使js引擎結束,被掛起的GUI渲染線程執行,擁有了初始狀態A后,在執行過度效果就OK了。
解決方法二(推薦):
cDiv.classList.add('easy') cDiv.clientLeft; // 任一觸發頁面回流的方法皆可 cDiv.classList.add('hard')
思路:既然可以讓js引擎滯后,那也可以讓GUI渲染線程提前,用立即觸發回流的任意方法,使之前在渲染隊列中的狀態A生效。 相對優點在于,同樣是觸發回流,方法二從代碼可讀性或操作性上都略勝一籌,優秀團隊有這種追求也是自然而然的。