vlambda博客
学习文章列表

深夜冥想——何为Event Loop?

先说说为啥JavaScript是一门单线程语言:
众所周知,JavaScript是一门单线程语言,即同一时间点,只能在做某一件事情。这也是js最大的特点,这与js的用途有关。正如我们所了解的,js是前端的逻辑层,主要用来处理逻辑、与用户互动以及操作DOM。因此决定了js必然是个单线程语言,不然会带来很复杂的同步问题。不妨设想,假如js有2个线程,在同一时间点,其中一个线程在某个DOM节点上添加或者修改内容,而另外一个线程则在此时删除了或者修改了该DOM节点。这时候不仅是孔子懵了,孟子懵了,老子也懵了,浏览器更懵了——这到底要我干啥?到底要以哪个线程的操作为准?故而,注定了JavaScript得是一门单线程语言。
但是为了利用多核CPU的计算能力,HTML5提出了Web Worker(工作线程)标准。允许js脚本创建多个线程,但是子线程完全受控于主线程,且不得操作DOM。因此这个标准并没有改变js单线程的本质,而是进行了改善。
执行栈与任务队列:
上面讲了JavaScript设计成单线程语言的原因,既然是单线程,因此也意味着所有的任务得按照先后顺序排队执行,前面的任务执行完了,才轮到后面的任务执行。如果前面的任务执行的很慢,后面的任务也得一直等到前面的任务执行完了才能执行。
但是在我们实际中好像不全是这样的,比如Ajax请求,setTimeout等并不阻塞主线程,比如当我们向服务端发起Ajax请求的时候,并没有一直等待结果返回,而是继续执行了后面的代码。于是,任务分为同步任务和异步任务。同步任务是指:在主线程上排队顺序执行的任务,只有等前一个任务执行完了才能执行后一个任务;异步任务是指:任务不进入主线程,而是进入任务队列(Task Quene)中,只有等到主线程同步任务执行完毕的时候,该异步任务才会进入到主线程中执行。
所有的同步任务都会在主线程上顺序地执行,形成了一个执行栈。当遇到异步任务时,不会一直等待其结果返回,而是将其挂起,继续执行执行栈中其他的任务。当异步任务返回结果后,js会将其推入到任务队列中,此时并不会立即执行其回调函数。而是当当前执行栈中的任务都执行完毕后,主线程会去读取任务队列中的任务(主线程会检查执行事件,如定时器只有到了规定时间才能进入到主线程中),取出排在第一位的任务,将其回调函数放入执行栈中,执行里面的同步代码。不断重复上述的过程,于是就形成了事件循环(Event Loop),流程图大致如下:


宏任务(Macro Task)微任务(Micro Task)
异步任务中,执行的优先级也有区别,分为:宏任务与微任务。当每个宏任务执行完毕后,都会先执行完当前任务队列里面所有的微任务,才会执行下一个宏任务。
宏任务主要包括: script (整体脚本代码) 、setTimeout、setInterval、I/O、UI交互事件、setImmediate (Node.js中)。
微任务主要包括: Promise、MutaionObserver、process.nextTick(Node.js中)。
举个栗子,直接上代码:

分析:
    1.先执行同步任务,会依次打印:"script start" -> "promise 1" -> "promise 2" -> "end script"(注意promise的executor函数是同步的,以后讲promise原理的时候会讲到)。
    2.执行这次宏任务(script整体可当中一个宏任务)产生的所有微任务,于是会依次打印:"promise 1-1" -> "promise 2-1"-> "promise 1-2" -> "promise 2-2" -> "promise 1-3"。(当执行完"promise 1-1" 的时候,就会把下一个then的任务放入到任务队列里面,因此"promise 1-2"会在"promise 2-1"之后执行,后面同理)
    3.当所有微任务执行完毕后,再执行下个宏任务,于是会依次打印:"timeout" -> "timeout 2"。
    4.执行结果如下图:


结语

基本就讲述到这里了,Event Loop总算给整明白了,你学废了吗?