js: 从setTimeout说事件循环模型

时间:2021-03-17 23:22:50

一、从setTimeout说起

  setTimeout()方法不是ecmascript规范定义的内容,而是属于BOM提供的功能。查看w3school对setTimeout()方法的定义,setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。

  语法setTimeout(fn,millisec),其中fn表示要执行的代码,可以是一个包含javascript代码的字符串,也可以是一个函数。第二个参数millisec是以毫秒表示的时间,表示fn需推迟多长时间执行。

  调用setTimeout()方法之后,该方法返回一个数字,这个数字是计划执行代码的唯一标识符,可以通过它来取消超时调用。

  起初我对 setTimeout()的使用比较简单,对其运行机理也没有深入的理解,直到看到下面代码

 var start = new Date;
setTimeout(function(){
var end = new Date;
console.log('Time elapsed:', end - start, 'ms');
}, 500);
while (new Date - start < 1000) {};

  在我最初对setTimeout()的认识中,延时设置为500ms,所以输出应该为Time elapsed: 500 ms。因为在直观的理解中,Javascript执行引擎,在执行上述代码过程中,应当是一个由上往下的顺序执行过程,setTimeout函数是先于while语句执行的。可是实际上,上述代码运行多次后,输出至少是延迟了1000ms。

 二、根据结果找原因

  通过阅读代码不难看出,setTimeout()方法执行在while()循环之前,它声明了“希望”在500ms之后执行一次匿名函数,这一声明,也即对匿名函数的注册,在setTimeout()方法执行后立即生效。代码最后一行的while循环会持续运行1000ms,通过setTimeout()方法注册的匿名函数输出的延迟时间总是大于1000ms,说明对这一匿名函数的实际调用被while()循环阻塞了,实际的调用在while()循环阻塞结束后才真正执行。

  使用Timer在Java中实现上述逻辑,运行多次,输出都是Time elapsed: 501 ms。java对于定时任务的解决方案是通过多线程手段实现的,任务对象存储在任务队列,由专门的调度线程,在新的子线程中完成任务的执行。通过schedule()方法注册一个异步任务时,调度线程在子线程立即开始工作,主线程不会阻塞任务的运行。

  这就是Javascript与Java/C#之类语言的一大差异,即Javascript的单线程机制。在现有浏览器环境中,Javascript执行引擎是单线程的,主线程的语句和方法,会阻塞定时任务的运行,执行引擎只有在执行完主线程的语句后,定时任务才会实际执行,这期间的时间,可能大于注册任务时设置的延时时间。在这一点上,Javascript与Java/C#的机制很不同。

三、事件循环模型

  在单线程的Javascript引擎中,setTimeout()是如何运行的呢,这里就要提到浏览器内核中的事件循环模型了。简单的讲,在Javascript执行引擎之外,有一个任务队列,当在代码中调用setTimeout()方法时,注册的延时方法会交由浏览器内核其他模块(以webkit为例,是webcore模块)处理,当延时方法到达触发条件,即到达设置的延时时间时,这一延时方法被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立,执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行,这一过程是一个不断循环的过程,称为事件循环模型。

  Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack)。程序中代码依次进入栈中等待执行,当调用setTimeout()方法时,即图中右侧WebAPIs方法时,浏览器内核相应模块开始延时方法的处理,当延时方法到达触发条件时,方法被添加到用于回调的任务队列,只要执行引擎栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些满足触发条件的回调函数。

  以示例进一步说明:

js: 从setTimeout说事件循环模型

js: 从setTimeout说事件循环模型

  以图中代码为例,执行引擎开始执行上述代码时,相当于先讲一个main()方法加入执行栈。继续往下开始console.log('Hi')时,log('Hi')方法入栈,console.log方法是一个webkit内核支持的普通方法,而不是前面图中WebAPIs涉及的方法,所以这里log('Hi')方法立即出栈被引擎执行。

js: 从setTimeout说事件循环模型

js: 从setTimeout说事件循环模型

  console.log('Hi')语句执行完成后,log()方法出栈执行,输出了Hi。引擎继续往下,将setTimeout(callback,5000)添加到执行栈。setTimeout()方法属于事件循环模型中WebAPIs中的方法,引擎在将setTimeout()方法出栈执行时,将延时执行的函数交给了相应模块,即图右方的timer模块来处理。

js: 从setTimeout说事件循环模型

  执行引擎将setTimeout出栈执行时,将延时处理方法交由了webkit timer模块处理,然后立即继续往下处理后面代码,于是将log('SJS')加入执行栈,接下来log('SJS')出栈执行,输出SJS。而执行引擎在执行万console.log('SJS')后,程序处理完毕,main()方法也出栈。

js: 从setTimeout说事件循环模型

js: 从setTimeout说事件循环模型

js: 从setTimeout说事件循环模型

  

  这时在在setTimeout方法执行5秒后,timer模块检测到延时处理方法到达触发条件,于是将延时处理方法加入任务队列。而此时执行引擎的执行栈为空,所以引擎开始轮询检查任务队列是否有任务需要被执行,就检查到已经到达执行条件的延时方法,于是将延时方法加入执行栈。引擎发现延时方法调用了log()方法,于是又将log()方法入栈。然后对执行栈依次出栈执行,输出there,清空执行栈。

  清空执行栈后,执行引擎会继续去轮询任务队列,检查是否还有任务可执行。

四、webkit中timer的实现

  到这里已经可以彻底理解下面代码的执行流程,执行引擎先将setTimeout()方法入栈被执行,执行时将延时方法交给内核相应模块处理。引擎继续处理后面代码,while语句将引擎阻塞了1秒,而在这过程中,内核timer模块在0.5秒时已将延时方法添加到任务队列,在引擎执行栈清空后,引擎将延时方法入栈并处理,最终输出的时间超过预期设置的时间。

 var start = new Date;
setTimeout(function(){
var end = new Date;
console.log('Time elapsed:', end - start, 'ms');
}, 500);
while (new Date - start < 1000) {};

  前面事件循环模型图中提到的WebAPIs部分,提到了DOM事件,AJAX调用和setTimeout方法,图中简单的把它们总结为WebAPIs,而且他们同样都把回调函数添加到任务队列等待引擎执行。这是一个简化的描述,实际上浏览器内核对DOM事件、AJAX调用和setTimeout方法都有相应的模块来处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块,html的解析,css样式的计算等都由webcore实现。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现,这里还是继续以setTimeout为例,看下timer模块的实现。

  Timer类是webkit 内核的一个必需的基础组件,通过阅读源码可以全面理解其原理,本文对其简化,分析其执行流程。

js: 从setTimeout说事件循环模型

  通过setTimeout()方法注册的延时方法,被传递给webcore组件timer模块处理。timer中关键类为TheadTimers类,其包含两个重要成员,TimerHeap任务队列和SharedTimer方法调度类。延时方法被封装为timer对象,存储在TimerHeap中。和Java.util.Timer任务队列一样,TimerHeap同样采用最小堆的数据结构,以nextFireTime作为关键字排序。SharedTimer作为TimerHeap调度类,在timer对象到达触发条件时,通过浏览器平台相关的接口,将延时方法添加到事件循环模型中提到的任务队列中。

  TimerHeap采用最小堆的数据结构,预期延时时间最小的任务最先被执行,同时,预期延时时间相同的两个任务,其执行顺序是按照注册的先后顺序执行。

 var start = new Date;
setTimeout(function(){
console.log('fn1');
}, 20);
setTimeout(function(){
console.log('fn2');
}, 30);
setTimeout(function(){
console.log('another fn2');
}, 30);
setTimeout(function(){
console.log('fn3');
}, 10);
console.log('start while');
while (new Date - start < 1000) {};
console.log('end while');

  上述代码输出依次为

 start while
end while
fn3
fn1
fn2
another fn2

转载自AlloyTeam:http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/

循环间隔:HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏 览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用 requestAnimationFrame()的效果要好于setTimeout()。

转载:http://www.ruanyifeng.com/blog/2014/10/event-loop.html

js: 从setTimeout说事件循环模型的更多相关文章

  1. 【转向Javascript系列】从setTimeout说事件循环模型

    本文首发在alloyteam团队博客,链接地址http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout ...

  2. JS 的线程、事件循环、任务队列简介

    JS 是单线程的,但是却能执行异步任务,这主要是因为 JS 中存在事件循环(Event Loop)和任务队列(Task Queue). 事件循环:JS 会创建一个类似于 while (true) 的循 ...

  3. Node&period;js实战&lpar;九&rpar;之事件循环

    Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高. Node.js 几乎每一个 API 都是支持回调函数的. Node ...

  4. &lbrack;JS&rsqb;异步任务之事件循环

    前言 常常会听到单线程和多线程这两个名词,单线程即一个时间段内程序从上到下执行任务,多线程即一个时间段内程序同时执行多个任务. 然而 JavaScript 是单线程的,它不像 Java 那样新开启一个 ...

  5. js事件循环机制

    本文参考链接:https://www.jianshu.com/p/cf47bc0bf2ab 一.先搞懂两个东西:堆和栈 栈由操作系统自动分配释放,用于存放函数的参数值.局部变量等一些基本的数据类型,其 ...

  6. node&period;js的作用、回调、同步异步代码、事件循环

    http://www.nodeclass.com/articles/39274 一.node.js的作用 I/O的意义,(I/O是输入/输出的简写,如:键盘敲入文本,输入,屏幕上看到文本显示输出.鼠标 ...

  7. JavaScript之JS单线程&vert;事件循环&vert;事件队列&vert;执行栈

    本博文基于知乎"JavaScript作用域问题?"一问,而引起了对JavaScript事件循环和单线程等概念与实践上的研究.深入理解. 一.概念 0.关键词:JavaScript单 ...

  8. 为什么JS是单线程?JS中的Event Loop&lpar;事件循环&rpar;?JS如何实现异步?setimeout?

    https://segmentfault.com/a/1190000012806637 https://www.jianshu.com/p/93d756db8c81 首先,请牢记2点: (1) JS是 ...

  9. 【nodejs原理&源码赏析(7)】【译】Node&period;js中的事件循环,定时器和process&period;nextTick

    [摘要] 官网博文翻译,nodejs中的定时器 示例代码托管在:http://www.github.com/dashnowords/blogs 原文地址:https://nodejs.org/en/d ...

随机推荐

  1. 解决html中&amp&semi;nbsp&semi;在不同浏览器中占位大小不统一的问题

    直接在html文档中使用 来表示空格,在不同浏览器中的占位大小是不一样的. 为什么呢,因为不同浏览器默认的字体是不一样的,不同字体下的空格表示 占位大小不一致. 这就好办了嘛,我们对 指定使用同样的字 ...

  2. C&num;中判断bool 类型 代码的最短写法

    看到一个关于写最短代码的,  是一个bool类型判断的:    public bool IsNull(object val) { if (val == null) { return true; } e ...

  3. ownCloud Virtual Machines&lpar;bitnami&period;com&rpar;

    ownCloud Virtual Machines(bitnami.com)https://bitnami.com/stack/owncloud/virtual-machine

  4. Springboot 入门之Hello World

    首先使用maven进行包加载和配置,但是你maven一定要配置好,maven的setting.xml文件一定要配置好,不然jar包加载不了的. <project xmlns="http ...

  5. U3D操作游戏对象

    游戏对象:所有出现在场景中的实体都是游戏对象. 一.创建游戏对象 创建游戏对象有两种方式:一是通过在unity中创建模型,而是通过脚本动态创建游戏对象.通过脚本动态创建的灵活性较高,重点也在于通过脚本 ...

  6. 来腾讯云开发者实验室 学习&period;NET Core 2&period;0

    腾讯云开发者实验室为开发者提供了一个零门槛的在线实验平台,开发者实验室提供的能力: 零门槛扫码即可免费领取实验机器,支持使用自有机器参与,实验完成后支持保留实验成果: 在线 WEB IDE 支持 sh ...

  7. &lbrack;NOI2005&rsqb;寿司晚宴

    题目描述 为了庆祝NOI的成功开幕,主办方为大家准备了一场寿司晚宴.小G和小W作为参加NOI的选手,也被邀请参加了寿司晚宴. 在晚宴上,主办方为大家提供了n−1种不同的寿司,编号1,2,3,⋯,n-1 ...

  8. CUDA编程模型——组织并行线程2 (1D grid 1D block)

    在”组织并行编程1“中,通过组织并行线程为”2D grid 2D block“对矩阵求和,在本文中通过组织为 1D grid 1D block进行矩阵求和.一维网格和一维线程块的结构如下图: 其中,n ...

  9. 《mysql必知必会》学习&lowbar;第17章&lowbar;20180807&lowbar;欢

    第17章:组合查询 P114 select vend_id ,prod_id,prod_price from products where prod_price <=5 ; select ven ...

  10. ubuntu mongodb backup&sol;restore &lpar;备份和恢复&rpar;

    备份(导出) 1.导出单个collection-.json格式 mongoexport --host:127.0.0.1 --port:27017 --db test --collection tes ...