深入理解JavaScript运行机制
深入理解JavaScript运行机制
前言
- 本文是写作在给团队新人培训之际,所以其实本文的受众是对JavaScript的运行机制不了解或了解起来有困难的小伙伴。也就是说,其实真正的原理和本文阐述的并不完全符合,就如中学课本和大学课本一样,大学老师会告诉你高中的一些东西是在某些理想情况下得到的结论,本文同理。
- 本文的目的是希望大家阅读之后能对JavaScript的运行机制有一个比较直观比较快的认识,但更重要的是自己动手实践,只有实践才能真正发现问题和得到提升:)
- 收到了大家的支持和反馈,非常感谢:)
想要理解JavaScript的运行机制,需要分别深刻理解以下几个点:
- JavaScript的单线程机制
- 任务队列(同步任务和异步任务)
- 事件和回调函数
- 定时器
- Event Loop(事件循环)
JavaScript的单线程机制
JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能做一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个。
JavaScript的单线程与它的语言用途是有关的。作为一门浏览器脚本语言,JavaScript的主要用途是完成用户交互、操作DOM。这就决定了它只能是单线程,否则会导致复杂的同步问题。
设想JavaScript同时有两个线程,一个线程需要在某个DOM节点上添加内容,而另一个线程的操作是删除了这个节点,那么浏览器应该以谁为准呢?
所以为了避免复杂性,JavaScript从诞生起就是单线程。
为了提高CPU的利用率,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个标准并没有改变JavaScript单线程的本质。
任务队列
一个接一个地完成任务也就意味着待完成的任务是需要排队的,那么为什么会需要排队呢?
通常排队有以下两种原因:
- 任务计算量过大,CPU处于忙碌状态;
- 任务所需的东西为准备好所以无法继续执行,导致CPU闲置,等待输入输出设备(I/O设备)。> 比如有的任务你需要Ajax获取到数据才能往下执行
由此JavaScript的设计者也意识到,这时完全可以先运行后面已经就绪的任务来提高运行效率,也就是把等待中的任务先挂起放到一边,等得到需要的东西再执行。就好比接电话时对方离开了一下,这时正好有另一个来电,于是你便把当前通话挂起,等那个通话结束后,再连回之前的通话。
所以也就出现了同步和异步的概念,任务也被分成了两种,一种是同步任务(Synchronous),另一种是异步任务(Asynchronous)。
- 同步任务:需要执行的任务在主线程上排队,一个接一个,前一个完成了再执行下一个
- 异步任务:没有马上被执行但需要执行的任务,存放在“任务队列”(task queue)中,“任务队列”会通知主线程什么时候哪个异步任务可以执行,然后这个任务就会进入主线程并被执行。> 所有的同步执行都可以看作是没有异步任务的异步执行
具体来说,异步执行如下:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
也就是所有能被马上执行的任务都在主线程上排好了队,一个接一个的被执行。
- 主线程之外,还存在一个“任务队列”(task queue)。只要异步任务有了运行结果,就在“任务队列”之中放置一个事件。
也就是说每个异步任务准备好了就会立一个唯一的flag,这个flag用来标识对应的异步任务。
- 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,就结束等待装袋,进入执行栈开始被执行。
也就是主线程把之前的任务做完了之后,就会来看“任务队列”中的flag,来把对应的异步任务打包来执行。
- 主线程不断重复以上三步。
只要主线程空了,就会去读取“任务队列”。这个过程会被不断重复,这就是JavaScript的运行机制。
事件和回调函数
事件
“任务队列”是一个事件的队列(也可以理解成是消息的队列),IO设备完成一项任务,就会在“任务队列”中添加一个时间,表示相关的异步任务可以进入“执行栈”。接着主线程读取“任务队列”,查看里面有哪些事件。
“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。
回调函数
所谓“回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,如果包含“定时器”,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
Event Loop
主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的运行机制又称为“Event Loop”(事件循环)
为了更好地理解Event Loop,下面参照Philip Roberts的演讲中的一张图。

上图中,主线程在运行时,产生了heap(堆)和stack(栈),栈中的代码调用各种外部API,并在“任务队列”中加入各种事件(click,load,done)。当栈中的代码执行完毕,主线程就会读取“任务队列”,并依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
上面的代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取“任务队列”。所以,它与以下的写法是等价的。
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面是无关紧要的,因为它们属于执行栈的一部分,系统总是执行完它们才会去读取“任务队列”。
定时器
除了放置异步任务的事件,“任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做定时器(timer)功能,也就是定时执行的代码。
SetTimeout()和setInterval()可以用来注册在指定时间之后单次或重复调用的函数,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者会在指定毫秒数的间隔里重复调用:
setInterval(updateClock, 60000); //60秒调用一次updateClock()
因为它们都是客户端JavaScript中重要的全局函数,所以定义为Window对象的方法。
但作为通用函数,其实不会对窗口做什么事情。
Window对象的setTImeout()方法用来实现一个函数在指定的毫秒数之后运行。所以它接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。 setTimeout()和setInterval()返回一个值,这个值可以传递给clearTimeout()用于取消这个函数的执行。
console.log(1);
setTimeout(function(){console.log(2);}, 1000);
console.log(3);
上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。
如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
setTimeout(function(){console.log(1);}, 0);
console.log(2)
上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会执行“任务队列”中的回调函数。
总之,setTimeout(fn,o)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是尽可能早地执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的事件都处理完,才会的到执行。
HTML5标准规定了
setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
需要注意的是,setTimeout()只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。
由于历史原因,setTimeout()和setInterval()的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval())。
关于深入理解定时器的工作原理,这里推荐阅读jQuery的作者John Resig的一篇文章: http://ejohn.org/blog/how-javascript-timers-work/
我自己也翻译了这篇文章,如有问题,欢迎指正:http://guoxunique.com/2016/12/07/how-javascript-timers-work/
参考阮一峰老师的博文 http://www.ruanyifeng.com/blog/2014/10/event-loop.html
参考《JavaScript权威指南》
深入理解JavaScript运行机制的更多相关文章
- JavaScript运行机制与setTimeout
前段时间,老板交给了我一个任务:通过setTimeout来延后网站某些复杂资源的请求.正好借此机会,将JavaScript运行机制和setTimeout重新认真思考一遍,并将我对它们的理解整理如下. ...
- javascript运行机制
太久没更新博客了,Javascript运行机制 Record it 1.代码块 JavaScript中的代码块是指由<script>标签分割的代码段.例如: <script type ...
- 从setTimeout谈JavaScript运行机制
从setTimeout说起 众所周知,JavaScript是单线程的编程,什么是单线程,就是说同一时间JavaScript只能执行一段代码,如果这段代码要执行很长时间,那么之后的代码只能尽情地等待它执 ...
- JavaScript可否多线程? 深入理解JavaScript定时机制(转载)
说明:最近写 js 时需要用setinterval函数做定时操作,谁知道,刚开始后运行完好,但一段时间后他就抽风了,定时任务运行的时间间隔越来越短,频率加快,这是一个完全不能容忍的问题,带着一个可以出 ...
- JavaScript运行机制详解
JavaScript运行机制详解 var test = function(){ alert("test"); } var test2 = function(){ alert(& ...
- 深入浅出JavaScript运行机制
一.引子 本文介绍JavaScript运行机制,这一部分比较抽象,我们先从一道面试题入手: console.log(1); setTimeout(function(){ console.log(3); ...
- Javascript 运行机制
先看一下下面这段js代码: console.log('1'); setTimeout(function(){ console.log('2'); },0); console.log('3'); 请问打 ...
- JavaScript 运行机制 & EventLoop
JavaScript 运行机制 & EventLoop 看阮老师博客和自己的理解,记录的学习笔记,js的单线程和 事件EventLoop 机制. 1. JavaScript是单线程 JavaS ...
- 阅读《深入理解JavaScript定时机制》
鸟哥的这篇<深入理解JavaScript定时机制>从javascript线程角度分析了setTimeout和setInterval两个定时触发器的实现原理. 看完的体验就是主要要记住两点: ...
随机推荐
- spark 官方文档(1)——提交应用程序
Spark版本:1.6.2 spark-submit提供了在所有集群平台提交应用的统一接口,你不需要因为平台的迁移改变配置.Spark支持三种集群:Standalone.Apache Mesos和Ha ...
- 部署React+webpack工程的步骤
# 部署React+webpack工程的步骤ps:以Mac os系统做开发环境.因为npm现在使用灰常的慢,所以我使用淘宝境像cnpm. 1,准备工作: 先确保存已经安装了node.js: 2,文件部 ...
- android3D动画,绕y轴旋转
原文地址:http://blog.csdn.net/x_i_a_o_h_a_i/article/details/40449847 其实网上的3D旋转的例子很多,在这里我只是想把其代码做一个解释. 先上 ...
- centos 7 配置网络
文档: https://wiki.centos.org/FAQ/CentOS7
- Xcode 属性面板添加自定义控件属性
让自定义控件像原生控件一样可以在属性面板配置参数,Apple文档传送 直接上效果图,根据
- Linux 命令备忘录(CentOS 7)
创建目录testdir: mkdir testdir 进入目录testdir:cd testdir 在testdir中创建空文件 1: touch 1 在testdir中创建空文件 2: t ...
- mysql character set exception
问题: 插入数据时,报了这样一个错误:“_mysql_exceptions.Warning: Incorrect string value: ‘\xE6\xB5\x81\xE8\xA1\x8C…’ f ...
- 浅析Java中的final关键字(转载)
自http://www.cnblogs.com/dolphin0520/p/3736238.html转载 一.final关键字的基本用法 在Java中,final关键字可以用来修饰类.方法和变量(包括 ...
- iOS书写高质量代码之耦合的处理
原创 2016-12-26 MrPeak MrPeak杂货铺 耦合是每个程序员都必须面对的话题,也是容易被忽视的存在,怎么处理耦合关系到我们最后的代码质量.今天Peak君和大家聊聊耦合这个基本功话题, ...
- MFC如何读取XML
<?xml version="1.0" encoding="utf-8"?> <Cases> <case> <No&g ...