https://b23.tv/mP8MLu

https://www.cnblogs.com/rubyxie/articles/9797564.html

思考先列代码用户在隐藏之前不会看到这个元素闪一下吗?

  1. <script>
  2. let el = document.createElement('div');
  3. el.innerText = '隐藏';
  4. document.body.appendChild(el);
  5. el.style.display = 'none';
  6. </script>

解析:
不会,js代码必须执行之后,浏览器才会执行渲染;事件环可以确保你的任务在下一次渲染之前完成。


while (true) 循环占用主线程

  1. <body>
  2. <div>待正式进入百度在线翻译界面后就可以在右侧查看并完成翻译操作了。</div>
  3. <img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1598981084742&di=e69163e0313900483ff9cd286ce105fe&imgtype=0&src=http%3A%2F%2Fimg.ui.cn%2Fdata%2Ffile%2F9%2F3%2F4%2F1918439.gif%3FimageMogr2%2Fauto-orient%2Fstrip" alt="">
  4. <button>while (true);</button>
  5. <script>
  6. let button = document.querySelector('button');
  7. button.addEventListener('click', event=>{
  8. while (true){
  9. console.log(44);
  10. }
  11. })
  12. </script>
  13. </body>

这是一个带有gif和一些文本的页面,以及触发无线循环的按钮;当点击按钮,一切gif停止了,也无法选择文字了;
这是因为这个无线循环永远不结束,它一直执行JavaScript,主线程被占用,导致其他任务不能执行了。


loop 循环(setTimeout)

  1. <body>
  2. <div>待正式进入百度在线翻译界面后就可以在右侧查看并完成翻译操作了。</div>
  3. <img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1598980416736&di=d42a745f9609141a19e4c9e8cdaaf555&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253d580%2Fsign%3D5829e57e37d3d539c13d0fcb0a86e927%2F1392853df8dcd1002b623da1738b4710b8122f0d.jpg" alt="">
  4. <button onclick="loop()">点击</button>
  5. <script>
  6. function loop() {
  7. setTimeout(loop, 0)
  8. }
  9. </script>
  10. </body>

image.png
解析:
这也是一个循环,在setTimeout的回调中调用自身,如图:它可以绕到渲染那一侧做渲染,这就是setTimeout循环没有阻止渲染的原因;也是由于setTimeout是宏任务。


requestAnimationFrame

以下代码是一个进度条

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <style>
  9. .dddd {
  10. width: 300px;
  11. border-radius: 100px;
  12. height: 6px;
  13. background-color: #ebeef5;
  14. overflow: hidden;
  15. position: relative;
  16. vertical-align: middle;
  17. }
  18. #kkkk {
  19. position: absolute;
  20. left: 0;
  21. top: 0;
  22. height: 100%;
  23. background-color: #409eff;
  24. text-align: right;
  25. border-radius: 100px;
  26. line-height: 1;
  27. white-space: nowrap;
  28. // 思考为什么加入动画后,requestAnimationFrame 执行完之后才会发生渲染
  29. /*transition: width 0.001s ease;*/
  30. }
  31. </style>
  32. <div class="dddd">
  33. <div id="kkkk"></div>
  34. </div>
  35. <script>
  36. let aa = 0, times = 0;
  37. function callback(time) {
  38. let dd = document.getElementById('kkkk');
  39. console.log(time-times);
  40. times = time;
  41. aa += 1;
  42. dd.style.width = `${aa}px`;
  43. if (aa >= 300) return;
  44. requestAnimationFrame(callback)
  45. }
  46. callback()
  47. </script>
  48. </body>
  49. </html>

image.png
浏览器帧图

image.png
事件环

  • 发生在每个帧的开头,包括样式计算,布局和绘制,不一定三个都有。
  • 如图:假设是显示给用户的帧图,浏览器的渲染。
  • requestAnimationFrame 回调函数运行处理在CSS之前和绘制之前。
  • requestAnimationFrame 每秒执行60次。

事件代码中多次操作dom,只有最后一次生效

  1. <body>
  2. <div id="box">显示影藏</div>
  3. <button>button</button>
  4. <script>
  5. let button = document.querySelector('button');
  6. let box = document.getElementById('box');
  7. button.addEventListener('click', event=>{
  8. box.style.display = 'none';
  9. box.style.display = 'block';
  10. box.style.display = 'none';
  11. box.style.display = 'block';
  12. box.style.display = 'none';
  13. box.style.display = 'block';
  14. box.style.display = 'none';
  15. })
  16. </script>

我们在一段代码中,频繁修改dom显示隐藏,只有最后一行起作用;
原因:其实js中每次设置dom的显示隐藏是生效的,但是由于主线程占用,没有时间去更新渲染,多有等主线程中任务执行完后,box 已经设置为隐藏了,并且这个时候浏览器进程渲染了,我们看到的是box隐藏了。

如上段代码,执行过程

  1. 点击按钮,鼠标点击会触发一个事件回调(由浏览器进程触发,通过IPC将任务发给渲染进程的消息队列),需要执行一个宏任务(渲染)
  2. 等待requestAnimationFrame函数回调执行,浏览器dom重新渲染。

实现一个动画,X位置从1000移动到50效果

  1. <body>
  2. <div id="box">显示影藏</div>
  3. <button id="buttons">button</button>
  4. <script>
  5. let button = document.getElementById('buttons');
  6. let box = document.getElementById('box');
  7. button.addEventListener('click', event=>{
  8. box.style.transform = 'translateX(1000px)';
  9. box.style.transition = 'transform 1s ease-in-out';
  10. box.style.transform = 'translateX(500px)';
  11. })
  12. </script>
  13. </body>

有个对象,我想把它的X位置从1000移动到500,如上代码实际是对象从0到500;
我们要的效果是1000移动到500,这是由于浏览器不会在乎中间的变化,所以它会忽略第一个动画;

第二个动画放入 requestAnimationFrame 中;他仍然是动画0到500。

  1. <body>
  2. <div id="box">显示影藏</div>
  3. <button id="buttons">button</button>
  4. <script>
  5. let button = document.getElementById('buttons');
  6. let box = document.getElementById('box');
  7. button.addEventListener('click', event=>{
  8. box.style.transform = 'translateX(1000px)';
  9. box.style.transition = 'transform 1s ease-in-out';
  10. requestAnimationFrame((time)=>{
  11. box.style.transform = 'translateX(500px)';
  12. });
  13. })
  14. </script>
  15. </body>

image.png
位置1:这里是一个任务,所以我们来到任务队列,这里是执行translate 和 transform 的地方;
位置2:我们加入了动画帧,并且开始执行,这里我们设置移动的终点translateX(500px)。但是浏览器不会考虑css,直接到达紫色区域进行渲染。(rAF黄色区域是计算css的地方,它没有显示第一个位置,因为它没有考虑中间样式的变化)

用两个 requestAnimationFrame, 动画可以1000 移动到 500

  1. button.addEventListener('click', event=>{
  2. box.style.transform = 'translateX(1000px)';
  3. box.style.transition = 'transform 1s ease-in-out';
  4. requestAnimationFrame(()=>{
  5. requestAnimationFrame(()=>{
  6. box.style.transform = 'translateX(500px)';
  7. });
  8. });
  9. })

可以使用 getComputedStyle,只需要访问其中一个属性,迫使浏览器更早的执行样式计算,会让浏览器记下你在此之前设置的所有内容,但是这种做法要小心,因为你最终可能会让浏览器在一帧的时间内做不少多余的工作。

实际解决这个问题的最佳方法:使用 web animation API, 目前只有 Chrome支持。

requestAnimationFrame 是在渲染之前运作的,一些浏览将rAF 放在了渲染之后(Edge 和 Safari),这是错的;

  1. button.addEventListener('click', event=>{
  2. box.style.transform = 'translateX(1000px)';
  3. box.style.transition = 'transform 1s ease-in-out';
  4. getComputedStyle(box).transition;
  5. box.style.transform = 'translateX(500px)';
  6. })

微任务

我个人将微任务和Promise关联起来想,微任务的初衷是浏览器想提供给开发者一种监听DOM变化的方法;
w3c说可以,于是提供了DOM变化事件
这段代码的逻辑是,在节点加入达到 body 元素的时候得到通知

  1. <body>
  2. <div id="app"></div>
  3. <button onclick="handleClick()">按钮</button>
  4. </body>
  5. <script>
  6. function handleClick () {
  7. let div = document.createElement('div');
  8. document.body.appendChild(div);
  9. div.innerText = '插入元素';
  10. }
  11. document.body.addEventListener('DOMNodeInserted', () => {
  12. console.log('Stuff added to <body>');
  13. })
  14. </script>

但是实践中存在问题,像是这段代码,我添加100个span,你预计会产生多少个事件呢?一个事件?还是整个行动只产生一个事件?100个span产生100个事件,为span设置文本的行为会产生事件,并且冒泡,这段简单的代码最终会产生200个事件,最终触发了成千上万的事件,即使你的回调函数很简单,很快会变成大量的工作,降低性能

和之前rAF相似,我们希望浏览器暂时不要处理,只想被通知1次而不是200次,解决的方案是使用DOM变化事件的观察者,它们创建了新的队列叫做微任务,我们读到的微任务的大量文档表明他发生在每次事件循环或者是发生在一项任务之后,某种程度上是对的,事件环中有特定的地方处理微任务,通常是意想不到的,微任务也会在JavaScript 结束后执行,因为此时JavaScript 堆栈中已经没有可以执行的内容了,所以最终,微任务可以在任务中执行,也可能在渲染阶段作为RAF的一部分,任何JavaScript 运行的时候都可能执行微任务

这意味着这段代码会运行到结束。

Promise 也使用了微任务

  1. Promise.resolve().then(res=>console.log('Hey'));
  2. console.log('Yo');

Promise 也使用了微任务,所以在这里我们排队了一个微任务,输出‘Yo!’, JavaScript 执行结束,于是处理微任务,输出“Hey!”,这就意味着当Promise 回调执行时,能确定中间没有JavaScript 执行,Promise 的回调位于堆栈底部时,这就是Promise 使用微任务的原因。

loop 循环(Promise.resolve())

我们使用微任务创建一个无线循环会怎么样呢?像之前setTimeout做的

  1. <body>
  2. <div>待正式进入百度在线翻译界面后就可以在右侧查看并完成翻译操作了。</div>
  3. <img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1598980416736&di=d42a745f9609141a19e4c9e8cdaaf555&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253d580%2Fsign%3D5829e57e37d3d539c13d0fcb0a86e927%2F1392853df8dcd1002b623da1738b4710b8122f0d.jpg" alt="">
  4. <button onclick="loop()">点击</button>
  5. <script>
  6. function loop() {
  7. Promise.resolve().then(loop);
  8. }
  9. </script>
  10. </body>

动画不动了,也不能选取文本,效果和之前的 while true 循环相同,和 setTimeout 不一样。
Promise 回调是异步的,但是 “异步” 实际意味什么?它意味同步的代码之后执行。这就是为什么先输出Yo!然后输出Hey!。
但是异步并不意味着它必须让步于渲染,并不意味着它必须让步于事件循环的任何特定部分。

我们提到了三个不同的队列~任务队列

image.png
RAF回调队列执行requestAnimationFrame回调的地方,现在我们来看微任务队列;
就像我们在任务队列中看到的那样,我们只每次执行一个任务,如果另一个任务加进来,就添加到队列尾部;
如果在动画回调内部又有动画回调。它们会在下一帧执行;
微任务同样也是一直执行,直到队列为空,如果处理微任务的过程中有新的微任务加进来,加入添加的速度比执行快,那么就永远执行微任务。事件环会阻塞,直到微任务队列完全清空,这就是他阻塞渲染的原因。

JavaScript 小测验1

  1. <button>按钮</button>
  2. <script>
  3. let button = document.querySelector('button');
  4. button.addEventListener('click', () => {
  5. Promise.resolve().then(()=>console.log('Microtask 1'));
  6. console.log('Listener 1');
  7. });
  8. button.addEventListener('click', () => {
  9. Promise.resolve().then(()=>console.log('Microtask 2'));
  10. console.log('Listener 2');
  11. });
  12. button.click();
  13. </script>

在同一个按钮上面有两个一样的事件监听器
上面是让浏览器自动执行,没有点击,那就是打印出 listener1 ,listener2,micro task1,micro task2,走微任务
如果换成是点击的就是listen1,micro task1,listen2,m2 就是走宏任务了

JavaScript 小测验2

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <a href="https://www.baidu.com/" target="_blank">baobu</a>
  9. <script>
  10. let link = document.querySelector('a');
  11. const nextClick = new Promise(resolve => {
  12. link.addEventListener('click', resolve, {onece: true})
  13. })
  14. nextClick.then(event=>{
  15. console.log(545);
  16. event.preventDefault()
  17. })
  18. link.click();
  19. </script>
  20. </body>
  21. </html>