Javascript 运行机制
JavaScript 语言特点是单线程,只有一个调用栈,一次只能执行一件事。在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。因此JS是一个非阻塞
、异步
、并发式
的编程语言。
异步操作有哪些?
- setTimeout、setInterval定时器
- 事件绑定
- promise
- async await
- 读写文件
当遇到异步任务时,会将异步任务放到任务队列中,等到整个运行栈中的内容执行完后再去执行任务队列中的内容。
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
主线程和任务队列的示意图如下:
任务队列
微任务包括 process.nextTick
,promise
,MutationObserver
。 宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
任务队列
中存放的异步任务会被分为了两大类:微任务
与宏任务
。微任务
(microtask) 和 宏任务
(macrotask)。在 ES6 规范中,microtask 称为 jobs
,macrotask 称为 task
。
- 微任务:
process.nextTick
,promise
,MutationObserver
。 - 宏任务:
script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
- JS的执行顺序,先同步后异步
- 异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列
- 调用Promise 中的resolve,reject属于微任务队列,setTimeout属于宏任务队列 注意以上都是
队列
,先进先出
。
示例如下:无论怎么调整顺序,输出结果都是:1 4 3 2
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
})
console.log(4);
微任务
在 宏任务
之前执行,即使 微任务
在 宏任务
之后才被加入到任务队列中。
执行顺序为: 微任务
> DOM渲染
> 宏任务
Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop
(事件循环)。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行
知乎解释: 为了插队, 一个
Event Loop
,Microtask
是在Macrotask
之后调用,Microtask
会在下一个Event Loop
之前执行调用完,并且其中会将Microtask
执行当中新注册的Microtask
一并调用执行完,然后才开始下一次Event loop
,所以如果有新的Macrotask
就需要一直等待,等到上一个Event loop
当中Microtask
被清空为止。 由此可见, 我们可以在下一次Event loop
之前进行插队。如果不区分Microtask
和Macrotask
,那就无法在下一次Event loop
之前进行插队,其中新注册的任务得等到下一个Macrotask
完成之后才能进行,这中间可能你需要的状态就无法在下一个Macrotask
中得到同步。