一 前言
相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 。在初期许多人会把异步理解成类似多线程的编程模式,其实他们中有着很大的差别,要完全理解异步,就需要了解 JS 的运行核心——事件循环(event loop)。
二 JS引擎的两大特点:单线程和非阻塞
单线程
JS引擎是基于单线程(Single-threaded)事件循环的概念构建的。同一时刻只运行一个代码块在执行,与之相反的是像JAVA和C++一样的语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不出错。
非阻塞
非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞是通过事件循环机制实现的。
JS通常是非阻塞的,除了某些特殊情况,JS会停止代码执行:
- alert, confirm, prompt(除了Opera)
- “页面上的程序正忙”的系统对话框弹出
三 事件队列
首先,我们先来看一段简单的代码:
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 1000);
console.log("script end");
我们可以看到,首先,程序输出 ‘script start’ 和 ‘script end’,在大约1s之后输出了 ‘setTimeout’ 。该程序的 ‘script end’ 并没有等待1s之后输出,而是立即输出。这是因为 setTimeout 是一个异步的函数。意思也就是说当我们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束之后,事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务过后便将任务压入执行栈(stack)当中,执行栈执行任务,输出 ‘setTimeout’。
事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。
四 事件循环机制
由于 JS 是单线程的,同步执行任务会造成浏览器的阻塞,所以我们将 JS 分成一个又一个的任务,通过不停的循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。
所以事件循环的运行机制大致分为以下步骤:
1.检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行 2;
2.取出事件队列的首部,压入执行栈;
3. 执行任务;
4.检查执行栈,如果执行栈为空,则跳回第 1 步;如不为空,则继续检查;
五 总结
关于事件循环,我们需要记住以下几点:
(1)事件队列严格按照时间先后顺序将任务压入执行栈执行;
(2)当执行栈为空时,浏览器会一直不停的检查事件队列,如果不为空,则取出第一个任务;
(3)在每一个任务结束之后,浏览器会对页面进行渲染;