广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >Javascript单线程和事件循环
  • 456
分享到

Javascript单线程和事件循环

2024-04-02 19:04:59 456人浏览 泡泡鱼
摘要

目录一、单线程二、事件循环三、事件循环的应用四、使用代码来说明五、setTimeout()六、思考:劣质的优化一、单线程 javascript 是单线程的,意味着不会有其他线程来竞争

一、单线程

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单线程 内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: Javascript单线程和事件循环

本文链接: https://www.lsjlt.com/news/150795.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Javascript单线程和事件循环
    目录一、单线程二、事件循环三、事件循环的应用四、使用代码来说明五、setTimeout()六、思考:劣质的优化一、单线程 Javascript 是单线程的,意味着不会有其他线程来竞争...
    99+
    2022-11-13
  • Javascript单线程和事件循环实例分析
    本篇内容介绍了“Javascript单线程和事件循环实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一、单线程Javascript 是...
    99+
    2023-07-02
  • Node.js事件循环(Event Loop)和线程池详解
    Node的“事件循环”(Event Loop)是它能够处理大并发、高吞吐量的核心。这是最神奇的地方,据此Node.js基本上可以理解成“单线程”,同时还允许在后台处理任意的操作。这篇文章将阐明事件循环是如何...
    99+
    2022-06-04
    线程 详解 事件
  • 简单聊聊JavaScript的事件循环机制
    目录前言概念举个栗子TIP再次举个栗子总结前言 JavaScript是一门单线程的弱类型语言,但是我们在开发中,经常会遇到一些需要异步或者等待的处理操作。 类似ajax,亦或者ES6...
    99+
    2022-11-13
  • 详解JavaScript事件循环
    目录一、事件循环的执行过程二、事件循环进阶用法三、JavaScript任务类型3.1 同步任务&异步任务3.2 宏任务&微任务JavaScript事件循环是一种机制,...
    99+
    2023-05-16
    JavaScript事件循环 JavaScript循环
  • JavaScript中事件循环总结
    这篇文章主要讲解了“JavaScript中事件循环总结”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JavaScript中事件循环总结”吧!js中的事件循环因为JavaScript是单线程的...
    99+
    2023-06-20
  • JavaScript中的事件循环方式
    目录概述原理同步>微任务>宏任务案例解析能否先将promise.then分发到微任务中js是单线程,非阻塞,试想如果是多线程,一个线程在某个DOM节点上添加内容,另一个线...
    99+
    2022-11-13
  • JavaScript事件循环实例分析
    这篇文章主要讲解了“JavaScript事件循环实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JavaScript事件循环实例分析”吧!前言异步函数也是有执行顺序的。本质上来说,Ja...
    99+
    2023-07-02
  • JavaScript中怎么实现事件循环
    今天就跟大家聊聊有关JavaScript中怎么实现事件循环,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1.Main.js 执行2.调用second...
    99+
    2022-10-19
  • JavaScript之事件循环案例讲解
    js中的事件循环 因为JavaScript是单线程的,同一事件只能执行一种方法,所以会将程序中的方法加入到执行栈中按照后进先出的顺序依次执行,当遇见异步任务时不会被阻塞,而是将任务放...
    99+
    2022-11-12
  • JavaScript之事件循环的示例分析
    这篇文章主要介绍JavaScript之事件循环的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!js中的事件循环因为JavaScript是单线程的,同一事件只能执行一种方法,所以会将程序中的方法加入到执行栈中按...
    99+
    2023-06-20
  • JavaScript事件循环的原理是什么
    今天小编给大家分享一下JavaScript事件循环的原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。理解 JavaS...
    99+
    2023-07-04
  • JavaScript 关于事件循环机制的刨析
    目录前言:一、事件循环和任务队列产生的原因:二、事件循环机制:三、任务队列:3.1 任务队列的类型:3.2 两者区别:3.3 更细致的事件循环过程四、强大的异步专家 process....
    99+
    2022-11-12
  • javascript事件循环event loop的示例分析
    小编给大家分享一下javascript事件循环event loop的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!具体如下:js是单线程的,但是event loop的出现,使得js...
    99+
    2022-10-19
  • JavaScript 的setTimeout与事件循环机制event-loop
    目录1.先说说我们都知道的setTimeout2.再讲讲我们可能不知道的setTimeoutevent-loop3. node中的时间循环执行顺序4. 关于事件循环中的promise...
    99+
    2022-11-13
  • 深入了解Javascript的事件循环机制
    目录单线程的Javascript同步 vs 异步 宏任务 vs 微任务定时器To Be Continued单线程的Javascript JavaScript是一种单线程语言,它主要用...
    99+
    2022-11-13
  • nodejs中事件和事件循环的示例分析
    这篇文章主要介绍nodejs中事件和事件循环的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!nodejs中的事件循环虽然nodejs是单线程的,但是nodejs可以将操作委托给系统内核,系统内核在后台处理这些...
    99+
    2023-06-14
  • Javascript前端事件循环机制详细讲解
    目录一、消息队列和事件循环1.单线程处理机制2.事件循环机制3.消息队列4.IO线程5.页面使用单线程的缺点二、setTimeout1.浏览器怎么实现 setTimeout2.使用s...
    99+
    2022-12-30
    JavaScript事件循环机制 JS循环机制
  • Node事件循环的流程是什么
    这篇文章主要讲解了“Node事件循环的流程是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Node事件循环的流程是什么”吧!我们都知道目前我们用的应用程...
    99+
    2022-10-19
  • Javascript中怎么利用闭包循环绑定事件
    Javascript中怎么利用闭包循环绑定事件,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。例如:一个不确定长度的列表,在鼠标...
    99+
    2022-10-19
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作