JS事件循环

2,680 阅读4分钟

欢迎大家关注我的 github

一、为什么JS是单线程

是由JS的用途决定的,其用途主要是与用户互动,以及操作DOM,如果不是单线程的话会带来很复杂的同步问题。比如:如果是多线程的话,一个用户新建一个DOM,另一个用户删除同一个DOM,该如何处理将变得非常麻烦。

在html5中提出了web worker(todo 改天添加web worker的讲解)标准,JS可以创建多个线程或者是iframe,但是其他子线程主要是用来进行JS计算不能操作DOM,且受到子线程的控制。所以也并没有改变JS单线程的本质

二、同步和异步

由于是单线程,所有任务需要排队,但是如果队列中所有的任务都是同步的话会造成资源的浪费。

于是任务分为两类:同步任务和异步任务。

异步任务的过程:

主线程(函数调用栈)发起一个异步请求,相应的工作线程接收请求,并告知主线程已经收到,主线程继续执行后面的同步代码,同时工作线程执行异步任务,工作线程完成工作后,通知主线程,主线程收到通知后,执行回调函数

三、宏任务和微任务

宏任务(macro-task)主要是:script代码段、setTimeout、setInterval、Promise的构造函数是同步的、setImmediate、I/O、UIrendering

微任务(micro-task)主要是:Promise的回调、process.nextTick

四、任务队列和事件循环

了解任务队列之前,先了解一下任务源,我们将发起异步任务的称之为任务源(setTimeout、Promise等),进入任务队列的是他们指定的任务。

在一个线程中,事件循环是唯一的,任务队列是多个的。来自不同任务源的队列进入到不同的任务队列,setTimeout和setInterval是同源的

事件循环的步骤:

1、运行主线程(函数调用栈)中的同步任务
2、主线程(函数调用栈)执行到任务源时,通知相应的webAPIs进行相应的执行异步任务,将任务源指定的异步任务放入任务队列中
3、主线程(函数调用栈)中的任务执行完毕后,然后执行所有的微任务,再执行宏任务,找到一个任务队列执行完毕,再执行所有的微任务
4、不断执行第三步

事件循环:指主线程重复从任务队列中取消息,执行的过程

先来一个简单的例子:

setTimeout(() => {
    console.log('begin')
})

new Promise((resolve) => {
    console.log('promise begin')
    for(let i = 0; i < 1000; i++) {
        i == 999 && resolve()
    }
}).then(() => {
    console.log('then begin')
})

console.log('end')

因为promise的构造函数是同步的,promise.then是异步的微任务,所以promise beigin先于end

根据上面对宏任务和微任务的分析,其输出的情况为【promise begin——end——then begin——begin】

再来一个复杂点的,我们来一步一步的分析一个例子来看:

console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})

new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

一、第一步、首先执行宏任务script。全局入栈。输出glob1

二、遇到setTimeout,作为任务源,将指定的任务加入宏任务队列

三、遇到setImmediate,作为任务源,将指定的任务加入宏任务队列

四、遇到process.nextTick,作为任务源,将指定的任务加入微任务队列

五、遇到Promise的构造函数,进入执行栈,输出glob1_promise,Promise.then()作为任务源,将指定的任务加入微任务

六、遇到setTimeout,作为任务源,将指定的任务加入宏任务队列

七、遇到process.nextTick,作为任务源,将指定的任务加入微任务队列

八、遇到Promise的构造函数,进入执行栈,输出glob2_promise,Promise.then()作为任务源,将指定的任务加入微任务

九、遇到setImmediate,作为任务源,将指定的任务加入宏任务队列

十、执行所有微任务队列,输出glob1_nextTick和glob2_nextTick、glob1_then、glob2_then

十一、循环执行宏任务队列,执行setTimeout队列、由于遇到promise、process.nextTick等任务源,将新的微任务添加到队列中,然后执行完setTimeout队列后,再将刚添加进去的微任务全部执行,然后再执行setImmediate,依次循环下去

事件机制其实就是异步任务的通知机制



参考资料:

segmentfault.com/a/119000000…

www.ruanyifeng.com/blog/2014/1…

zhuanlan.zhihu.com/p/33127885

segmentfault.com/a/119000001…

www.cnblogs.com/nullcc/p/58…