如何验证JavaScript是单线程?

最近虽疫情严重,但是春天马上就要到来,面试季也要拉开序幕,大家在跳槽之前一定要努力复习,以免错过心仪的公司。

言归正传

今天逛技术论坛看到了这么一个问题,"如何用代码验证JS是单线程的?"。作为一个两年工作经验的前端工程师,虽然知道JS是单线程的,但是确实没有深入研究过。

于是查翻资料,夯实了一下这方面的基础。

进程与线程

所谓进程,简单理解就是程序的一次执行过程,它占有独有的一片内存空间,而线程是CPU的基本调度单位。

一个进程中一般至少有一个运行的线程:主线程,

一个进程中也可以同时运行多个线程,这时程序就是多线程,

一个进程中的数据可以供其中多个线程共享, 多个进程之间的数据不能直接共享。

前端开发自然离不开浏览器,目前大部分新版浏览器都是多进程的,老版本大多数都是单进程的,同时浏览器都是多线程的。

验证JavaScript是单线程

直接上代码

let nowTime =  Date.now()
    console.log("现在的时间:" + nowTime) // 1581391609874
    setTimeout(function(){
        console.log("之后的时间:" + (Date.now() - nowTime)) // 116
    }, 100)
    for (let i = 0; i < 9999; i++) { // 耗时任务
    }
前端学习裙:950919261

通过代码可以看出,首先输出当前时间nowTime,然后通过定时器延迟100毫秒后输出延迟后的时间与nowTime的时间差。按照定时器的设定,他们的差值应该为100,但实际输出为116,这是为什么?

这是因为JavaScript引擎执行代码的基本流程是先执行初始化代码,包括一些特别的代码,比如设置定时器,绑定监听,发送ajax等。等初始化代码执行完成后才可能执行回调代码(异步)。

上述代码先执行除了定时器回调function以外的代码,然后再执行function,导致执行过程在for循环中浪费了部分时间,等for循环执行完后才会执行定时器回调function,所以才会出现16毫秒的偏差。

而如果是多线程的话,当遇到定时器或者耗时任务时,可能会再次开启一个线程去单独执行对应的定时器或耗时任务,肯定不会像JavaScript一样出现阻塞,这个例子可以验证JavaScript是单线程。

再看一个例子

setTimeout(function(){
        console.log("1s后执行")
    }, 1000)
    function func() {
        console.log("func()")
    }
    func() //  func()
    alert('阻断')
    console.log(123123)

执行上述代码会发现,当通过alert阻断执行后,只要不点掉alert弹窗,后续任何任务都不会执行,包括定时器回调。通过这个例子也可以看出JavaScript是单线程的, 一旦一个任务阻断卡死了,后续的都不会执行。

为什么JavaScript要采用单线程?

了解了如何验证JavaScript是单线程以后,已知单线程如果当前任务没有执行完成,后续任务会被阻断,不会执行,那为什么JavaScript还要采用单线程呢?

其实这个用一个场景就可以解答,首先JavaScript尽管可以作用于服务器(Node.js),但是它的设计初衷是作用在浏览器中的脚本语言,所以在浏览器中操作DOM时,如果JavaScript是多线程的就可能出现多个线程操作同一个DOM的情况,比如线程1删除了该DOM,而线程2修改了该DOM,两个冲突的命令会导致浏览器不知所措,所以将JavaScript设计成单线程可以完全避免这个问题。

单线程如何实现异步?

这就要了解一下JavaScript的事件循环模型。

JavaScript引擎在执行代码的时候会先初始化执行代码,也就是同步代码,包括绑定DOM事件监听,设置定时器,发送Ajax请求等,当初始化完成后会交给对应的管理模块处理,当事件发生时,管理模块会通知JavaScript引擎将对应的回调函数及其数据添加到回调队列中,当所有初始化代码执行后,才会遍历读取回调队列中的回调函数执行。

用个场景简单描述一下,比如我们需要一个点击操作和一个定时器操作,那么JavaScript会先初始化这些代码,绑定一个click事件,设置定时器,初始化后发送任务交给浏览器,因为浏览器是多线程的,所以浏览器会用多线程处理这些任务。

当click事件发生或定时器启动运行时,浏览器对应的事件管理模块和定时器模块会通知JavaScript引擎将对应的回调函数及其数据添加到回调队列中,当初始化代码执行完成后,就会遍历读取回调队列中的回调函数执行,如果碰上最开始那个for循环的例子,可能就会需要一定时间。

关于setTimeout严谨地说法

所以,最开始的这个例子

let nowTime =  Date.now()
    console.log("现在的时间:" + nowTime) // 1581391609874
    
    setTimeout(function(){
        console.log("之后的时间:" + (Date.now() - nowTime)) // 126
    }, 100)
    
    for (let i = 0; i < 9999; i++) { // 耗时任务
    }

其中setTimeout按照一般的说法是3s后执行function回调函数,这样说其实是不准确的,严谨地解释应该是3s后setTimeout中的回调函数会被推入回调队列中,而回调队列中的回调函数需要初始化代码for循环执行完成后才会执行,所以才会造成时间差不准确。

发布了119 篇原创文章 · 获赞 19 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq470603823/article/details/104271862