js 事件循环
为什么JavaScript是单线程的?
单线程意思就是说同一个时间只能做一件事。那这样的话效率不是很低?也没有啦,其实JavaScript的单线程特点是跟他的用途有关的。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。假如不是单线程话,在一个线程当我们在给某个DOM节点增加内容的时候,另一个线程正在删除这个DOM节点的内容,那还得了,那不是乱套了吗。所以JavaScript只能是单线程
虽然JavaScript是单线程,但是JavaScript中有同步和异步的概念,解决了js阻塞的问题。
同步和异步
一.同步
如果在一个函数返回的时候,调用者就能得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
用代码解释一下:
1 | console.log('hello word') |
如果在函数返回时,就看到了预期效果:在控制台打印了 hello word
二.异步
如果在函数返回的时候,调用者还不能得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
代码解释:
1 | fs.readFile('test.txt', 'utf8', function(err, data) { |
在上面的代码中。我们希望通过fs.readFile函数读取文件test.txt的内容,并打印出来。但是在fs.readFile函数返回时,我们期望的结果并不会发生,而是要等到文件全部读取完成之后。如果文件很大的话可能要很长时间。
小总结
- 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
- 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作
JavaScrip就可以进行同步任务和异步任务。把读文件这种操作,ajax请求这些需要耗时的任务放到任务队列中,我还是能够一步步的继续下面的任务。所以啊,JavaScript还是可以很6.那么异步任务里面只是要放进行异步操作的任务吗?里面会发生啥呢?
任务队列
上面说过了JavaScript里面的任务有两种,同步任务和异步任务。
同步任务是指:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。
异步任务指的是,不进入主线程、而进入‘任务队列’的任务,只有‘任务队列’通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
先来看个小例子:
1 | console.log('a') |
js中代码从上往下执行,执行第一行代码的时候控制台输出a,执行第二行代码的适合遇到了setTimeout 函数,因为setTimeout函数是一个异步函数,所以,浏览器会记住这个事件,添加事件表中,之后把这个事件的回调函数入栈到任务队列中。而此时主线程程序继续往下执行,到了第五行:console.log('c')
,执行这条,控制台输出c.这时主线程空了,他会到任务队列里面去查找是否有可以执行的任务,有的话直接拿出来执行,没有的话会一直去询问,等到有可以执行的。
为了更好的说明任务队列和事件循环,先看一张图。
这张图片里面已经画出了js的事件循环的流程了。流程:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 当主线程的执行栈为空时,检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行3;
- 取出任务队列的首部,压入执行栈;
- 执行任务;
- 检查执行栈,如果执行栈为空,则跳回第2步;如不为空,则继续检查
Event Loop:
事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个。另外我们常见的点击和键盘等事件也属于宏任务。
事件循环其实就是入栈出栈的循环。上面的例子中说道了setTimeout,那么setInterval呢,Promise呢,等等等等,有很多的异步函数,但是这些异步任务有宏任务(macro-task)和 微任务(micro-task):
宏任务特征: 有明确的异步任务需要执行和回调;需要其他异步线程支持。
微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持
宏任务(macro-task): setTimeout、 setInterval、setImmediate, I/O, UI rendering….
微任务(micro-task): process.nextTick、 promises、 Object.observe, MutationObserver…
每一次Event Loop 触发时:
- 执行完主线程中的任务也就是执行第一个macro-task任务,例如setTimeout任务。
- 取出micro-task(微任务)中任务执行直到清空。
- 取出macro-task(宏任务)中一个任务执行。
- 取出micro-task(微任务)中任务执行直到清空。
- 重复3和4
注意:
- 在浏览器和node中的执行不一样。
- 任务队列里面是”先入先出”的.
定时器误差
事件循环中,总是先执行同步代码后,才会去任务队列中取出异步回调来执行。当执行 setTimeout 时,浏览器启动新的线程去计时,计时结束后触发定时器事件将回调存入宏任务队列,等待 JS 主线程来取出执行。如果这时主线程还在执行同步任务的过程中,那么此时的宏任务就只有先挂起,这就造成了计时器不准确的问题。同步代码耗时越长,计时器的误差就越大。不仅同步代码,由于微任务会优先执行,所以微任务也会影响计时,假设同步代码中有一个死循环或者微任务中递归不断在启动其他微任务,那么宏任务里面的代码可能永远得不到执行。所以主线程代码的执行效率提升是一件很重要的事情。
我们再来个小例子测试一下你是不是已经完全理解拉:
1 | console.log('global') |
控制台输出:
总结
现在,相信你已经认识了 JavaScript 的真实面目了吧。 JavaScript 是一门单线程的语言,但是其事件循环的特性使得我们可以异步的执行程序。这些异步的程序也就是一个又一个独立的任务,这些任务包括了 setTimeout、setInterval、ajax、eventListener 等等。关于事件循环,我们需要记住以下几点:
- 事件队列严格按照时间先后顺序将任务压入执行栈执行;
- 当执行栈为空时,浏览器会一直不停的检查事件队列,如果不为空,则取出第一个任务;
- 在每一个任务结束之后,浏览器会对页面进行渲染;