点进来的前端佬,先别走!
让我详细给你逼逼叨!

在很久很久以前,前端圈就广泛流传,Javascript的加载和执行,都会阻塞浏览器Render。
然后过了这些日子,作为一名优秀的前端佬的意识爆发。
按照上面的说法,那是不是可以构造一个Javascript程序,让后续的CSS以及HTML文本永远都不能被解析Render到?
喔,觉的挺来劲的,说干就干!

前言
一开始构建了这么一个HTML,如下:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<h1 id="start" class="h1-title">开始渲染了</h1>
<script>
console.log(document.getElementById('start'))
</script>
</body>
</html>
<script>
// 此处插入代码
</script>
<h1 class="h1-title">看到这里就失败了!</h1>
<style>
.h1-title {
color: red;
}
</style>
预想阻塞js代码会写在script
标签里。
以上代码运行后如下:

以上展示的,因为没有填入代码,符合期望。
这里解释下为什么要将script
脚本和h1
要放在html之外。
因为根据各个资料上说,浏览器解读HTML文本就是从上往下解析的。当遇到</html>
文档结束标签,就会开始生成DOM树+CSSOM树,并开始Render。
那我脑袋一拍,灵光一闪,自以为是的将需要Render的HTML和CSS放在</html>
后,期望只Render第一行文字开始渲染了
,而第二行文字看到这里就失败了!
就永远得不到Render。
开始挑战!!!
方法一 递归
脑子第一个蹦出来的方法,就是用递归,来模拟JavaScript阻塞。
在上面HTML模板中填入如下代码:
js
function block() {
Math.sqrt(Math.random());
block();
}
block();
结果如下:

失败了,还在控制器里报了一个错误.RangeError: Maximum call stack size exceeded
。
oh,shit,明显这里我忽略了一个细节。
大家都知道的,Javascript是单线程运行机制。
而Javascript的函数分为解析和调用。解析有一个入栈的过程,调用有一个出栈过程。当入栈停止后,才会出栈被调用执行。而上面递归代码,构造了一个无限入栈的场景,结果就是直接撑爆内存。
很显然,浏览器识别到这种风险,直接作出报错处理。
失败~继续尝试!
方法二 while死循环
有了JS的单线程执行思路,顺理成章的,就有了使用while死循环,来模拟阻塞。
插入如下代码试一试。
js
while (true) {
// 持续执行同步任务
Math.sqrt(Math.random());
}
效果如下:

喔!成功了???? ★,° :.☆( ̄▽ ̄)/$:.°★ 。

其实并没有~
之所以能有上面的效果,在于我使用了VSCODE中的Live Server
插件,并构造了特殊的场景。基本原来就是Live Server是有热更新,我动态插入了</html>
之后的代码到文件中。

究其原因,在现代浏览器中,浏览器有着强大的纠错机制。很多浏览器都不会遇到</html>
就停止解析,忽略后续的文本。他们仍然会好心好意 的将后续能看懂的文本,插入到<body>
里去。
所以实际上,正常的去执行上面构造的代码,只能得到如下效果:

但现在离成功,也算走了一半!
动态插入的思路,让我想到了第三个方法。
方法三 按钮手动添加代码
这就是构造一个添加
按钮,点击之后,动态添加上HTML标签和Script脚本。
初始是这个样子的:

HTML代码构造如下:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<h1 id="start" class="h1-title">开始渲染了</h1>
<button id="execute">添加</button>
<script>
function sleepBlocking(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// 什么都不做,纯粹阻塞
}
}
document.getElementById('execute').onclick = () => {
now1 = new Date().getTime()
// 在前
document.getElementById('execute').insertAdjacentHTML('beforebegin', `
<h1 class='h1-title'>看到这里就失败了!</h1>
<style>
.h1-title {
color: red;
}
<\/style>
`)
// 在后
const script = document.createElement("script");
// 5秒后执行
script.innerHTML = 'sleepBlocking(5000);console.log("休眠后", new Date().getTime() - now1)'
console.log('所有脚本添加后', new Date().getTime() - now1)
document.body.appendChild(script);
}
</script>
</body>
</html>
从上面的代码可以看到,我弄了一个阻塞执行的5秒函数。接下来预期的效果就是:
点击前,先展示黑色的文字开始渲染了
点击添加
按钮后,经过5秒后,就会使得所有文字变红,并出现看到这里就失败了!
的效果,最终如下图:

符合预期!!完美~
以上就是整个验证的思路了,个人觉的基本可以回答标题上的问题。Javascript是真的会阻塞浏览器Render!!
另外还有一种思路,就是使用stream
来构造一个一直会执行的远程脚本,为避免无聊,这里就不尝试了,都是大差不差的。
如果还能看到这里的前端佬,那我想说在这个尝试的过程还有一个意外,就是我们经常会看到很多技术类文档,解说Event Loop
,都会用上宏任务和微任务解释,个人觉的有点牵强不太行。感兴趣接着往下看!
方法四 构造永不结束的"宏任务"?
先贴下Event Loop
的一些解释:
- 从宏任务的头部取出一个任务执行;
- 执行过程中若遇到微任务则将其添加到微任务的队列中;
- 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
- GUI 渲染;
- 回到步骤 1,直到宏任务执行完毕;
按照上面思路,是不是我我构造一个永远不结束的宏任务,也可以阻塞Render???
现在我把Javascript代码替换成如下:
js
function setTime() {
Math.sqrt(Math.random());
setTimeout(() => {
setTime()
}, 1)
}
setTime()
然后我们看到的效果确实是这样的。

并没有阻止,定时器任务还在依旧运行代码。
所以,我是不太相信网上那些所谓的事件循环的解释了!
另外我自己去找权威书籍《JavaScript高级程序设计(第4版)》 和 《JavaScript权威指南(第7版)》,英文版本,连那些词都没得~
嗯....先这样吧。
看到这里,我是想说,我这篇表情包很克制了!前端佬们给点小心心吧♥(ˆ◡ˆԅ)
