浏览器渲染问题疑惑

2021-07-22 10:27:16 +08:00
 Alander
<body>
<div id="root">
</div>
</body>
<script>
  const div = document.querySelector('#root')
  div.innerHTML = '1'
  const now = performance.now()
  console.log(div.innerHTML);
  while (performance.now() - now < 100) {
    console.log(now)
  }
  console.log(div.innerHTML);
  div.innerHTML = '2'
  console.log(div.innerHTML);
</script>

不懂就问:

这段代码在 chrome 渲染直接渲染成 2,没有 1 的过程,是为什么?这段 while 阻塞不生效? js 上是符合代码的逻辑的,但是渲染上是直接 2,有什么资料可供查看?

1164 次点击
所在节点    前端开发
10 条回复
runze
2021-07-22 10:36:24 +08:00
cyrbuzz
2021-07-22 12:05:43 +08:00
参考下这个: https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

说下自己的理解,里面提到第 11 步是更新渲染,11 之后还有两步,一个是执行浏览器空闲回调(requestsIdleCallback)一个是 worker 的回调。

11 步之前是执行 JS,JS 主线程就一个线程,UI 渲染会等待 JS 执行,所以你这个阻塞也阻塞了渲染,

```
const div = document.querySelector('#root')
div.innerHTML = '1'
const now = performance.now()
console.log(div.innerHTML);
```
执行到这里如果没有后续代码,浏览器会尝试执行微任务栈,然后执行 UI 渲染,此时就会渲染 1 。

但要注意 innerHTML 实时改变了 DOM,但不是触发渲染的条件,改变 DOM != 渲染,走完这个循环才是执行渲染的条件。

```
while (performance.now() - now < 100) {
console.log(now)
}
console.log(div.innerHTML);
div.innerHTML = '2'
console.log(div.innerHTML);
```

加上下面这些,只是多阻塞了主线程一会,此时走完前面的 1-10,去执行 UI 渲染,而是不断执行 console.log(now),之后执行到了:

```
div.innerHTML = '2'
console.log(div.innerHTML);
```
执行渲染时的 innerHTML 已经是 2,所以渲染出来 2 了。

一般可以用 setTimeout+Promise,这样不会阻塞 UI 渲染,只会阻塞后续代码执行:

```
function sleep(times) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, times)
})
}
```

```
async function draw() {
const div = document.querySelector('#root')
div.innerHTML = '1'
//const now = performance.now()
//console.log(div.innerHTML);
//while (performance.now() - now < 100) {
// console.log(now)
//}
await sleep(100)
console.log(div.innerHTML);
div.innerHTML = '2'
console.log(div.innerHTML);
}

draw()
```

相关扩展可以了解一下 Vue 的$nextTick,曾经某一个版本把它从微任务改到了宏任务,此时用$nextTick 改变 DOM 的 Style 会出现抖动(因为 宏-> 微 -> UI 渲染 -> 宏 2),本来任务应该在 UI 渲染前结果成了 UI 渲染后。
Alander
2021-07-22 12:25:25 +08:00
@cyrbuzz 我可能没表述明白这个事情,页面的表现是:已进入页面直接显示 2,同时控制台不停在打印 console.log(now);

我的疑惑是为什么不是。先渲染出 1,100 毫秒后渲染出 2 或者 先是空白,100ms 后直接渲染 2 ?有点不明白,我去看下你提到的规范说明
cyrbuzz
2021-07-22 13:09:10 +08:00
@Alander

emm,感觉我有说明= =,我简化一下:

innerHTML 并非触发渲染的条件,执行完整个循环才是,包裹你写的阻塞在内的所有代码都属于渲染前执行的任务。
Alander
2021-07-22 13:29:53 +08:00
@cyrbuzz 我理解你的意思,现在的 chrome 表现情况是 js 在执行中,但是页面已经渲染出来了 2,不好意思哈,可能是我没理解你的意思,我只是再确认一下我有没有表述明白:这段 html 在浏览器中执行页面的渲染并没有被阻塞直接渲染出<div id='root'>2</div>,同时 js 代码运行 while 循环,而非先 js 运行完 while 循环再执行渲染。如果说是你明白了我的意思但是我没有明白你的解释真的不好意思哈,我再仔细找下其他资料。
Alander
2021-07-22 13:31:57 +08:00
@cyrbuzz innerHTML 并非触发渲染的条件,执行完整个循环才是,包裹你写的阻塞在内的所有代码都属于渲染前执行的任务。
你的这段话也是我的想法,但是实际页面表现与我的预期不一致,按照这个想法是否是先 while 循环结束后再渲染出字符串 2 ?但是实际上是页面一加载就直接渲染出了 2
Alander
2021-07-22 13:39:55 +08:00
@cyrbuzz 哈哈哈,不好意思,是我理解错浏览器了,我看见的是刷新前渲染的结果误以为是一进来就是 2,不好意思哈,你的解释是正确的
3dwelcome
2021-07-22 13:46:55 +08:00
我测试了一下,阻塞没问题啊。

div.innerHTML = '1'
这句正常执行了,但是被后面的 while()语句阻塞了,所以 1 没办法正常显示出来。

你之所以老是看到页面是 2,是因为上次的页面没刷新。而不是 JS 跳过 while(),自己去执行了后面的 div.innerHTML = '2'
hazardous
2021-07-22 13:54:50 +08:00
100 毫秒太短所以感觉不明显,改成 3000 或者 5000 再试。
Alander
2021-07-22 14:27:49 +08:00
@3dwelcome 是的是的,是上次页面没刷新导致的我的错觉

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/790991

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX