前言
众所周知,JavaScript是一门单线程的语言,虽然HTML5提出了Web Worker标准,允许JavaScript创建多个线程,但是它的子线程却完全受主线控制。且不能用于操作DOM,所以并没有改变JavaScript是单线程语言的本质
为什么是单线程?
JavaScript作为一门浏览器的脚本语言,他是用户和浏览器交互的桥梁,其中自然避免不了操作DOM并且渲染DOM这个根本且重要的事,而既然要操作DOM。如果是多线程的话,如果某一时刻,一个线程给一个DOM元素添加一个属性,另一个线程又要删除那个属性时该怎么办呢?要以哪个为主呢?所以它的用途已经决定了它是单线程的本质。
任务队列
单线程,这就异味着任务的执行需要一个个执行,而如何保证执行的顺序是可预期的,这时候就要引出任务队列的概念了,既然叫做队列,自然就遵循First-In-First-Out(FIFO)先进先出的原则,最先进入队列的最先执行。
但这里会出现一个问题,如果一些非阻塞性的任务执行需要耗时很久,如(Ajax请求等),需要时间等待才能执行结束的话,此刻控制的CPU就会造成不必要的资源浪费,所以在JavaScript中,任务又被分为了同步任务(synchronous)异步任务(asynchronous)两种
同步任务:会进入JS的主线程,在主线程上执行的任务,只有前一个任务执行完毕才会继续下一个任务
异步任务:异步任务不进入主线程,而是进入Event Table, 满足条件后进入“任务队列”(Task Queue),异步任务的执行,是在异步任务执行有了响应结果之后,在任务队列中推进一个事件,等待“执行栈”中的所有同步任务执行完毕,便读取“任务队列”,看看里面有哪些事件,这些事件对应的异步任务便结束等待,进入执行栈,开始执行
总结起来,异步任务的运行机制便可以如下
1. 所有的同步任务都在主线程上执行,形成一个执行栈
2. 主线程之外,会维护一个任务队列,只要异步任务运行有了结果,就在任务队列中放入一个事件
3. 如果执行栈的同步任务执行完毕,系统会读取任务队列,依次执行里面的任务
4. 主线程重复如上的步骤
事件循环和任务队列
任务队列其实是一个事件的队列,也可以理解为成消息队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关对应的异步任务可以进入执行栈了。主线程在执行完同步任务后,就会读取任务队列,其实就是读取里面的事件来执行。
而任务队列中的事件,除了一些IO设备的事件之外,还包括一些用户行为产生的事件,如点击,双击,获取焦点等回调,这些回调函数(事件)都会被推进任务队列,等待主线程读取然后依次执行
队列的本质是一个先进先出的数据结构,也就是说执行完的事件就会从队头弹出去,在队列中清空掉,而主线程执行栈的执行,就是读取任务队列中的事件,这个过程是循环不断的,直到队列清空为止,当前的执行环境才会结束,所以这个机制又被称为事件循环(Event Loop)
例子
下面,我们举个小例子来说明一下
console.log(1)
console.log(2)
console.log(3)
setTimeout(function() {
console.log(0)
}, 10)
// 输出 1, 2, 3, 0
上面的例子,定时器setTimeout就是一个异步任务,如果剩余的三个console都是同步任务,所以他们会按照JavaScript的运行机制——解释执行,依次进入主线程的执行栈,逐步执行,而setTimeout则会主线程挂起,直到10毫秒后,异步任务结束,便将对应的回调函数(事件)放入事件队列(任务队列)中,由主线程中执行完同步任务后,依次读取。
上面的例子可以稍微改造一下
console.log(1)
console.log(2)
console.log(3)
setTimeout(function() {
console.log(0)
}, 10)
console.log(5)
// 输出 1, 2, 3,5, 0
这个改造后的例子,我们在定时器后面又加了一个console,但是他输出的顺序依然排在了定时器的前面,这更是说明了,主线程会优先读取完所有的同步任务依次执行,结束后才会读取任务队列依次执行。
宏任务和微任务
在我的理解中,JS是分为了同步和异步任务,而异步任务又分为两种,一种是宏任务,一种又是微任务,同步任务的执行顺序优先于异步任务,而微任务又优先于宏任务。
宏任务
# | 浏览器 | Node | setTimeout | √ | √ | setInterval | √ | √ | setImmediate | x | √ | requestAnimationFrame | √ | x |
---|
微任务
# | 浏览器 | Node | process.nextTick | x | √ | MutationObserver | √ | x | Promise.then catch finally | √ | x |
---|
下面,我们再详细举个例子
console.log(1)
setTimeout(function(){console.log(2)}, 10)
console.log(4)
function fn() {
return new Promise((r, j) => {
console.log(6)
r()
})
}
Promise.resolve().then(function() {
console.log(5)
})
fn().then(function() {
console.log(7)
})
// 输出
// 1, 4, 6, 5, 7, 2,
以上的运行机制,便是先遇到到了代码块,姑且也叫同步任务,输出1,然后遇到宏任务,由主线程挂起,进入到Event Table,说的再细一点,是挂起后,进入了宏任务队列(macrotask queue),再之后又遇到代码块console,输出4,继续往下,声明了fn函数,但是还没调用,再往下,遇到了一个promise.then, 将其放入微任务队列(microtask queue), 再继续,我们调用了fn函数,进入了fn的执行环境,输出了6,然后调用了resolve,掉任务放进微任务队列,然后退出fn的执行环境。这时候,主线程的同步任务执行结束,开始扫描微任务队列,第一个输出5, 第二个输出了7,再继续,微任务队列被清空,开始进入宏任务队列扫描,输出了2,至此,这段代码便执行结束了。
值得注意的是,process.nextTick 永远大于 promise.then,在Node中, _tickCallback在每一次执行完TaskQueue中的一个任务后被调用,而这个_tickCallback中实质上干了两件事:
1.nextTickQueue中所有任务执行掉(长度最大1e4,Node版本v6.9.1)
2.第一步执行完后执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调)
所以很明显 process.nextTick > promise.then
但是,js和nodejs的event loop又存在于一些差异,到时候再找时间写一篇nodejs的event loop相关的文章吧
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!