JS事件循环机制是目前我们在面试中会频繁被问到的一个问题。很多人可能大概知道JS事件循环机制是个什么东西,但是对具体又比较模糊,无法在面试官前清晰地表述出来。
所以今天我就来带大家深入理解一下JS事件循环机制到底是个什么东西。
一、JS异步实现
JS是通过事件循环机制实现单线程异步的。
首先,我们都知道JS是一门单线程的语言。在设计初期,由于JS是运行在浏览器端的脚本语言,目的就是为了实现与页面的动态交互,其核心就是DOM操作,这就决定了他必须使用单线程去处理脚本,从而避免对同一DOM元素同时进行操作时产生冲突问题。
若是遇到耗时操作,页面便会产生堵塞。例如请求接口返回数据慢,图片加载完成等等。这样显然是不合理也不实用的,因此异步模式应运而生。
异步任务:
异步任务指的是,不进入主线程、而进入"任务队列"(task queue) 的任务,只有等主线程执行完毕,"任务队列"开始通知主线程,请求知心任务,该任务才会进入主线程执行。
异步模式的方式:
回调函数callback; 事件驱动Event-Driven; 观察者模式Observer pattern(又称发布订阅模式publish-subscript pattern); Promise,async/await; 宏任务(定时器,ajax,DOM事件监听); setImmediate(立即执行,Node.js执行环境); mutationobserver(H5,监控DOM元素变化)。
特点:
不会等待这个任务的结束才开始任务;
对于耗时操作开启后就立即往后执行下一个任务;
耗时任务的后续逻辑一般通过回调函数的方式定义,耗时任务完成后就会自动执行传入的回调。
二、执行栈与任务队列
执行栈:
当执行某个函数、用户点机一次鼠标、Ajax请求完成、一个图片加载完毕等事件发生时,只要指定回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,并遵循先进先出原则。
主线程:
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程会不停的从执行栈中读取事件,并执行完所有栈中的同步代码。
当遇见一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
当主线程执行栈中所有的代码执行完之后,主线程将会查看任务队列是否有任务。 如果有,那么主线程会一次执行那些任务队列中的回调函数。
什么是异步任务?
异步任务分为宏任务(macrotask) 与微任务 (microtask), 不同的API注册的任务会依次进入自动队列中,然后等待Event Loop将它们依次压入执行栈中执行。
什么是宏任务?
script(整体代码) setTimeout setInterval UI渲染 I/O postMessage MessageChannel setImmediate(Node.js环境)
微任务(microtask):
Promise MutationObserver process.nextTick(Node.js环境)
宏任务和微任务的区别:
微队列是唯一的,在整个事件循环中,仅存在一个,并且同一论事件循环中的微任务会按循序依次执行。
而宏任务存在的优先级(用户I/O部分优先级更高)。且同一轮事件循环中,只执行一个。
三、Event Loop(事件循环)
当JS代码执行时,所有任务(同步/异步)都在主线程上执行,形成一个执行栈;
执行栈之外有用于存储待执行异步回调的任务队列(task queue)===>宏队列与微队列;
浏览器汇中在其它分线程执行相关管理:定时器管理模块,Ajax请求管理模块,DOM事件管理模块。若碰到这些任务源,就会将其回调函数加入到宏队列中;
若碰到微任务源,例如Promise,则会将其回调函数加入到微队列中;
直至script宏任务执行结束后,就会执行微队列中的任务;
当微队列中的所有微任务执行结束,就会检查宏队列中有没有可执行的宏任务。如果有,则执行该宏任务,之后检查微队列并执行微任务,依次循环;反之,要是宏队列中没有待执行任务,则循环结束。这就是我们常说的事件循环机制。
Event Loop(事件循环)中,每一次循环称为tick, 每一次tick的任务如下:
执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行; 检查是否存在微任务,如果存在则不停的执行,直至清空微任务队列; 更新render(每一次事件循环,浏览器都可能会去更新渲染); 重复以上步骤。