Javascript中的事件环

226 阅读4分钟

JS为什么是单线程的

众所周知,JavaScript语言的一大特点就是单线程,也就是说js在运行时一次只做一件事。

为什么JavaScript不是多线程呢?我们都知道js的主要用途是与用户互动,以及操作DOM,如果两个线程同时操作一个DOM,一个增一个删同时进行,那浏览器应该听谁的呢,为了避免这些复杂性,从诞生之时js就是单线程的。

同样的问题,如果一个http请求数据过大或超时,就必须等他执行完再执行下一个任务了吗?这样页面岂不是卡死?导致执行效率低下。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的,并不在主线程上,但他们的执行顺序受主线程控制。

堆、栈和队列

这里先解释下堆(Heap)、栈(Stack)、队列(Queue)的概念也许会帮助大家更好的理解

1、 堆(Heap):存放对象。对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。

2、 栈(Stack):执行上下文的栈,先进后出, 存放一些var变量、function、dom操作、setTimeout等

function a(){
    let a = 1;
    b();
    function b(){
        let b = 2;
        c();
        function c(){
            console.log(b);
        }
    }
}
a();
//函数a()、b()、c()依次放入栈中,但执行顺序是c()先执行

3、 队列(Queue):先进先出。js运行时异步代码没有进入主线程,进入到任务队列中,比如:ajax、setTimeout等。当执行栈为空时,会从队列中取出一个消息进行处理。当栈再次为空,会继续从队列中取,直至队列为空,消息处理完毕。这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

同步与异步

什么是同步:线性执行。在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,比如:

console.log(1);
console.log(2);

什么是异步:不进入主线程而进入"任务队列"的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。常见有以下几种:

  • setTimeout、setInterval、setImmediate

  • Promise.then

  • process.nextTick 等

同步和异步任务分别进入不同的执行"场所":

1)所有同步任务都在主线程上执行,形成一个执行栈。

2)主线程之外,还存在一个"任务队列"。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

3)一旦"执行栈"中的所有同步任务执行完毕,就会读取"任务队列"可执行任务并执行。

4)主线程不断重复上面的第三步。

详细可见下图:

下边举个小栗子,看下执行结果,理解下代码的执行顺序:

console.log(1);
setTimeout(()=>{
    console.log('setTimeout');
})
let promise = new Promise((resolve,reject)=>{
    console.log('promise');
    resolve(100);
}).then((data)=>{
    console.log(data);
})
console.log(4);
//执行结果:1  promise  4   100   setTimeout

Promise.then和setTimeout不都是异步?不应该是谁先进入队列,谁先执行吗?为什么setTimeout最后输出?我们继续往下看 ⬇

微任务和宏任务

微任务和宏任务对任务进行更精细的划分:

  • 微任务(micro-task):整段script、Promise.then、process.nextTick、MutationObserve、MessageChannel

  • 宏任务(macro-task):setTimeout、setInterval、setImmediate

执行机制:

1)先执行主线程的同步代码

2)主线程代码执行完毕后先到“微任务”队列读取可执行代码并执行

3)上述执行完后,到“宏任务”队列中读取可执行代码并执行

4)依次重复2)3)。

了解完执行机制后,对上个例子做详细分析:

console.log(1); //同步代码
setTimeout(()=>{
    console.log('setTimeout');
})
let promise = new Promise((resolve,reject)=>{
    console.log('promise');
    resolve(100);
}).then((data)=>{
    console.log(data);
})
console.log(4); //同步代码
//先执行完同步代码,遇到Promise也是立即执行
//setTimeout放入对应的宏任务队列
//Promise.then放入对应的微任务队列
//同步代码执行完,会先执行微任务Promise.then,输出100,再执行宏任务setTimeout,输出setTimeout
//执行完毕