前言
我在刚刚接触JavaScipt的时候,经常看到一些JavaScipt是一门单线程的语言,JavaScipt的特性就是异步的言论,那时的我一脸懵逼🤔️什么是单线程,什么又是异步呢?因此我认为深入了解JavaScipt的运行机制有助于我们更快,更容易去理解JavaScript其他内容。
闲话少说,今天的故事就从单线程讲起。
单线程
JavaScript作为一门浏览器脚本语言,当初研发之时就是为用户操作和浏览器DOM服务的,而多线程存在线程之间资源抢占,死锁,冲突等一系列问题。这就决定了JavaScript一定是单线程的,不然就存在着冲突问题。
因此所有浏览器的JS引擎(浏览器用于读取并执行JavaScript)在执行JavaScript只分配一个线程。虽然单线程不存在冲突的问题,但它同时也带来了一个新的问题--阻塞
很简单的例子,就想是汽车行驶在马路上,单线程只有一条路,如果前面的车抛锚了,那就意味着后面的车就得乖乖等着,等到前面的车换完轮胎,才能继续上路。
但我们都知道JavaScript是非阻塞的,为了解决上述的问题,JavaScript使用了异步和回调的方式来解决这种情况。
任务队列
上面已经提到,JS引擎是单线程的,这意味着所有的任务执行必须得排队,前一个任务结束了,下一个任务才能开始,但是如果遇到了异步的任务,后面的任务就需要等待,而中间等待的时候cpu其实是闲置的,这无疑是一种效率极低的行为,JavaScript对此的处理是这样的,针对异步的任务,会将其搁置,等待其结果返回后,再让其回到主线程。而异步任务临时存储的地方就是任务队列。
蹭一下最近etc的热度,简单来说同步任务就是办了ect的,异步任务就是没办etc的。
js引擎判别出是异步任务时,就会通过调用浏览器给它的接口,由浏览器完成这些功能。所以异步操作不代表不工作,只是该工作转交给浏览器处理 ,当浏览器处理完这部分操作后会通知往Event queue(事件队列))中注入回调函数,等待被主线程调取。
EventLoop
上面当流程图已经简单的说明了什么是EventLoop,接下来我们通过网上比较流传广泛的一张图,来仔细看一下什么是EventLoop
右上角的虚线框中,代表了主线程运行时,产生的执行栈和存储object类型数据堆内存,我们的同步代码就存在执行栈中。而我们的异步代码则被分配给WebAPIs去执行,当其执行结束后,就会按照推入上文所说的任务队列中,并注册回调函数,供主线程调用。
当主线程处理完所有同步任务,这时执行栈就空了,主线程就会访问任务队列的头部,看看任务队列的头部有没有已经注册好的回调函数,如果有就把其放置执行栈,进行执行。执行完后执行栈又空了,就会继续前面的操作,如此循环往复,这就是JS的运行机制——EventLoop
有个小疑问?
这个EventLoop按照我的想法,应该是个无限循环,难道不会影响性能吗?如果记录所有异步任务,调用完后跳出循环,或者设置超时,会不会更好一些?但是网上搜索了很多,都没有得到我想要的答案,希望能有大佬解答一下。
了解到这里就足够了吗,很遗憾并不可以,让我们看一道很经典的题目
console.log(1)
setTimeout(function(){
console.log(2)
},0)
new Promise(function(resolve){
resolve()
}).then(function(){
console.log(3)
})
console.log(4)
按照之前的逻辑,应该是1,4,2,3 但是运行后的输出是1,4,3,2 这时候我们应该明白,任务队列中也有优先级,这是我们就需要引入了解两个新概念--宏任务和微任务。
宏任务和微任务
在刚开始的学习中我将宏任务和微任务,归结于异步任务之下。但是我发现它并不能解释上述例子,因为肯定在任务队列中肯定是先执行宏任务,然后在宏任务结束的间隙内去执行微任务。
经过进一步的了解,我明白整块代码块进入执行栈的行为就是一个宏任务,因此宏任务微任务并不是异步任务的子集,而是除了同步异步之外,更宽泛的对执行栈任务的分类,而之所以会造成一开始的理解偏差,是因为恰好异步任务中含有宏任务和微任务。那么什么才是真正的宏任务和微任务呢?
对此,我并没有找到十分标准的定义,下面是我个人的理解,由宿主环境(对JS而已就是浏览器或者Node环境)提供的是宏任务,而由一些语言标准(ES6,node等)提供的是微任务。(这个解释算是便于理解吧,如果更加有说服力的解释,可以在下方留言,大家一起讨论学习)
这样我们就可以很容易解释上一段代码了
1.代码块进入执行栈,首先明确这是一个宏任务
2.执行console.log(1),输出1
3.遇到setTimeout,是一个异步任务,且是一个宏任务,进入宏任务队列
4.遇到New Promise是同步任务,执行resolve,遇到Promise.then是异步任务,且是一个微任务,进入微任务队列。
5.执行console.log(4),输出4
6.第一个宏任务执行完毕,检查微任务队列中是否存在微任务,有的话全部执行。
7.执行Promise.then,输出3
8.微任务执行结束,检查宏任务队列,执行一个宏任务
9.执行setTimeout,输出2
10.检查微任务队列。。。检查宏任务队列。。。
用流程图表示,更加便于理解
那么常见的宏任务和微任务有那些呢?
- 宏任务:主代码块,setTimeout,setInterval等
- 微任务:Promise.then,process.nextTick等
值得一提的是在node环境下,process.nextTick的优先级高于Promise.then,也就是可以简单理解为:在微任务队列中也是有优先级的,优先执行nextTick部分,然后才会执行微任务中的Promise.then部分。至于为什么,感兴趣的伙伴可以去看一下这个zhihu.com/question/36…
写在最后
JavaScript的运行机制写到这里,算是告一段落了,重新整理了一遍之后,于我来说,对这些碎片化的知识更加清晰了,同时也遇到了很多新的问题,其实文章中还是很多地方都值得更深入的探讨,例如微任务队列是有优先级的,那么宏任务队列是否也有优先级?Node环境的事件循环又是通过什么来实现的?有时间的话这些问题会单独出一些文章去解答,算是给自己挖一些坑吧。
果然越学习,越感受到了自身基础的薄弱和JS世界的复杂,书山有路勤为径,学海无涯苦作舟,算是给自己加油打气吧,接下来一篇文章,算是正式进入了JavaScript的回顾,想谈谈JavaScript的作用域和作用域链。
参考
- ssssyoki juejin.cn/post/684490…
- 阮一峰 www.ruanyifeng.com/blog/2014/1…