浏览器(或者说JS引擎)执行JS的机制是基于事件循环。
由于JS是单线程,所以同一时间能且只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。
为了避免因为某些长时间任务造成的无意义等待,JS引入了异步的概念,用另一个线程来管理异步任务。
同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程。等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如ajax请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环。
先来看一下实现异步的一些方式,下面用分类的方式列举一下
- 经典的回调函数
- callback
- 监听事件
- onload
- new Image
function asynByImg( callback ) {
var img = new Image();
img.onload = img.onerror = img.onreadystatechange = function() {
img = img.onload = img.onerror = img.onreadystatechange = null;
callback();
}
img.src = "data:image/png,";
}
asynByImg(function(){
console.log(1);
});
console.log(2);
- 事件绑定
- 发布/订阅模式
- Message
- 延迟类
- setTimeout
setTimeout(function() {
console.log(1);
});
console.log(2);
- setInterval
setInterval(function() {
console.log(1);
});
console.log(2);
- requestAnimationFrame
(function() {
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
var startTime = window.mozAnimationStartTime || Date.now();
requestAnimationFrame(draw);
function draw (timestamp) {
// 计算两次重绘的时间间隔
var drawStart = timestamp || Date.now();
var diff = drawStart - startTime;
startTime = drawStart;
requestAnimationFrame(draw);
}
})()
- setImmediate
- process.nextTick()
- 异步功能函数
- promise
- ajax
- async / await
- Worker
- Co / Generator
这些方式的执行顺序是什么样的?为什么?
先看下面的示例:
console.log('start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve()
.then(() => {
console.log('promise 5')
})
.then(() => {
console.log('promise 6')
})
.then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve()
.then(() => {
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
})
上面的执行结果依次打印下面的内容
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6
setInterval
。。。
想要知道上面的原理需要先来了解下面的几个内容
- 事件循环
JavaScript在一个时间仅处理一个任务. 就是JavaScript在执行时, 存在一个执行队列, 依次执行队列中的任务, 不能同时执行多个任务。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)关于事件循环详细说明可以查看这里。
这个过程用一个现实的例子就像是去银行只有一个窗口,办理业务就要排队,好痛苦。但是如果一个任务需要执行很久很久,肿么办?等着呗。但是聪明的程序员怎么会忍受一个任务执行那么长时间,于是把任务分成了macrotasks和microtasks两类。 - macrotasks和microtasks
macrotasks【同步任务】: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks【异步任务】: process.nextTick, Promises, Object.observe, MutationObserver
同步任务以及异步任务会进入不同的“场所”,同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程执行完毕为空,就会去Event Queue读取对应的函数,进入主线程执行。
然后解析上面代码的执行:
1. 同步任务直接放入到主模块(主线程)任务队列执行. 异步任务挂起后台执行, 等待IO事件完成或行为事件被触发.
2. 系统后台执行异步任务, 如果某个异步任务事件发生(或者是行为事件被触发), 则将该任务push到任务队列中, 每个任务会对应一个回调函数进行处理. 这个步骤在后台一直执行, 因为就不断有事件被触发, IO不断完成, 任务被不断的加入到任务队列中.
3. 执行任务队列中的任务. 任务的具体执行是在执行栈中完成的. 当运行栈中一个任务的基本运行单元(称之为Frame, 桢)全部执行完毕后, 去读取任务队列中的下一个任务, 继续执行. 是一个循环的过程. 处理一个任务队列中的任务, 称之为一个tick.
即 macrotasks执行console.log('start')
执行后打印了‘start’; 然后macrotasks完成进入microtasks,执行了console.log('promise 1')
和console.log('promise 2')
;microtasks执行完成返回查看macrotasks执行了console.log('setInterval')
和console.log('setTimeout 1')
; microtasks执行console.log('promise 3')
和console.log('promise 4')
; 再次进入macrotasksconsole.log('setInterval')
和console.log('setTimeout 2')
;最后执行microtasks队列console.log('promise 5')
和console.log('promise 6')
,到这里microtasks全部完成,后面执行macrotasks
备注:事件队列是遵循先进先出, 需要依次处理. 所以, 定时器函数运行时,如果遇到了定时的事件, 事件发生, 也仅仅是将该任务push入任务队列而已(并没有立即执行回调函数)。同时这也是为什么setTimeout预定的时间不一定会执行,例如你预定300ms后执行,它最早是300ms的原因