Javascript单线程和事件循环

目录
  • 一、单线程
  • 二、事件循环
  • 三、事件循环的应用
  • 四、使用代码来说明
  • 五、setTimeout()
  • 六、思考:劣质的优化

一、单线程

Javascript 是单线程的,意味着不会有其他线程来竞争。为什么是单线程呢?

假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作:

function changeValue() {
  const e = document.getElementById("ele1");
  if (e) {
    e.value = "VALUE";
  }
}

function deleteElement() {
  const e = document.getElementById("ele1");
  if (e) {
    e.remove();
  }
}

一个线程将执行 changeValue() 函数,如果元素存在就修改元素的值;一个线程将执行 deleteElement() 函数,如果元素存在就删除元素。此时在多线程的条件下,两个函数同时执行,线程 1 执行,判断元素存在,准备执行修改值的代码 e.value = "VALUE";,此时线程 2 抢占了 CPU,执行了 deleteElement() 函数,完整的执行结束,成功删除了元素,CPU 的控制权回到了线程 1,线程 1 继续执行剩下的代码,也就是将要执行的 e.value = "VALUE";,然而因为这个元素被线程 2 删除了,获取不到元素,修改元素的值失败!

能够发现,浏览器环境下,不管有几个线程,都是共享同一个文档(Document),对 DOM 的频繁操作,多线程将带来极大的不稳定性。如果是单线程,则能够保证对 DOM 的操作是极其稳定和可预见的。你永远不用担心有别的线程抢占了资源,做了什么操作而影响到原来的线程。

由于单线程,JS 一次只能处理一个任务,在该任务处理完成之前,其他任务必须等待。这一点非常重要,在理解下面的事件循环前,首先得明确这个概念。

二、事件循环

如你所见,因为浏览器执行 Javascript 是单线程,所以一次只能够执行一个任务。那么当出现多个要执行的任务,其他尚未执行的任务在什么地方等待呢?

为了能够让任务有个可以等待执行的地方,浏览器就建立了一个队列,所有的任务都在队列里等待,当要执行任务的时候,就从队列的队头里拿一个任务来执行,执行过程中,其他任务继续等待。当任务执行完之后,再从队列里拿下一个任务来执行。

可是,除了开发者编写的 Javascript 代码之外,还有很多事件发生,比如浏览器的点击事件,鼠标移动事件,键盘事件,网络请求等。这些事件也需要执行,而且为了客户体验的流畅,需要尽快执行,以更新页面。我们的队列可能有很多任务正在等待执行,如果把浏览器发生的事件排入队列的队尾,那么在前面的任务执行完成之前,浏览器的页面将一直堵塞住,在用户看在,将是非常卡顿的。

为了应对这种问题,浏览器就多加了一个队列,这个队列中的任务,将被尽快执行。为了和前一个队列做区分,前面一个队列就叫宏任务队列吧,这个新加的队列就叫微任务队列吧。宏任务队列的任务叫宏任务,微任务队列里的任务叫微任务。

宏任务队列的执行方式仍不变,还是一次拿一个宏任务来执行。但是在执行完一个宏任务后,就变了,不检查宏任务队列是否为空,而是检查微任务队列是否为空! 如果微任务队列不为空,就执行一个微任务,当前微任务执行完成后,继续检查微任务队列是否为空,如果微任务队列不为空,就再执行一个微任务,直到微任务队列为空。当微任务队列为空后,就渲染浏览器,回到宏任务队列执行,如此循环往复。

通过这种模型,浏览器将需要快速响应的 DOM 事件放入微任务队列,以达到快速执行的目的。当微任务队列执行完成后,便按需要重新渲染浏览器,用户就会感觉自己的操作被迅速地响应了。

这种事件执行方式,称为事件循环。浏览器中的事件和代码,就在事件循环模型下执行。

三、事件循环的应用

通过上图的事件循环模型,我们得知浏览器渲染的顺序,是在执行了一个宏任务和剩下的所有微任务之后,那么为了保证浏览器的渲染顺畅,我们不宜让每一个宏任务的执行事件太长,也不能让清空微任务队列太耗时。一次事件循环中,只执行一个宏任务,那么,对耗时的宏任务需要分解成尽可能小的宏任务,微任务却不同。由于微任务是清空整个微任务队列,所以,在微任务里不要生成新的微任务。毕竟微任务队列的使命就是为了尽可能先处理微任务,然后重新渲染浏览器。

宏任务队列和微任务队列这两者,都是独立于事件循环的,也就是说,在执行 Javascript 代码时,任务队列的添加行为也在发生,即使现在正在清空微任务队列。这是为了避免在执行代码时,发生的事件被忽略。如此可知,即使我们分解一个耗时任务,也不能因为微任务会被优先执行就选择将它分解成多个微任务,这将阻塞浏览器重新渲染。更好的做法是分解成多个宏任务,这样执行一个分解后的宏任务不会太耗时,可以尽快达到让浏览器渲染。

在浏览器的渲染之前,会清空微任务队列,所以,对浏览器 DOM 的修改更新,就适合放到微任务里去执行。

浏览器渲染的次数大概是每秒 60 次,约等于 16ms 一次。在浏览器渲染页面的时候,任何任务都无法再对页面进行修改,这意味着,为了页面的平滑顺畅,我们的代码,单个宏任务和当前微任务队列里所有微任务,都应该在 16ms 内执行完成。否则就会造成页面卡顿。

四、使用代码来说明

我会用一些简单却有效的代码来说明事件循环如何影响页面效果,以下的代码很少,建议你一起编写,体验一下。

先看下面的代码,我定义了一个 foo() 函数,它将一次性往元素中添加 5 万个子元素,我将在页面加载完成后立即执行它。

function foo() {
  const d = document.getElementById("container");
  for (let index = 0; index < 50000; index++) {
    const e = document.createElement("div");
    e.textContent = "NEW";
    d.appendChild(e);
  }
}

可见这是一个耗时的操作,如果你电脑很好,体验不到卡顿的话,可以换成循环 50 万次。

在一阵时间的卡顿后,页面一次性出现了大量子元素。虽说添加元素的目的达到了,但是元素出现之前的卡顿却不能忍受。根据事件循环,我们能够知道,是因为执行了一个非常耗时的宏任务,导致阻塞了页面的渲染。用下面一张图说明。

上面这张图代表着本次事件循环的执行,一开始,浏览器就将 foo() 放进宏任务队列。从 0ms 开始,宏任务队列里有任务,事件循环取出一个宏任务,该宏任务为 foo(),执行,添加 5 万个子元素,执行非常耗时,需要 2000ms(假设的时间),foo() 执行完后,执行微任务,假设我们的清空微任务队列需要执行 5ms,清空后,时间来到了 2005ms,这个时候才能开始重新渲染浏览器。经过了这一次事件循环,竟然耗时了 2015ms!

那么,我们要改善体验,期望是一个平滑的渲染效果。因为浏览器页面的变化,只有在事件循环中重新渲染浏览器这一步才会发生变化,所以我们要做的就是,尽可能快地到事件循环中的渲染浏览器这一步。所以,我们要将这个 foo() 分解成多个宏任务。

为什么不能分解成微任务?因为微任务会在宏任务完成后全部执行。假设我们将添加 5 万 个元素分解成宏任务添加 1000 个,微任务添加 49000 个,那么事件循环还是必须执行完添加 1000 个元素的宏任务后,执行添加 49000 个元素的微任务,才能渲染页面。所以我们要分解成宏任务。

假设我们分解成了 200 个宏任务,每个宏任务都添加 250 个元素,那么,在事件循环执行的时候,任务队列里有 200 个宏任务,取出一个执行,这个宏任务只添加 250 个元素,耗时 10ms。当前宏任务完成后,便清空微任务,耗时 5ms,时间来到了 15ms,就可以渲染浏览器了。这一次事件循环,在渲染浏览器前只耗时 15ms!

接着,渲染浏览器后,页面上出现了 250 个元素,又开始事件循环,从宏任务队列里拿出一个宏任务执行。

如上图所示,接连不断的事件循环使浏览器渲染看起来平滑顺畅。

接下来我们便改造我们的代码,让它分解成多个宏任务。

五、setTimeout()

setTimeout() 函数,用于将一个函数延迟执行,是我们的重点方法。

你应该很熟悉这个函数的用法了,setTimeout() 接收两个参数,第一个是一个回调函数,第二个是数字,用于指示延迟多少时间,以毫秒为单位(ms)。

这里主要介绍的是第二个参数,很多人以为第二个参数是指延迟多少毫秒后执行传进来的函数,但其实,它的真正含义是:延迟多少毫秒后进入宏任务队列

假设如下代码:

setTimeout(() => {
  console.log("execute setTimeout()");
}, 10);

下面我用一张图说明这段代码的执行,图中,上方代表时间轴,下方代表宏任务队列。

在 0ms 时,注册 setTimeout 函数,第一个参数里的方法将在 10ms 后加入宏任务队列,此时,宏任务时没有我们代码里的任务的。

其他我们不知道的 JS 代码执行了 10 ms。

到了 10ms 后,setTimeout 到期,第一个参数里的方法加入宏任务队列。

上图中,10ms 到了,加入了宏任务队列。但是要注意,事件循环此时可能正在执行一个宏任务,或者正在清空微任务队列,或者正在渲染浏览器,所以不会马上执行新增加的宏任务,只有又一次循环到了执行宏任务的时候,才会从宏任务队列中获取宏任务执行(JS 是单线程的)。假设这段时间耗时了 5ms,那么如下图。

如上图所示,在 15ms 的时候,我们才从宏任务队列里取出在 10ms 时放入宏任务队列的宏任务,并执行。和我们的代码对比,尽管 setTimeout 的第二个参数是 10ms,却在 15ms 才执行。

当理解了 setTimeout 的原理之后,便可以使用 setTimeout 将一个耗时的任务分解成多个宏任务,以充分给予浏览器渲染。

我修改了 foo 函数,如下所示:

function foo() {
  const d = document.getElementById("container");
  const total = 50000;
  const size = 250;
  const chunk = total / size;
  let i = 0;
  setTimeout(function render() {
    for (let index = 0; index < size; index++) {
      const e = document.createElement("div");
      e.textContent = "NEW";
      d.appendChild(e);
    }
    i++;
    if (i < chunk) {
      setTimeout(render, 0);
    }
  }, 0);
}

在 foo 方法中,首先获取了要添加子元素的元素,和定义了各种变量。total 表示一共有几个元素要添加,因为我电脑性能差,所以是 5 万,你可以修改成你喜欢的值;size 是指我们分解后每个宏任务要添加几次元素;chunk 是指分解后,一共有几个宏任务,通过简单的计算得到;i 是用于标记执行到了第几个宏任务了。

接下来就是重点了,注册了 setTimeout,在 0ms 后将传入的 render 函数放进宏任务队列里。然后这个 foo 函数就执行结束了,事件循环继续往下执行,清空微任务队列,渲染浏览器。等到下一个事件循环的时候,才会从宏任务队列里拿出由 setTimeout 放入的 render 函数(如果是第一个的话)并执行。

如上图所示,当前的事件循环正在执行 foo() 函数,此时 render() 在宏任务队列中等待。

假设这次事件循环需要的时间是 10ms,那么到了 10ms 后,事件循环开始了新的一轮,从宏任务队列里获取一个新的宏任务,获取到了 render() 任务并执行。来看 render() 函数里的代码:

function render() {
  for (let index = 0; index < size; index++) {
    const e = document.createElement("div");
    e.textContent = "NEW";
    d.appendChild(e);
  }
  i++;
  if (i < chunk) {
    setTimeout(render, 0);
  }
}

代码执行了 for 循环,添加 size 次数的子元素,在示例中 size 定义为了 250,添加 250 个子元素,数量不多,添加过程会非常快。在执行完 for 循环后,将外部的 i 变量加 1,我们将使用 i 判断所有的子元素是否添加完毕,如果是则结束函数,如果不是,则再次通过 setTimeout 注册一个 render() 函数,然后结束当前函数。

如上图,在 15ms 的时候,render() 函数添加了 250 个子元素,然后使用 setTimeout 注册了一个新的宏任务,在 0ms 后进入宏任务队列。注意此时,尽管 render() 函数添加了 250 个子元素,但是事件循环还没有到渲染浏览器这一步,所以页面没有出现 250 个新元素。

事件循环继续执行:

到了 15ms,执行微任务队列,假设需要执行 5ms。到了 20 ms,清空了微任务队列,开始渲染浏览器,假设渲染需要 5ms,界面上出现了 250 个新元素。这次,只花费了 15ms,就让页面上渲染出了元素,而不是一开始那样卡顿了 2000ms 后才页面才渲染!

接下来的事件循环就是一直重复 10ms 开始到 25ms 的动作了,直到所有子元素都渲染完毕。

通过改造后的 foo() 函数,我们将卡顿的页面优化成了观感良好顺畅的页面。从新旧 foo() 函数的代码量来看,代码数量的多少跟页面顺畅与否没有太大关系。重点是理解事件循环中发生的事。

六、思考:劣质的优化

如果我将 foo() 函数改写成如下的形式,会怎么样,亲自试一试,思考执行的事件循环和宏任务队列中发生了什么。

function foo() {
  const d = document.getElementById("container");
  const size = 1000;
  const chunk = 50000 / size;
  for (let index = 0; index < chunk; index++) {
    setTimeout(() => {
      const e = document.createElement("div");
      e.textContent = "NEW";
      d.appendChild(e);
    }, 0);
  }
}

到此这篇关于Javascript单线程和事件循环的文章就介绍到这了,更多相关JS单线程 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于js的事件循环机制剖析

    前言 众所周知, JavaScript是单线程这一核心,可是浏览器又能很好的处理异步请求,那么到底是为什么呢?其中的原理与事件循环机制大有关系. 在探索事件循环之前,我们得先了解浏览器执行线程~~ 浏览器的渲染进程是多线程的,浏览器每一个tab标签都代表一个独立的进程,其中浏览器内核属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等.其包含的线程有以下几种 GUI 渲染线程:负责渲染页面,解析 HTML,CSS 构成 DOM 树: JS 引擎线程:解释执行代码.用户输入和网络请求:

  • JavaScript单线程和任务队列原理解析

    这篇文章主要介绍了JavaScript单线程和任务队列原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.JavaScript为什么设计为单线程? JavaScript语言的一大特点就是单线程,换言之就是同一个时间只能做一件事. for(var j = 0; j < 5; j++) { console.log(j); } console.log('end'); 上面的代码,只有for循环执行完毕,才会执行end: JavaScript的

  • 带你了解NodeJS事件循环

    浏览器中存在两个任务队列,一个是宏任务一个是微任务.但是在NodeJS中一共存在六个事件队列,timers,pending callbacks,idle prepare,poll,check,close callbacks.每一个队列里面存放的都是回调函数callback. 这六个队列是按顺序执行的.每个队列负责存储不同的任务. timer里面存在的是setTimeout与setInterval的回调函数 pending callback是执行操作系统的回调,例如tcp,udp. idle 和

  • JavaScript的单线程和异步详细

    目录 一.任务队列 二.借以解释几个容易困惑的问题 1.setTimeout(f1,0)是什么鬼 2.Ajax请求是否异步 3.界面渲染线程是单独开辟的线程 三.如何利用浏览器的异步机制 四.异步的好处和适合的场景 前言: 说到JavaScript的单线程(single threaded)和异步(asynchronous),很多同学不禁会想,这不是自相矛盾么?其实,单线程和异步确实不能同时成为一个语言的特性.js选择了成为单线程的语言,所以它本身不可能是异步的,但js的宿主环境(比如浏览器,No

  • 浅谈JS三座大山之异步和单线程

    单线程 但是我们在开发中,遇到请求网络,或者定时任务的时候,如果等待网络请求结束或者定时任务结束的时候再去做其他事情,这样页面就会卡住,所以js有异步机制解决这个问题. 异步 异步的特点是不会阻塞后面的代码执行,当同步任务执行完毕之后,再执行异步任务.相对的,同步会阻止代码执行.异步任务的应用主要有网络请求和定时任务. 异步是通过callback的方式实现的,在callback里面执行异步执行的代码,但是有一些场景比如我们有三个网络请求abc需要依次执行,在a的回调里发起b请求,在b的回调里发起

  • 详细分析单线程JS执行问题

    大家在学习javascript的时候很多朋友在执行问题上有疑惑,小编通过本篇文章给大家详细的分析介绍了JS的执行问题,希望能够帮助到你理解. 一.介绍 随着js不断学习,你可能会慢慢的好奇,用了这么久的js,却不知道这js在浏览器怎么被执行的,很尴尬.所以,我查阅很多资料来总结JS的执行过程,也分享出来,和大家一起学习. 本篇主要讲单线程的JS 涉及的名词:JS引擎,单线程,执行栈,执行上下文(execution context) 二.JS引擎 JS引擎是浏览器的重要组成部分,主要用于读取并执行

  • 单线程JavaScript实现异步过程详解

    前两天硬着头皮在部门内部做了一次技术分享,主题如题.索性整理成文章留个纪念! 要了解异步实现,首先我们得先了解: 同步 & 异步 同步:会逐行执行代码,会对后续代码造成阻塞,直至代码接收到预期的结果之后,才会继续向下执行任务. 异步:调用之后先不管结果,继续向下执行任务. 网上各种文章对同步和异步的解释也不外如是,但是看文字总是有点晦涩难懂!我就生活化的来比拟一下这两个概念吧! 就好比请人吃饭: 比如你要请两个人吃饭,一个是巴菲特,由于他是举世瞩目股神想请他吃饭的人从这里排到了法国,你为表诚意,

  • Nodejs探秘之深入理解单线程实现高并发原理

    前言 从Node.js进入我们的视野时,我们所知道的它就由这些关键字组成 事件驱动.非阻塞I/O.高效.轻量,它在官网中也是这么描述自己的. Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. 于是在我们刚接触Node

  • Javascript单线程和事件循环

    目录 一.单线程 二.事件循环 三.事件循环的应用 四.使用代码来说明 五.setTimeout() 六.思考:劣质的优化 一.单线程 Javascript 是单线程的,意味着不会有其他线程来竞争.为什么是单线程呢? 假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作: function changeValue() { const e = document.getElementById("ele1"); if (e) { e.value = "VAL

  • JavaScript中的事件循环方式

    目录 概述原理 同步>微任务>宏任务 案例解析 能否先将promise.then分发到微任务中? js是单线程,非阻塞,试想如果是多线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程. js的事件循环分为主线程(同步)和任务队列(异步),任务队列里又分为宏任务和微任务 1.宏任务(macro-task): script(整体代码),setTimeout,setInterval,setI

  • 在实例中重学JavaScript事件循环

    单线程的JS 众所周知js是一门单线程语言,即同一时间只能做一件事.为什么js是单线程的呢,主要与它的用途有关. 作为浏览器脚本语言,js的主要用途是和用户互动&操作DOM,我们并不想并行的操作DOM.如果不是单线程的话,我们一个线程在给DOM节点上添加内容,另一个线程却删除了这个节点,到底该以哪个为准呢? 所以为了避免复杂性,从一诞生,JavaScript 就是单线程. 事件循环(event loop) JS是一门单线程语言,意味着代码要一行一行的执行.所有任务都要排队,前一个任务结束,才会执

  • JavaScript运行机制之事件循环(Event Loop)详解

    一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. JavaScript的单线程,与它的用途有关.作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM.这决定了它只能是单线程,否则会带来很复杂的同步问题.比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线

  • javascript中的event loop事件循环详解

    前言 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM.这决定了它只能是单线程,否则会带来很复杂的同步问题.比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是

  • 详解JavaScript事件循环机制

    众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心.可看HTML规范中的这段话: To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There a

  • javascript事件循环event loop的简单模型解释与应用分析

    本文实例讲述了javascript事件循环event loop的简单模型解释与应用.分享给大家供大家参考,具体如下: js是单线程的,但是event loop的出现,使得js拥有可以处理高并发的性能.那么怎么理解event loop呢?网上百度一堆文章,什么heap,stack,micro queue,macro queue,让初学者直接懵掉.这里采用很通俗的理解方式介绍下event loop. event loop顾名思义是事件循环,既然是循环,那循环的是什么呢? 对于一个js文件, 1,执行

  • 深入分析JavaScript 事件循环(Event Loop)

    事件循环(Event Loop),是每个JS开发者都会接触到的概念,但是刚接触时可能会存在各种疑惑. 众所周知,JS是单线程的,即同一时间只能运行一个任务.一般情况下这不会引发问题,但是如果我们有一个耗时较多的任务,我们必须等该任务执行完毕才能进入下一个任务,然而等待的这段时间常常让我们无法忍受,因为我们这段时间什么都不能做,包括页面也是锁死状态. 好在,时代在进步,浏览器向我们提供了JS引擎不具备的特性:Web API.Web API包括DOM API.定时器.HTTP请求等特性,可以帮助我们

  • JavaScript之事件循环案例讲解

    js中的事件循环 因为JavaScript是单线程的,同一事件只能执行一种方法,所以会将程序中的方法加入到执行栈中按照后进先出的顺序依次执行,当遇见异步任务时不会被阻塞,而是将任务放入事件队列中,继续执行执行栈中的同步代码,等当前执行栈中的所有任务都执行完毕则查找事件队列中的任务,并把任务的回调函数放入执行栈中,执行其中的同步代码,如此反复形成的循环被称为事件循环. node.js node.js特点 事件驱动 从上向下执行代码,当遇到需要回调的地方就加入到事件队列中,主线程运行完就去执行事件队

  • JavaScript 关于事件循环机制的刨析

    目录 前言: 一.事件循环和任务队列产生的原因: 二.事件循环机制: 三.任务队列: 3.1 任务队列的类型: 3.2 两者区别: 3.3 更细致的事件循环过程 四.强大的异步专家 process.nextTick() 4.1 process.nextTick()在何时调用? 前言: 这次主要整理一下自己对 Js事件循环机制,同步,异步任务,宏任务,微任务的理解,大概率暂时还有些偏差或者错误.如果有,十分欢迎各位纠正我的错误! 一.事件循环和任务队列产生的原因: 首先,JS是单线程,这样设计也是

随机推荐