js线程和事件循环原理

先看一个案例:

console.log(1)
setTimeout(() => {
    console.log(2)
    new Promise((resolve) => {
        console.log(3)
        resolve(4)
    }).then(res => {
        console.log(res)
    })
},0)
setTimeout(() => {
    console.log(5)
    new Promise((resolve) => {
        console.log(6)
        resolve(7)
    }).then(res => {
        console.log(res)
    })
},0)
new Promise((resolve) => {
    console.log(8)
    resolve(9)
}).then(res => {
    console.log(res)
})
console.log(10)

执行这段代码依次打印:1 8 10 9 2 3 4 5 6 7

大部分人在不了解js线程和事件循环的时候应该和我有一样的疑问:

1、为什么7会是在最后打印?

2、为什么8立刻就打印了?

3、4为啥紧随3之后打印出来,而9却不是紧跟着8之后输出?

没关系,接下来我以我自己的理解和语言来输出一下,以输出代学习,希望对你有所帮助。

一、js线程

js设计之初就是一个单线程的语言,也就是单行道,只能执行完一个任务后再继续执行下一个任务,主线程执行完了后才会执行异步函数。为什么会是单线程呢?这和js语言运行的环境有关,js主要运行于浏览器环境,会涉及到对页面dom的操作,如果说js支持多线程处理,就有可能出现这样的情况:第一条线程在处理dom的事件,然后用户触发了该dom上的另一个事件,浏览器开启第二条线程去处理当前事件,如果前后两个事件内容有冲突的地方,就会特别混乱和不可理解。所以就直接暴力地把js设计成单线程语言,避免这样的事情发生。

但是下一个问题就来了。如果遇到异步会函数和异步回调怎么办,比如上面例子里提到setTimeout,类似的还有setInterval等,那是不是意味着js运行到这里就只能停在这里,等它执行完了才继续往下执行呢?这样的话体验也太不好了,我们也都知道 实际情况并非这样的。

这就要提到定时器线程了。js执行的线程只有一个,那就是主线程,但是一个线程难免忙不过来,需要其它线程来帮忙,辅助一下。其它相关线程主要有:GUI线程,负责解析渲染html,呈现页面样式;事件循环线程,也就是eventLoop线程;定时器线程,setTimeout、setInterval、setImmediate等;浏览器事件线程;http异步请求线程等。

现在可以简单的来说一下上面例子里开始几部的执行流程了:

1、console.log(1)毫无疑问首先执行

2、遇到第一个setTimeout把该函数挪到定时器线程去执行,回调函数标记为task1,主线程继续往下执行,此时定时器线程对于定时器的解析和主线程的解析是同时进行的,相当于两条并行的线路。

3、遇到第二个setTimeout,同上,回调函数标记为task2

4、主线程继续,遇到new Promise,promise本身属于同步函数,new的时候立即执行内部的代码,所以打印8,而它的回调函数resolve属于异步函数,和setTimeout一样提取出来,放到另一条专门处理该异步方法的线程中解析。

5、主线程继续,打印10

此时主线程以及跑完了,依次打印了1、8、10

在继续解释异步线程的解析流程之前得先说一下两个概念:宏任务和微任务

宏任务和微任务都属于异步任务,属于一个异步处理队列,区别在于他们的回调函数在主线程中的处理顺序。

宏任务:setTimeout、setInterval、setImmediate

微任务:promise、promise.nextTick、MutationObserver

下面接着上面主线程步骤2讲

主线程2执行完成后,把setTimeout扔给异步线程,异步线程立刻开始执行,并在指定时间延迟后把回调函数task1和task2按先进先出的顺序放入一个池子当中,并且打上宏任务的logo。这个池子是专门拿来存放异步回调函数的,叫做“事件队列”。因为回调函数还是需要主线程来执行的,异步线程只是解析异步函数的外壳,这里就是setTimeout这个壳。

主线程4执行完成后,把promise的回调函数resolve扔给异步线程执行,类似于对setTimeout的处理,处理完成后会把回调函数console.log(4)放进“事件队列”的池子,打上微任务的logo。

上面给每个回调函数都打上了相应的任务类型logo便于后续使用。

二、事件循环(eventLoop)

还记得上面主线程执行完毕后打印了1、8、10,但是异步函数的回调还没执行,而上一步异步函数在异步线程执行的时候已经把回调函数扔到事件队列里了,就等着被主线程翻牌了。

那么浏览器怎么知道主线程已经执行完毕了和事件队列里还有函数没执行的呢?这就是eventLoop的作用了。当主线程执行完毕后,事件循环线程会循环去找事件队列里是否有待执行的函数,如果有则拿到主线程上去执行。

还记得异步线程执行完成后给回调函数打上的宏任务和微任务的logo么?就是在这里使用的。主线程调用回调事件的时候会优先调用微任务的回调函数,也就是具有微任务logo回调函数。

所以这个列子会先执行promise的回调,即先打印9

随后后再执行第一个宏任务的回调task1。

这里还有一个知识点:宏任务或者微任务回调函数中如果嵌套了其它宏任务或微任务,会先把当前宏任务内包含的宏任务或微任务执行完毕后,才会执行下一个任务回调

接着上面的task1,立即打印2,内部含有promise,执行,输出3,当前回调函数内只有一个异步回调:resolve,按刚才的规则,会立即执行这个异步回调,打印4,。如果task1里面还有其它异步函数,当然也应该按照正常的规则先执行微任务回调,然后是宏任务回调。

接着执行task2回调,打印5,promise中打印6,同样,内部只有一个异步回调,立即执行打印7

手绘了一个原理图:

综上,打印结果为1、8、10、9、2、3、4、5、6、7

三、案例

虽然例子简单,但只要把这个原理搞明白了,再复杂的情况都同理可得,比如:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

一步一步来,第一轮解析:

1、console.log(1)

2、遇到setTimeout,先把回调函数扔给异步线程处理,打上宏任务logo放进事件队列池子里备用,标识为task1

3、遇到process.nextTick,同样异步线程执行,把回调打上微任务logo放入队列池,标识为mic1

4、遇到new Promise,直接执行同步代码部分:console.log(7),异步执行回调函数resolve,把回调打上微任务logo放入队列池,标识为mic2

5、遇到setTimeout,同第一步,回调函数标识为task2

第一轮下来打印了1、7

主线程执行完毕,事件循环从队列池中拿取函数执行,首先会拿取微任务的回调。

第二轮

1、按顺序来首先执行mic1,即console.log(6)

2、执行mic2,console.log(8)

微任务执行完毕,执行宏任务

4、task1,执行逻辑相同,首先console.log(2),遇到process.nextTick,回调函数推入队列池,标记为task1_mic1

5、遇到new Promise,直接执行同步代码部分:console.log(4),回调函数推入队列池,标识为task1_mic2

6、task1中属于主线程的内容执行完毕了,再在task1的范围内寻找异步回调,先进先出,首先执行task1_mic1,输出3,再执行task1_mic2,输出5

7、task1全部执行完毕,执行task2,首先console.log(9)

8、遇到process.nextTick,回调函数推入队列池,标记为task2_mic1

9、遇到new Promise,直接执行同步代码部分:console.log(11),回调函数推入队列池,标识为task2_mic2

10、开始执行task2中的异步回调,同样按顺序来,首先执行task2_mic1,输出10,接着是为task2_mic2,输出12

所以第二轮下来输出结果为:1、7、6、8、2、4、3、5、9、11、10、12

这样捋一遍就清晰多了!

四、js线程解释for循环闭包打印问题

我们都遇到过一道关于闭包的经典题

for(var i=0,i<5,i++){
    setTimeout(() => {
        console.log(i)
    },i*1000)
}

打印的结果是5 5 5 5

这样的结果用线程来解释就很明显了。

var声明的变量是全局变量,后续使用的是同一个i值,setTimeout是异步函数,一开始主线程会执行for循环,按顺序将setTimeout推入异步线程执行,执行完成后按顺序将回调函数放入事件队列里,等待for循环执行完毕,主线程空出来,而for循环结束的条件是i===5,此时再来执行回调函数,打印出来的当然就都是5了。

如果把var换成let就可以解决这个问题了,因为let声明的时候,每次迭代i都是一个新的值,是用上一次迭代结束的值来初始化当前值。

发布了59 篇原创文章 · 获赞 29 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/dongguan_123/article/details/103550101