一、JavaScript是单线程语言
JavaScript作为主要运行在浏览器的脚本语言,主要用途之一是操作DOM。
如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
二、JavaScript将任务的执行模式 - 同步模式、异步模式
因为JavaScript是单线程,如果遇到耗时较大的程序,会被阻塞出现假死的情况。因此JavaScript引擎在执行JavaScript代码时,会将任务分为两类:同步任务和异步任务。同步任务在主线程上执行,而异步任务则由任务队列中的事件循环机制异步执行。在异步任务完成后,就会将该任务对应的回调函数放入任务队列中,并等待主线程执行完当前所有的同步任务后再执行该回调函数。
三、调用栈Call stack
在JavaScript运行的时候,主线程会形成一个栈,这个栈主要是解释器用来最终函数执行流的一种机制。通常这个栈被称为调用栈Call Stack,或者执行栈(Execution Context Stack)。
调用栈内存放的是代码执行期间的所有执行上下文。
四、Event Loop 事件循环
事件循环是指不断从任务队列中取出任务,并执行其对应的回调函数的过程。事件循环在JavaScript引擎内部以非常高效的方式运行,在等待异步I/O操作返回数据时,可以将CPU时间释放给其他线程使用。
事件循环的基本流程
- 主线程执行同步任务,当遇到异步任务时,将其回调函数添加到任务队列中,然后继续执行同步任务。
- 当所有同步任务执行完成后,主线程会立即去任务队列中查找是否有已经完成的异步任务的回调函数需要执行,如果有,则会按照回调函数添加的先后顺序执行它们。
- 执行完所有已经完成的异步任务回调函数后,重复步骤2,直到任务队列中没有任何任务。
使用图文解释事件循环的流程
-
加载整体代码,在调用栈中压入一个匿名的全局调用,然后依次执行每行代码
-
第一行代码console.log('begin')是同步代码,压入调用栈,然后执行,在控制台打印begin, 执行完之后弹出调用栈。
-
setTimeout(timer1)先压入调用栈,但是函数内部是一个异步调用,所以内部API(web API)给timer1开启一个1.5s的倒计时器。然后针对setTimeout的调用已经完成,所以从调用栈弹出,继续执行下面的代码。 -
setTimeout(timer2)同理,压入调用栈,内部API给timer2开启一个1s的倒计时器,setTimeout完成调用,继续执行后面的代码。
-
console.log()调用,压栈、执行(打印)、弹栈。执行完当前的console.log调用后,匿名调用就已经完成了,调用栈就会被清空掉。 -
此时
Event Loop开始工作,从任务队列中取出第一个已经完成的异步任务的回调函数压入调用栈,继续执行。 但是此时任务队列中没有已经完成的异步任务的回调函数,所以需要等待一会,执行相当于暂停下来了。 -
等待倒计时器任务结束,由于
timer2是1s倒计时,timer1是1.5s倒计时,所以timer2对应的倒计时先结束,结束之后timer2就会被放入任务队列的第一位,然后在timer1对应的倒计时结束后就会放入任务队列的第2位。 -
一旦任务队列中发生了变化,时间循环就会监听到,就会把任务队列的第一个取出即
timer2,压入调用栈中,继续执行timer2,相当于开启了新一轮的执行,执行顺序与刚才保持一致。
-
执行
console.log('timer2 invoke'),控制台打印timer2 invoke -
执行setTimeout(inner),开启inner的1s倒计时器,timer2调用结束,清空调用栈。
-
Event Loop继续取出任务队列第一个已完成的异步任务的回调函数,即timer1,压入调用栈执行,执行setTimeout('timer1 invoke'),timer1调用结束,清空调用栈。
-
inner对应的倒计时器结束后,放入任务队列,Event Loop取出inner压入调用栈,执行console.log(),inner执行结束,弹出调用栈。
五、宏任务和微任务
在上面提到的任务队列中,其实还分为宏任务队列(Task Queue)和微任务队列(Microtask Queue),对应的里面存放的就是宏任务和微任务。
首先,宏任务和微任务都是异步任务。
在同步任务中,任务的执行都是按照代码顺序执行的,而异步任务的执行也是需要按顺序的,队列的属性就是先进先出(FIFO,First in First Out) ,因此异步任务会按照进入队列的顺序依次执行。
| 常见的宏任务 | 常见的微任务 |
|---|---|
| script(整体代码) | Promise.then() |
| setTimout | Object.observe |
| setInterval | process.nextTick(node 独有) |
| setImmediate(node 独有) | MutationObserver |
| requestAnimationFrame(浏览器独有) | |
| IO | |
| UI render(浏览器独有) |
事件循环流程(加入宏任务和微任务区分)
如果考虑到宏任务和微任务,那么事件循环的具体流程如下:
- 主线程执行同步任务,当遇到异步任务时,将其回调函数添加到任务队列中,然后继续执行同步任务;
- 当所有同步任务执行完成后,主线程会从宏任务队列中,按照进入任务队列的顺序,找到第一个执行的宏任务,放入调用栈,开始执行;
- 执行完该宏任务下所有同步任务后,即调用栈清空后,然后主线程从微任务队列中,按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;
- 当微任务队列清空后,一个事件循环结束;
- 重复步骤234,继续从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。