JavaScript工作原理之事件循环和基础的异步实现
事件循环
常规的JavaScript引擎是单线程的,也就是说所有的代码块都是顺序按照顺序被执行,这就导致遇到处理慢的代码块会阻塞软件的运行,甚至使程序停止响应,通常解决方案是使用异步来处理,把需要时间处理的代码块异步的进行处理,后面的块则继续执行,等到该代码块处理完以后再回过头来进行结果的处理,但是在ES6前并没有很好的异步解决方案,所以大部分是使用setTimeout来进行处理。下面就通过setTimeout的执行原理来理解一下JavaScript的异步处理的核心机制--事件循环机制。
首先看一下JavaScript的运行时模型:
-
JavaScript引擎部分(例如V8引擎) ,黑框中部分
-
WebAPIs部分,由宿主环境提供的额外API不属于引擎的原生部分
-
EventLoop & CallbackQueue 事件循环和回调队列,同样属于宿主环境提供的机制,用于辅助引擎工作
下面基于这个模型,通过定时器来理解一下,JavaScript的事件循环机制以及异步是如何调用的。
首先写一个基本的定时器
[图片上传失败...(image-5c33a6-1553756162784)]
1.代码运行,此时进行代码的解析
2.调用console.log('HI')
进入到调用栈中
3.控制台打印Hi
4.解析下一部分代码
5.执行定时器,加入到调用栈中
6.在WebAPIs中创建一个Timer,并将定时器的内容移过去
7.定时器部分执行完毕,弹出调用栈,此时定时器内的内容被保存在WebAPIs环境当中
8.调用console.log('Bye')
进入到调用栈中
9.控制台打印Bye
10.console.log('Bye')
弹出调用栈
11.等待WebAPIs中的timer执行,将cb1
加入到回调队列中
12.通过事件循环将回调队列中的cb1
重新压入到调用栈中
13.cb1
内调用了console.log('cb1')
所以也要压入到调用栈中
14.控制台打印cb1
15.弹出console.log('cb1')
16.弹出cb1
通过对setTimeout
的流程解析,很容易发现JavaScript在运行时的调用过程是首先由JS引擎将代码解析编译,然后根据调用顺序加入到调用栈中(栈中的每一项都叫做帧)逐帧执行,其中需要用到WebAPIs、事件循环、回调队列的辅助,最后将执行的结果返回给调用处,至此JavaScript就完成了一次调用的循环。
基础异步实现
上面的例子已经使用setTimeout
实现了一个基础的异步调用但是需要注意的是,虽然例子中使用的setTimeout(myCallback, 5000);
但这并不意味着回调函数会在5秒后立即被执行,而是表示回调方法在5秒后把回调函数添加到回调队列中,如果此时队列中存在待处理任务,那么该回调函数也会相应的被延迟执行。
所以即使是像下面这个例子一样也依然会是一个异步的调用结果,因为setTimeout
的第二个参数仅仅是延迟多久将回调内容放置到回调队列
中,而不是确保延迟多久后一定执行。
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
/**输出结果**/
//Hi
//Bye
//callback