点进来的前端佬,先别走!
让我详细给你逼逼叨!
在很久很久以前,前端圈就广泛流传,Javascript的加载和执行,都会阻塞浏览器Render。
然后过了这些日子,作为一名优秀的前端佬的意识爆发。
按照上面的说法,那是不是可以构造一个Javascript程序,让后续的CSS以及HTML文本永远都不能被解析Render到?
喔,觉的挺来劲的,说干就干!
前言
一开始构建了这么一个HTML,如下:
1<!DOCTYPE html> 2<html> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>demo</title> 7</head> 8<body> 9 <h1 id="start" class="h1-title">开始渲染了</h1> 10 <script> 11 console.log(document.getElementById('start')) 12 </script> 13</body> 14</html> 15<script> 16 // 此处插入代码 17</script> 18<h1 class="h1-title">看到这里就失败了!</h1> 19<style> 20 .h1-title { 21 color: red; 22 } 23</style> 24
预想阻塞js代码会写在script标签里。
以上代码运行后如下:
以上展示的,因为没有填入代码,符合期望。
这里解释下为什么要将script脚本和h1要放在html之外。
因为根据各个资料上说,浏览器解读HTML文本就是从上往下解析的。当遇到</html>文档结束标签,就会开始生成DOM树+CSSOM树,并开始Render。
那我脑袋一拍,灵光一闪,自以为是的将需要Render的HTML和CSS放在</html>后,期望只Render第一行文字开始渲染了,而第二行文字看到这里就失败了!就永远得不到Render。
开始挑战!!!
方法一 递归
脑子第一个蹦出来的方法,就是用递归,来模拟JavaScript阻塞。
在上面HTML模板中填入如下代码:
1function block() { 2 Math.sqrt(Math.random()); 3 block(); 4} 5block(); 6
结果如下:
失败了,还在控制器里报了一个错误.RangeError: Maximum call stack size exceeded。
oh,shit,明显这里我忽略了一个细节。
大家都知道的,Javascript是单线程运行机制。
而Javascript的函数分为解析和调用。解析有一个入栈的过程,调用有一个出栈过程。当入栈停止后,才会出栈被调用执行。而上面递归代码,构造了一个无限入栈的场景,结果就是直接撑爆内存。
很显然,浏览器识别到这种风险,直接作出报错处理。
失败~继续尝试!
方法二 while死循环
有了JS的单线程执行思路,顺理成章的,就有了使用while死循环,来模拟阻塞。
插入如下代码试一试。
1while (true) { 2 // 持续执行同步任务 3 Math.sqrt(Math.random()); 4} 5
效果如下:
喔!成功了???? ★,°:.☆( ̄▽ ̄)/$:.°★ 。
其实并没有~
之所以能有上面的效果,在于我使用了VSCODE中的Live Server插件,并构造了特殊的场景。基本原来就是Live Server是有热更新,我动态插入了</html>之后的代码到文件中。
究其原因,在现代浏览器中,浏览器有着强大的纠错机制。很多浏览器都不会遇到</html>就停止解析,忽略后续的文本。他们仍然会好心好意的将后续能看懂的文本,插入到<body>里去。
所以实际上,正常的去执行上面构造的代码,只能得到如下效果:
但现在离成功,也算走了一半!
动态插入的思路,让我想到了第三个方法。
方法三 按钮手动添加代码
这就是构造一个添加按钮,点击之后,动态添加上HTML标签和Script脚本。
初始是这个样子的:
HTML代码构造如下:
1<!DOCTYPE html> 2<html> 3 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>demo</title> 8</head> 9<body> 10 <h1 id="start" class="h1-title">开始渲染了</h1> 11 <button id="execute">添加</button> 12 <script> 13 function sleepBlocking(ms) { 14 const start = Date.now(); 15 while (Date.now() - start < ms) { 16 // 什么都不做,纯粹阻塞 17 } 18 } 19 document.getElementById('execute').onclick = () => { 20 now1 = new Date().getTime() 21 // 在前 22 document.getElementById('execute').insertAdjacentHTML('beforebegin', ` 23 <h1 class='h1-title'>看到这里就失败了!</h1> 24 <style> 25 .h1-title { 26 color: red; 27 } 28 <\/style> 29 `) 30 // 在后 31 const script = document.createElement("script"); 32 // 5秒后执行 33 script.innerHTML = 'sleepBlocking(5000);console.log("休眠后", new Date().getTime() - now1)' 34 console.log('所有脚本添加后', new Date().getTime() - now1) 35 document.body.appendChild(script); 36 } 37 </script> 38</body> 39</html> 40
从上面的代码可以看到,我弄了一个阻塞执行的5秒函数。接下来预期的效果就是:
点击前,先展示黑色的文字开始渲染了
点击添加按钮后,经过5秒后,就会使得所有文字变红,并出现看到这里就失败了!的效果,最终如下图:
符合预期!!完美~
以上就是整个验证的思路了,个人觉的基本可以回答标题上的问题。Javascript是真的会阻塞浏览器Render!!
另外还有一种思路,就是使用stream来构造一个一直会执行的远程脚本,为避免无聊,这里就不尝试了,都是大差不差的。
如果还能看到这里的前端佬,那我想说在这个尝试的过程还有一个意外,就是我们经常会看到很多技术类文档,解说Event Loop,都会用上宏任务和微任务解释,个人觉的有点牵强不太行。感兴趣接着往下看!
方法四 构造永不结束的“宏任务”?
先贴下Event Loop的一些解释:
- 从宏任务的头部取出一个任务执行;
- 执行过程中若遇到微任务则将其添加到微任务的队列中;
- 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
- GUI 渲染;
- 回到步骤 1,直到宏任务执行完毕;
按照上面思路,是不是我我构造一个永远不结束的宏任务,也可以阻塞Render???
现在我把Javascript代码替换成如下:
1function setTime() { 2 Math.sqrt(Math.random()); 3 setTimeout(() => { 4 setTime() 5 }, 1) 6} 7setTime() 8
然后我们看到的效果确实是这样的。
并没有阻止,定时器任务还在依旧运行代码。
所以,我是不太相信网上那些所谓的事件循环的解释了!
另外我自己去找权威书籍《JavaScript高级程序设计(第4版)》 和 《JavaScript权威指南(第7版)》,英文版本,连那些词都没得~
嗯....先这样吧。
看到这里,我是想说,我这篇表情包很克制了!前端佬们给点小心心吧♥(ˆ◡ˆԅ)