本篇内容介绍了“Javascript单线程和事件循环实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Javascript 是单线程的,意味着不会有其他线程来竞争。为什么是单线程呢?假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作:一个线程将执行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 万个子元素,我将在页面加载完成后立即执行它。可见这是一个耗时的操作,如果你电脑很好,体验不到卡顿的话,可以换成循环 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()
接收两个参数,第一个是一个回调函数,第二个是数字,用于指示延迟多少时间,以毫秒为单位(ms)。这里主要介绍的是第二个参数,很多人以为第二个参数是指延迟多少毫秒后执行传进来的函数,但其实,它的真正含义是:延迟多少毫秒后进入宏任务队列!假设如下代码:下面我用一张图说明这段代码的执行,图中,上方代表时间轴,下方代表宏任务队列。在 0ms 时,注册setTimeout
函数,第一个参数里的方法将在 10ms 后加入宏任务队列,此时,宏任务时没有我们代码里的任务的。其他我们不知道的 JS 代码执行了 10 ms。到了 10ms 后,setTimeout
到期,第一个参数里的方法加入宏任务队列。上图中,10ms 到了,加入了宏任务队列。但是要注意,事件循环此时可能正在执行一个宏任务,或者正在清空微任务队列,或者正在渲染浏览器,所以不会马上执行新增加的宏任务,只有又一次循环到了执行宏任务的时候,才会从宏任务队列中获取宏任务执行(JS 是单线程的)。假设这段时间耗时了 5ms,那么如下图。如上图所示,在 15ms 的时候,我们才从宏任务队列里取出在 10ms 时放入宏任务队列的宏任务,并执行。和我们的代码对比,尽管setTimeout
的第二个参数是 10ms,却在 15ms 才执行。当理解了setTimeout
的原理之后,便可以使用setTimeout
将一个耗时的任务分解成多个宏任务,以充分给予浏览器渲染。我修改了foo
函数,如下所示:在foo
方法中,首先获取了要添加子元素的元素,和定义了各种变量。total
表示一共有几个元素要添加,因为我电脑性能差,所以是 5 万,你可以修改成你喜欢的值;size
是指我们分解后每个宏任务要添加几次元素;chunk
是指分解后,一共有几个宏任务,通过简单的计算得到;i
是用于标记执行到了第几个宏任务了。接下来就是重点了,注册了setTimeout
,在 0ms 后将传入的render
函数放进宏任务队列里。然后这个foo
函数就执行结束了,事件循环继续往下执行,清空微任务队列,渲染浏览器。等到下一个事件循环的时候,才会从宏任务队列里拿出由setTimeout
放入的render
函数(如果是第一个的话)并执行。如上图所示,当前的事件循环正在执行foo()
函数,此时render()
在宏任务队列中等待。假设这次事件循环需要的时间是 10ms,那么到了 10ms 后,事件循环开始了新的一轮,从宏任务队列里获取一个新的宏任务,获取到了render()
任务并执行。来看render()
函数里的代码:代码执行了 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()
函数改写成如下的形式,会怎么样,亲自试一试,思考执行的事件循环和宏任务队列中发生了什么。“Javascript单线程和事件循环实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注百云主机网站,小编将为大家输出更多高质量的实用文章!
本文小编为大家详细介绍“怎么用C#实现合并Word文档功能”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么用C#实现合并Word文档功能”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。本次测试时,在程序中引入Free Spire.…
免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。