理解JS事件循环


Posted in Javascript onJanuary 07, 2016

伴随着JavaScript这种web浏览器脚本语言的普及,对它的事件驱动交互模型,以及它与Ruby、Python和Java中常见的请求-响应模型的区别有一个基本了解,对您是有益的。在这篇文章中,我将解释一些JavaScript并发模型的核心概念,包括其事件循环和消息队列,希望能够提升你对一种语言的理解,这种语言你可能已经在使用但也许并不完全理解。

这篇文章是写给谁的?

这篇文章是针对在客户端或服务器端使用或计划使用JavaScript的web开发人员的。如果你已经精通事件循环,那么这篇文章的大部分对你来说会很熟悉。对于那些还不是很精通的人,我希望能给你提供一个基本的了解,这样可以更好地帮助你阅读和编写日常代码。

非阻塞I / O

在JavaScript中,几乎所有的I/O都是非阻塞的。这包括HTTP请求,数据库操作和磁盘读写,单线程执行要求在运行期执行一个操作时,提供一个回调函数,然后继续做其它的事情。当操作已经完成时,消息和已提供的回调函数一起插入到队列。在将来的某个时候,消息从队列移除,回调函数触发。

虽然这种交互模型可能对已经习惯使用用户界面的开发人员很熟悉,比如“mousedown,”和“click”事件在某一时刻被触发。这与通常在服务器端应用程序进行的同步式请求-响应模型是不同的。

让我们来比较一下两小块代码,发出HTTP请求到www.google.com和输出响应到控制台。首先看看Ruby,配合使用Faraday(一个Ruby 的HTTP 客户端开发库):

response = Faraday.get 'http://www.google.com'
puts response
puts 'Done!'

执行路径很容易跟踪:

1、执行get方法,执行的线程等待,直到收到响应
2、从谷歌收到响应并返回给调用者,它存储在一个变量中
3、变量的值(在本例中,就是我们的响应)输出到控制台
4、值“Done!“输出到控制台
让我们使用Node.js和Request库在JavaScript做同样的事情:

request('http://www.google.com', function(error, response, body) {
 console.log(body);
});
 
console.log('Done!');

表面上看略有不同,实际行为截然不同:

1、执行请求函数,传递一个匿名函数作为回调,当响应在将来某个时候可用时执行回调。
2、“Done!“立即输出到控制台
3、在将来的某个时候,响应返回和回调执行时,输出它的内容到控制台
事件循环

将调用者和响应解耦,使得JavaScript在运行期等待异步操作完成和回调触发时可以做其他事情。但是这些回调在内存中是如何组织的,按什么顺序执行?什么导致他们被调用?

JavaScript运行时包含一个消息队列,它存储了需要处理的消息的列表和相关的回调函数。这些消息是以队列的形式来响应回调函数所涉及的外部事件(如鼠标单击或收到HTTP请求的响应)的。例如,如果用户单击一个按钮,但没有提供回调函数,那么也没有消息会被加入队列。

在一次循环,队列提取下一条消息(每次提取称为一次“tick”),当事件发生,该消息的回调执行。

理解JS事件循环

回调函数的调用在调用栈作为初始化frame(片段),由于JavaScript是单线程的,未来的消息提取和处理因为等待栈的所有调用返回而被停止。后续(同步)函数调用会添加新的调用frame到栈(例如,函数init调用函数changeColor)。

function init() {
 var link = document.getElementById("foo");
 
 link.addEventListener("click", function changeColor() {
  this.style.color = "burlywood";
 });
}
 
init();

在这个例子中,当用户单击“foo”元素时,一条消息(及其回调函数changeColor)会被插入到队列,并触发“onclick“事件。当消息离开队列时,其回调函数changeColor被调用。当changeColor返回(或者是抛出一个错误),事件循环仍在继续。只要函数changeColor存在,并指定为“foo”元素的onclick方法的回调,那么在该元素上单击会导致更多的消息(和相关的回调changeColor)插入队列。

队列附加消息

如果一个函数在代码中按异步调用(比如setTimeout),提供的回调将最终作为一个不同的消息队列的一部分被执行,它将发生在事件循环的某个未来的动作上。例如:

function f() {
 console.log("foo");
 setTimeout(g, 0);
 console.log("baz");
 h();
}
 
function g() {
 console.log("bar");
}
 
function h() {
 console.log("blix");
}
 
f();

由于setTimeout的非阻塞特性,它的回调将在至少0毫秒后触发,而不是作为消息的一部分被处理。在这个示例中,setTimeout被调用, 传入了一个回调函数g且延时0毫秒后执行。当我们指定时间到达(当前情况是,几乎立即执行),一个单独的消息将被加入队列(g作为回调函数)。控制台打印的结果会是像这样:“foo”,“baz”,“blix”,然后是事件循环的下一个动作:“bar”。如果在同一个调用片段中,两个调用都设置为setTimeout -传递给第二个参数的值也相同-则它们的回调将按照调用顺序插入队列。

Web Workers

使用Web Workers允许您能够将一项费时的操作在一个单独的线程中执行,从而可以释放主线程去做别的事情。worker(工作线程)包括一个独立的消息队列,事件循 环,内存空间独立于实例化它的原始线程。worker和主线程之间的通信通过消息传递,看起来很像我们往常常见的传统事件代码示例。

理解JS事件循环

首先,我们的worker:

// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
 pi = SomeLib.computePiToSpecifiedDecimals(e.data);
 postMessage(pi);
};
 
onmessage = reportResult;

然后,主要的代码块在我们的HTML中以script-标签存在:

// our main code, in a <script>-tag in our HTML page
var piWorker = new Worker("pi_calculator.js");
var logResult = function(e) {
 console.log("PI: " + e.data);
};
 
piWorker.addEventListener("message", logResult, false);
piWorker.postMessage(100000);

在这个例子中,主线程创建一个worker,同时注册logResult回调函数到其“消息”事件。在worker里,reportResult函数注册到自己的“消息”事件中。当worker线程接收到主线程的消息,worker入队一条消息同时带上reportResult回调函数。消息出队时,一条新消息发送回主线程,新消息入队主线程队列(带上logResult回调函数)。这样,开发人员可以将cpu密集型操作委托给一个单独的线程,使主线程解放出来继续处理消息和事件。

关于闭包的

JavaScript对闭包的支持,允许你这样注册回调函数,当回调函数执行时,保持了对他们被创建的环境的访问(即使回调的执行时创建了一个全新的调用栈)。理解我们的回调作为一个不同的消息的一部分被执行,而不是创建它的那个会很有意思。看看下面的例子:

function changeHeaderDeferred() {
 var header = document.getElementById("header");
 
 setTimeout(function changeHeader() {
  header.style.color = "red";
 
  return false;
 }, 100);
 
 return false;
}
 
changeHeaderDeferred();

在这个例子中,changeHeaderDeferred函数被执行时包含了变量header。函数 setTimeout被调用,导致消息(带上changeHeader回调)被添加到消息队列,在大约100毫秒后执行。然后 changeHeaderDeferred函数返回false,结束第一个消息的处理,但header变量仍然可以通过闭包被引用,而不是被垃圾回收。当 第二个消息被处理(changeHeader函数),它保持了对在外部函数作用域中声明的header变量的访问。一旦第二个消息 (changeHeader函数)执行结束,header变量可以被垃圾回收。

提醒

JavaScript 事件驱动的交互模型不同于许多程序员习惯的请求-响应模型,但如你所见,它并不复杂。使用简单的消息队列和事件循环,JavaScript使得开发人员在构建他们的系统时使用大量asynchronously-fired(异步-触发)回调函数,让运行时环境能在等待外部事件触发的同时处理并发操作。然而,这不过是并发的一种方法。

以上就是本文的全部内容,希望对大家的学习有所帮助。

Javascript 相关文章推荐
jQuery 行级解析读取XML文件(附源码)
Oct 12 Javascript
jquery中对表单的基本操作代码
Jul 29 Javascript
jquery判断字符输入个数(数字英文长度记为1,中文记为2,超过长度自动截取)
Oct 15 Javascript
js导出table到excel同时兼容FF和IE示例
Sep 03 Javascript
纯js写的分页表格数据为json串
Feb 18 Javascript
JavaScript常用字符串与数组扩展函数小结
Apr 24 Javascript
jQuery Easyui使用(二)之可折叠面板动态加载无效果的解决方法
Aug 17 Javascript
AngularJS自定义指令实现面包屑功能完整实例
May 17 Javascript
bootstrap实现二级下拉菜单效果
Nov 23 Javascript
Vue框架之goods组件开发详解
Jan 25 Javascript
layui 实现表格某一列显示图标
Sep 19 Javascript
vue实现点击追加选中样式效果
Nov 01 Javascript
angularjs创建弹出框实现拖动效果
Aug 25 #Javascript
JavaScript中定义类的方式详解
Jan 07 #Javascript
javascript类型系统 Window对象学习笔记
Jan 07 #Javascript
直接拿来用的页面跳转进度条JS实现
Jan 06 #Javascript
基于jQuery实现鼠标点击导航菜单水波动画效果附源码下载
Jan 06 #Javascript
JavaScript动态创建div等元素实例讲解
Jan 06 #Javascript
谈谈JavaScript类型系统之Math
Jan 06 #Javascript
You might like
jQuery each()方法的使用方法
2010/03/18 Javascript
基于jquery的direction图片渐变动画效果
2010/05/24 Javascript
jQuery右键菜单contextMenu使用实例
2011/09/28 Javascript
JS实现从顶部下拉显示的带动画QQ客服特效代码
2015/10/24 Javascript
JS触摸屏网页版仿app弹窗型滚动列表选择器/日期选择器
2016/10/30 Javascript
vue日期组件 支持vue1.0和2.0
2017/01/09 Javascript
详解webpack分离css单独打包
2017/06/21 Javascript
Nodejs之http的表单提交
2017/07/07 NodeJs
详解从Vue.js源码看异步更新DOM策略及nextTick
2017/10/11 Javascript
vue-cli之router基本使用方法详解
2017/10/17 Javascript
关于Angularjs中自定义指令一些有价值的细节和技巧小结
2018/04/22 Javascript
vue 实现在函数中触发路由跳转的示例
2018/09/01 Javascript
Nuxt.js SSR与权限验证的实现
2018/11/21 Javascript
使用vue制作滑动标签
2019/09/21 Javascript
使用Python的Supervisor进行进程监控以及自动启动
2014/05/29 Python
python迭代器实例简析
2014/09/25 Python
实现python版本的按任意键继续/退出
2016/09/26 Python
Python与R语言的简要对比
2017/11/14 Python
python模拟表单提交登录图书馆
2018/04/27 Python
Python实现的从右到左字符串替换方法示例
2018/07/06 Python
python实现鸢尾花三种聚类算法(K-means,AGNES,DBScan)
2019/06/27 Python
Python操作excel的方法总结(xlrd、xlwt、openpyxl)
2019/09/02 Python
Windows10下 python3.7 安装 facenet的教程
2019/09/10 Python
Python Pandas数据分析工具用法实例
2020/11/05 Python
python全栈开发语法总结
2020/11/22 Python
python 制作简单的音乐播放器
2020/11/25 Python
关于解决iframe标签嵌套问题的解决方法
2020/03/04 HTML / CSS
狗狗玩具、零食和咀嚼物的月度送货服务:Super Chewer
2018/08/22 全球购物
捷克多品牌在线时尚商店:ANSWEAR.cz
2020/10/03 全球购物
伊莱克斯阿根廷网上商店:Tienda Electrolux
2021/03/08 全球购物
平面网站制作专科生的自我评价分享
2013/12/11 职场文书
小学德育工作经验交流材料
2014/05/22 职场文书
2014机关党员干部“正风肃纪”思想汇报
2014/09/15 职场文书
大学生简历自我评价2015
2015/03/03 职场文书
少先队中队工作总结2015
2015/07/23 职场文书
分析SQL窗口函数之取值窗口函数
2022/04/21 Oracle