学习
实践
活动
专区
工具
TVP
写文章
专栏首页李维亮的博客事件循环是如何影响页面渲染的?

事件循环是如何影响页面渲染的?

JavaScript 是单线程的,但提供了很多异步调用方式比如 setTimeout,setInterval,setImmediate,Promise.prototype.then,postMessage,requestAnimationFrame, I/O,DOM 事件等。 这些异步调用的实现都是事件循环,但根据插入的队列不同和取任务的时机不同他们的表现也不同。 尤其在涉及与页面渲染的关系时。

TL;DR

  • 页面渲染/交互任务也会插入在 Task Queue 中,会与各种异步机制插入的任务交错执行。
  • Microtask Queue 会在下一个任务开始之前清空。
  • 单个耗时任务和 Microtask Queue 都会阻塞页面交互,Task Queue 则不影响。
  • 渲染时机可以通过 requestAnimationFrame 精确控制。
  • setImmediate 与 setTimeout 一样使用 Task Queue,但克服了 4ms 限制。

任务与队列的概念 JavaScript 的异步机制由 事件循环 实现,这些 API 的不同表现在进入和离开任务队列的时机。 为了讨论方便,先解释几个概念。

  • 任务与调用栈。由于单线程的特性,每个 JavaScript 执行上下文只有一个调用栈,其中保存着当前任务中所有未执行完的函数。只要调用栈非空,JavaScript 引擎就会持续地、不被打断地(从进程内的角度来看)执行完当前栈中的所有函数,因此 JavaScript 有 “run-to-completion” 的特性。调用栈被清空时意味着当前任务执行结束。
  • Task Queue 是事件循环的主要数据结构。当前调用栈为空时(上一个任务已经完成),事件循环机制会持续地轮询 Task Queue,只要队列中有任务就拿出来执行。在任务执行期间插入的任务会进入 Task Queue 尾部。会加入 Task队列的包括:setTimeout, setInterval, setImmediate,postMessage,MessageChannel,UI 事件,I/O,页面渲染。
  • Microtask Queue 在 Task Queue 的每个任务执行结束后,下一个任务执行开始前,会执行并清空 Microtask Queue 中的所有任务。在 Microtask 执行期间插入的任务也会进入当前 Microtask Queue。会加入MicroTask 队列的包括:Promise, MutationObserver,process.nextTick。

上述异步 API 的分类依据的是最新标准或最新实现。存在一些例外,比如:Node < 9 的 process.nextTick 实现的是 Task 语义(而非 Microtask);IE8 中的 postMessage 是同步的;Edge 浏览器在点击事件处理函数之间不会清空 Microtask Queue。

无论是 Task Queue 还是 Microtask Queue,其中的 task 和 microtask 的执行都是异步的。 为了对上述两个队列有更直观的认识,这里举个例子:

setTimeout(() => console.log('setTimeout'));
Promise.resolve().then(() => {
    console.log('Promise');
    Promise.resolve().then(() => console.log('Promise queued by Promise'));
});
console.log('stack');

上述代码片段中有两个Task(stack, setTimeout),两个 Microtask(Promise、Promise queued by Promise)。 stack 是当前任务会先执行;setTimeout 是第二个任务,在它执行前会清空 Microtask Queue。 这时 Microtask Queue 只有一个 Microtask(Promise), 在它执行的过程中会插入第二个 Microtask(Promise queued by Promise)。 这些 Microtask 都会在下一个 Task(setTimeout)之前执行。因此输出为:

stack
Promise
Promise queued by Promise
setTimeout
  • 注意与 .then 的回调不同,new Promise 的回调是同步执行的。可参考 Promise 回调的执行 一文。
  • 在 Jake 的 Tasks, microtasks, queues and schedules一文中有更加详细的例子,感兴趣的读者可前往观摩。

何时会阻塞 UI

UI 渲染和交互的处理是通过 Task Queue 来调度的,因此耗时任务会导致渲染和交互任务得不到调用,也就是页面“卡死”。 典型的浏览器会在每秒插入 60 个渲染帧,也就是说每 16ms 需要一次渲染。 如果存在一个任务在 16ms 内未能执行结束,页面就会掉帧给人卡顿的感觉。

在 “Loop for 10 seconds” 部分我们写了 4 种不同的循环,它们的表现如下:

循环 API

队列类型

期间页面能否交互 &ast;

每秒执行次数

while(true)

当前任务

701665.8

Promise

Microtask Queue

609555.4

setTimeout

Task Queue

208.3

requestAnimationFrame

Task Queue

59

页面不可交互是指:无法点击其他按钮、无法操作输入控件、无法选择/赋值页面文本。 以 PC Chrome 为例,iOS Safari 尤其是 UIWebview 的表现可能会不同。 单个的耗时任务和 Microtask Queue 都会阻塞页面交互,Task 则不影响。 因为 Task 之间浏览器有机会会插入 UI 任务。 这里还可以观察到 setTimeout 虽然设置了 0 延时但调用次数远小于 while,甚至远小于 Microtask。 下文 setImmediate 章节会详细讨论原因。

渲染任务的时机

有时我们希望精确地控制浏览器在每一帧的绘制,这时就要了解浏览器绘制的时机。 首先举个例子,我们希望页面背景闪现一下红色:

document.body.style.background = 'red';
document.body.style.background = 'white';

上述代码一定达不到效果,背景会稳定地呈现白色。 因为 JavaScript “run-to-completion” 的特性,在上述两行代码之间不可能插入渲染任务。 这时可能有人想到 setTimeout:

document.body.style.background = 'red';
setTimeout(function () {
    document.body.style.background = 'white';
})

这样两次背景设置会在不同的任务中执行,如果这两个任务之间插入了渲染任务背景就会发生闪动。 但渲染任务是 16ms 一次,你怎么知道浏览器会正好插入在这两个任务之间? 因此上述代码只会几率性起作用,背景闪动的几率大概 4/16.67 = 25%。 16.67 是渲染帧间隔,那为什么是 4ms 呢?请看下文 setImmediate。

想要增大几率到 100% 怎么办?setTimeout 100ms 呗… 其实 HTML5 中给出了 requestAnimationFrame API,使得脚本有机会精确地控制动画:

requestAnimationFrame(function () {
    document.body.style.background = 'red';
    requestAnimationFrame(function () {
        document.body.style.background = 'white';
    })
})

所以 setImmediate 是啥 setImmediate 是由 IE 提出的, 目前尚未形成标准。当前状态是 Proposal 且只有 IE 有实现。 setImmediate 是为了让脚本更快地执行,与 setTimeout 一样都使用 Task Queue。 为了解 setImmediate 的用途,我们先看 setTimeout 为什么不够快。 下面的文本来自 HTML5 Living Standard 的 timer initialization steps:

If timeout is less than 0, then set timeout to 0. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4. Increment nesting level by one.

其中 nesting level 是指 timer nesting level, 每一级可能是 setTimeout 也可能是 setInterval。也就是说在嵌套 5 层以上时,会设置最小 4ms 的延迟。 setImmediate 意在让脚本有机会在 UA 事件和渲染发生后立即得到调用,从渲染的角度上类似于渲染之后调用的 requestAnimationFrame。 由于没有广泛实现,使用 setImmediate 需要引入 Polyfill。请参考:

https://github.com/YuzuJS/setImmediate/blob/master/README.md 插入的任务会在每次渲染任务之前执行,因此等待渲染之后需要调用两次来插入到第二次渲染之前。 这样背景一定会闪现红色。

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:http://www.liweiliang.com/复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 企业面试题: 浏览器是如何渲染页面的?

    第二步:当本地的域名服务器收到请求后,就先查询本地的缓存,如果有该纪录项,则本地的域名服务器就直接把查询的结果返回。

    舒克
  • 循环神经网络(RNN)是如何循环的?

    循环神经网络(RNN:Recurrent Neural Network)是一种主要用于处理和预测序列数据的神经网络。

    enenbobu
  • bash for循环是如何使用的

    用户8418197
  • 事件是如何到达activity的?

    android的view管理是以window为单位的,每个window对应一个view树。这里管理涉及到view的绘制以及事件分发等。Window机制不仅管理着...

    Rouse
  • HTML/CSS/JS 是如何在浏览器中,渲染成你看到的页面?【图解Chrome】

    Chrome 算是程序员的标配了,从全球的市场份额来看,它在全球市场的份额已经超过 60%。

    一墨编程学习
  • 边缘渲染是如何提升前端性能的?

    在讲ESR(Edge Side Rendering,边缘渲染)如何提速渲染之前,我们有必要先了解一下前端渲染的发展历史以及前端各项性能指标优化是如何被提上议程的...

    唐志远
  • Node 事件循环究竟是如何工作的: 为何大部分的事件循环图都是错的

    当 Bert 在 2016 年欧洲 Node 交流大会上提出关于事件循环的主题时,他以一句“大部分的事件循环图都是错的”开场。我很愧疚,我演讲中也用过一些错误的...

    疯狂的技术宅
  • 页面是如何生成的(宏观角度)

    当你启动一个应用程序,对应的进程就被创建。进程可能会创建一些线程用于帮助它完成部分工作,新建线程是一个可选操作。在启动某个进程的同时,操作系统(OS)也会分配内...

    前端柒八九
  • Spring是如何解决循环依赖的

    在面试的时候这两年有一个非常高频的关于spring的问题,那就是spring是如何解决循环依赖的。这个问题听着就是轻描淡写的一句话,其实考察的内容还是非常多的,...

    纪莫
  • Spring 是如何解决循环依赖的?

    正要创建的 bean 记录在缓存中,Spring 容器架构一个正在创建的 bean 标识符放在一个 “当前创建 bean 池”中国, 因此如果在创建 Bean ...

    王小明_HIT
  • Spring 是如何解决循环依赖的?

    Requested bean is currently in creation: Is there an unresolvable circular refer...

    程序员小航
  • JavaScript 异步执行的学习笔记 - 什么是事件循环 Event loop?

    使用像 JavaScript 这样的语言进行编程时,最重要但也经常被误解的部分之一是如何表达和操作一段需要某段时间才能完成执行的程序行为。

    Jerry Wang
  • 时序约束是如何影响Vivado编译时间的

    常有工程师会抱怨,自己的Vivado工程从综合到生成bit文件太耗时,尤其是在调试阶段,一天跑不出一个版本,压力骤增。抛开FPGA芯片本身容量大、设计复杂等因素...

    Lauren的FPGA
  • 硬盘是如何影响数据库性能的?

    松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程

    江南一点雨
  • 移动端touch事件影响click事件以及在touchmove添加preventDefault导致页面无法滚动的解决方法

    这两天自己在写一个手机网页,用到了触屏滑动的特效,就是往右滑动的时候左侧隐藏的菜单从左边划出来。

    yaphetsfang
  • spring:我是如何解决循环依赖的?

    最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的,直到遇到...

    苏三说技术
  • 再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的?

    侄子:那你赶紧给我妈花吧,我妈要是跑了,你还得花钱娶一个,到最后,钱我捞不着,亲妈还混没了

    青石路

扫码关注腾讯云开发者

领取腾讯云代金券