JavaScript运行机制(事件循环)

javascript运行时是单线程的。但是JavaScript为什么是单线程的吗?javascript又为什么需要异步?JavaScript又是如何依靠单线程实现异步的?我们又为什么要掌握JavaScript的单线程?

首先来先看下面的例子。

setTimeout(function(){

console.log('我执行了')

},1000)

这段代码相信大家都非常熟悉,一个常见的定时函数,一秒后执行setTimeout中的函数,但真的是一秒后吗?这种理解是不准确的,那应该怎么来理解呢?这时我们就需要了解JavaScript的运行机制了,我们先来解释一下本文开头的几个问题。

1.JavaScript为什么是单线程的?

JavaScript设计的初衷是用在浏览器中, 那么,我们来想象一下,如果JavaScript是多线程的话,必然可以有两个进程,process1和process2,那么这两个进程可以同时对同一个DOM进行操作。如果这个时候,一个进程要删除这个DOM,另一个进程要编辑这个DOM,岂不是矛盾嘛。所以,这样应该更好理解,JS为什么是单线程了。

2. JavaScript为什么需要异步?单线程为什么需要异步呢?

JavaScript如果不存在异步,而是自上而下执行,这样的话,假如上一行解析时间很长,那么下面的代码直接就会被阻塞。这种现象对于用户来说,意味着"卡死"。严重影响用户流失,这样解释好理解吧,所以JavaScript需要异步处理。

3. 单线程如何实现异步呢?JavaScript竟然需要异步,那么它是如何实现异步的呢?

JavaScript是通过事件循环(event loop)来实现的,事件循环机制也就是今天要说的JavaScript运行机制。

 

运行机制详解

1.同步任务和异步任务

先来看一个例子:

    console.log(1)
    setTimeout(function {
        console.log(2)
    },0)
    console.log(3)

这段代码的输出顺序应该是什么呢?

输出:1 3 2

其中setTimeout需要延迟一段时间才去执行,这类代码就是异步代码。

看到这个结果,所以通常我们都这么理解JS的执行原理:

第一,判断JS是同步还是异步,同步进入主线程,异步则进入event table。

第二,异步任务在event table中注册函数,当满足触发条件后,被推入event queue(事件队列)。

第三,同步任务进入主线程后一直执行,直到主线程空闲,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程。

按到这个逻辑,上面这段实例代码,是不是就很好理解了。1,3是同步任务进入主要线程,自上而下执行,2是异步任务,满足触发条件后,推入事件队列,等待主线程有空时调用。

2.宏任务和微任务

然而,按照同步和异步任务来理解JS的运行机制似乎并不准确。

来看一段代码。看看它的输出顺序。

    setTimeout(function(){
        console.log(1)
    },0)
    new Promise(function(resolve){
        console.log(2)
        resolve()
    }).then(function(){
        console.log(3)
    })
    console.log(4)

上面这段代码,按同步和异步的理解,输出结果是:2,4,1,3。因为2,4是同步任务,按顺序在主线程自上而下执行,而1,3是异步任务,按顺序在主线程有空后自先而后执行。

可事实输出并不是这个结果,而是这样的:2,4,3,1。为什么呢?来理解一下宏任务和微任务。

宏任务:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务:MutationObserver、Promise、process.nextTick(node环境)。

第一,执行一个宏任务(主线程的同步script代码),过程中如果遇到微任务,就将其放到微任务的事件队列里。

第二,当前宏任务(同步的)执行完成后,会查找微任务的事件队列,将全部的微任务依次执行完,再去依次执行宏任务事件队列。

上面代码中promise的then是一微任务,因此它的执行在setTimeout之前。

宏任务和微任务在意义上是有绝对区别的,宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务。微任务则是语言层面的。

JavaScript执行顺序总结为:

同步宏任务→微任务→异步宏任务

process.nextTick 是一个独立于 eventLoop 的任务队列。

同步宏任务完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。

所以最后总结一下,对于文章一开头提到的那段代码,我们可以准确的理解为:

1秒后,setTimeout里的函数会被推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。也就是需要同时满足两个条件(1)1秒后。(2)主线程必须空闲,这样1秒后才会执行该函数。

补充:

requestAnimationFrame

requestAnimationFrame也属于执行是异步执行的方法,但我任务该方法既不属于宏任务,也不属于微任务。按照MDN中的定义:

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

requestAnimationFrame是GUI渲染之前执行,但在微服务之后,不过requestAnimationFrame不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值