首先声明,JavaScript是单线程运行(JavaScript引擎线程)事件驱动。
一、浏览器中有多个线程
一款浏览器中包含的最基本的线程:
1、JavaScript引擎线程。
2、定时器线程,setInterval和setTimeout会触发这个线程。
3、浏览器事件触发线程,这个线程会触发onclick、onmousemove和其它浏览器事件。
4、界面渲染线程,负责渲染浏览器界面HTML元素。注意:在JavaScript引擎运行脚本期间,界面渲染线程都是处于挂起状态的。也就是说当使用JavaScript对界面中的节点进行操作时,并不会立即体现出来,要等到JavaScript引擎线程空闲时,才会体现出来。(这个最后说)
5、HTTP请求线程(Ajax请求也在其中)。
以上这些线程在浏览器内核的控制下,相互配合,完成工作(具体我也不知道)。
二、任务队列
我们知道JavaScript是单线程的,所有JavaScript代码都在JavaScript引擎线程中运行。阮一峰老师的文章中叫这个线程为主线程,是一个执行栈。(以下内容也主要是根据阮一峰老师的文章理解总结。)
这些JavaScript代码我们可以把他们看成一个个的任务,这些任务有同步任务和异步任务之分。同步任务(比如变量赋值语句,alert语句,函数声明语句等等)直接在主线程上按顺序执行,异步任务(比如浏览器事件触发线程触发的各种各样的事件,使用Ajax返回的服务器响应等)按照时间先后顺序在任务队列(也可以叫做事件队列、消息队列)中排队,等待被执行。只要主线程上的任务执行完了,就会去检查任务队列,看有没有排队等待的任务,有就让排队的任务进入主线程执行。
比如下面的例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta content="width=device-width, initial-scale=1"> <title>定时机制</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div> </div> <script> var pro = document.getElementById('test'); pro.onclick = function() { alert('我没有立即被执行。'); }; function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } test(); </script> </body> </html>
在这个例子中test()函数执行完大概要8~9秒,所以当我们打开这个页面,在8秒之前点击粉色方块,不会立即弹出提示框,而要等到8秒之后才弹出,而且8秒之前点击几次粉色框,8秒之后就弹出几次。
我们打开这个页面时,主线程先声明函数test,再声明变量pro,然后把p节点赋值给pro,然后给p节点添加click事件,并指定回调函数(挂起),然后调用test函数,执行其中的代码。在test函数中的代码执行期间,我们点击了p节点,浏览器事件触发线程检测到这个事件,就把这个事件放在了任务队列中,以便主线程上的任务(这里是test函数)执行完后,检查任务队列时发现这个事件并执行相应的回调函数。如果我们多次点击,这些多次触发的事件就按触发时间的先后在任务队列中排队(可以再给另外一个元素添加点击事件,交替点击不同的元素来验证)。
下面是总结的任务的运行机制:
异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
1、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2、主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3、一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4、主线程不断重复上面的第三步。
三、事件和回调函数
我们在给DOM元素指定事件时,都会指定一个回调函数,以便事件真的发生时执行相应的代码。
主线程中事件的回调函数会被挂起,如果任务队列中有正在排队的相应的事件,当主线程检测到时就会执行相应的回调函数。我们也可以说主线程执行异步任务,就是在执行相应的回调函数。
四、事件循环
主线程检查任务队列中事件的过程是循环不断的,因此我们可以画一个事件循环的图: