JS是单线程的(所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个,叫主线程),就是说JS同一时间只能处理一件事。那么就可能出现这种情况:一件事需要花费很长时间处理,后面的事情只能等待,体验就非常差。
所以JS中将执行的任务分为两类:同步任务和异步任务。
同步任务,同步任务指的是,发出调用立即获得结果的为同步任务。同步任务会在调用之后一直等待,直到返回结果。
异步任务,异步任务指的是,发出调用,但无法立即获得结果,需要额外的操作才能得到预期的结果的为异步任务。调用之后和拿到结果之间,可以进行其他操作。
JS运行环境运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈;
- 主线程之外,还存在一个任务队列(一个先进先出的队列,里面放着各种事件)。只要异步任务有了运行结果,就在任务队列之中放置一个事件;
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;
- 主线程不断重复上面的第三步;
所以执行栈中的代码(同步任务),总是在读取任务队列(异步任务)之前执行
事件和回调函数
事件,所谓的事件驱动就是将一切抽象为事件。I/O操作完成是一个事件,用户点击是一个事件,Ajax完成是一个事件,一个图片加载完成是一个事件,当产生事件后,这个事件会被放入任务队列中等待被处理。
回调函数,与事件关联的函数,会在执行事件时调用。
setTimeout(fn, 1000); // 例如setTimeout时间到了就会对应一个事件,而fn就是事件执行时调用的回调函数
事件循环(event loop)
主线程只会做一件事情,就是从任务队列里面取事件、执行事件,再取事件、再执行。当任务队列为空时,就会等待直到任务队列变成非空。而且主线程只有在将当前的事件执行完成后,才会去取下一个事件。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
事件循环过程:
- 首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table(事件列表);
- 异步任务在event table中注册函数,当满足触发条件后,被推入event queue(事件队列);
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue(事件队列)中查看是否有可执行的异步任务,如果有就推入主进程中;
setTimeout(function () { console.log(1); }, 0); console.log(2); // 2 // 1 // setTimeout(..) 并不是直接把回调函数挂在任务队列中。它所做的是设定一个定时器。当定时器到时后,相当于产生了一个时间到了的事件,这个事件进入任务队列。这样,下个事件循环主线程会取出并执行这个事件的回调函数 // 因为主线程会先执行完执行栈中的同步任务,才会去任务队列提取事件,所以异步任务的回调函数总是在同步任务之后执行 // 但如果任务队列中已经有多个事件排队,那么就会导致setTimeOut的函数延后运行,这就是为什么setTimeOut和setInterval不准确的原因
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date());
}, 1000);
}
console.log(new Date());
// 先输出一个日期,一秒后再几乎同时输出五个日期,但可以看出五个日期有细微的差别,这些误差就是因为任务队列中有多个事件排队导致后边事件延迟的结果
同步任务与异步任务的另一种分类方式
除了将JS运行任务分为同步和异步,还有一种更准确的分类方式:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate,I/O,UI渲染;
micro-task(微任务):Promise,process.nextTick,MutationObserver(DOM变动API);
setTimeout(function(){ console.log(1); }); new Promise(function(resolve){ console.log(2); resolve(); }).then(function(){ console.log(3); }); console.log(4); // 以上代码,如果仅仅是按简单的同步与异步分类我们认为运行结果应为 // 2 -> 4 -> 1 -> 3 但实际结果是: 2 -> 4 -> 3 -> 1
那么JS事件循环更准确的运行过程:
- 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的事件队列里;
- 当前宏任务执行完成后,会查看微任务的事件队列,并将里面全部的微任务依次执行完;
所以微任务是先于宏任务运行的。