JS 运行机制
JS 的执行是单线程的,所谓的单线程就是事件任务要排队执行,前一个任务结束,才会执行后一个任务,这就是同步任务,为了避免前一个任务执行了很长时间还没结束,那下一个任务就不能执行的情况,引入了异步任务(js 的异步事件就是依赖于事件循环机制)的概念。
JS 运行机制步骤
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,会把其回调函数作为一个任务添加到任务队列中。
- 一旦执行栈中的所有同步任务执行完毕,就会读取任务队列,看看里面有那些任务,将其添加到执行栈,开始执行。
- 主线程不断重复上面的第三步。也就是常说的事件循环(Event Loop)。
- 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
- 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。
两个任务队列
宏任务(Macrotask)
- setTimeout
- setInterval
- setImmediate(nodejs,在当前"任务队列"的尾部添加事件)
- I/O
- 用户交互操作,UI渲染
微任务(Microtask)
- Promise(重点)
- process.nextTick(nodejs,当前"执行栈"的尾部,下一次EventLoop(主线程读取"任务队列")之前)
- Object.observe(不推荐使用)
process.nextTick和setImmediate的一个重要区别:
- 多个process.nextTick语句总是在当前"执行栈"一次执行完,
- 多个setImmediate可能则需要多次loop才能执行完。
由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。
事件循环
- 检查宏任务队列是否为空,若不为空,则进行下一步,若为空,则跳到3
- 从宏任务队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完后进入下一步
- 检查微任务队列是否为空,若不为空,则进入下一步,否则,跳到1(开始新的事件循环)
- 从微任务队列中取队首(在队列时间最长)的任务进去事件队列执行,执行完后,跳到3 其中,在执行代码过程中新增的microtask任务会在当前事件循环周期内执行,而新增的宏任务任务只能等到下一个事件循环才能执行了。
简而言之,一次事件循环只执行处于宏任务队首的任务,执行完成后,立即执行微任务队列中的所有任务。
第一次执行异步任务的时候会先清空微任务队列,然后才是本次事件循环中的宏任务,然后是下次事件循环的微任务被清空,再是宏任务,如此循环往复。