最近跟同学谈论时聊到多线程,突然想到Javascript是一门单线程的语言,那么它是怎么实现异步的一些操作(如ajax请求,setTimeout等),先看一下下面的这些概念
目录
一、js单线程与非阻塞
-
单线程:是指一个浏览器进程中只有一个JS的执行线程,这个就是js的主线程,也就是js代码的执行栈,同一时刻内只会有一段代码在执行,这就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着,可以使用IE的标签式浏览试试看效果,这时打开的多个页面使用的都是同一个JS执行线程,如果其中一个页面在执行一个运算量较大的function时,其他窗口的JS就会停止工作
-
非阻塞:当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调
二、为什么js是单线程的
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言
js在设计之初只是为了解决一些简单的需求,主要用来与浏览器进行交互,比如进行各种各样的dom操作,试想,如果有两个线程同时去操作同一个DOM元素,一个线程点击了一个元素,而另一个删除了这个元素,那么浏览器到应该以哪一个为准呢?所以js的单线程是必要的,这也是javascript的基石
单线程的缺点
- 因为只有一个线程,代码需要排队按顺序执行,前一个任务执行完毕,后一个任务才会被执行
- 排队等待就需要大量的计算。耗费CPU。限制了js的执行效率。
- 如果有一段代码报错,就可能会导致代码阻塞,页面假死
三、浏览器多线程
- GUI 渲染线程
- 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
- 页面重绘和回流
- 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
- JS 引擎线程
- 负责 JS 脚本代码的执行
- 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
- 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
- 事件触发线程
- 负责将准备好的事件交给 JS 引擎线程执行
- 多个事件加入任务队列的时候需要排队等待(JS 的单线程)
- 定时器触发线程
- 负责执行异步的定时器类的事件,如 setTimeout、setInterval
- 定时器到时间之后把注册的回调加到任务队列的队尾
- HTTP 请求线程
- 负责执行异步请求
- 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行
例如:异步请求是由两个常驻线程,JS执行线程和事件触发线程共同完成的,JS的执行线程发起异步请求,这时浏览器会开一条新的HTTP请求线程来执行请求,这时JS的任务已完成,继续执行线程队列中剩下的其他任务,然后在未来的某一时刻事件触发线程监视到之前的发起的HTTP请求已完成,它就会把完成事件插入到JS执行队列的尾部等待JS处理。
又例如定时触发(settimeout和setinterval)是由浏览器的定时器线程执行的定时计数,然后在定时时间把定时处理函数的执行请求插入到JS执行队列的尾端,所以用这两个函数的时候,实际的执行时间是大于或等于指定时间的,不保证能准确定时的
四、宏任务(macro task)和微任务(micro task)
在任务对列中注册的异步回调又分为两种类型,宏任务和微任务,为了方便理解可以认为在任务队列中有宏任务队列和微任务队列,宏任务队列有多个,微任务只有一个
以下事件属于宏任务
- setTimeout()
- setInterval()
- setImmediate() (Node环境下)
- requestAnimationFrame
- UI渲染
…
微任务 - Promise
- pocess.nextTick(Node 环境下)
…
在一个事件循环中,异步事件返回结果后会被放到一个任务队列中,然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去,并且在当前执行栈为空的时候,主线程会先扫描一遍微任务队列是否还有事件存在,看还有没有微任务没有被执行,如果没有,则去宏任务队列中取出一个事件并把对应的回调到加入当前执行栈,如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环,也就是说当所有微任务被执行完毕后,才会开始执行宏任务
因此我们只需记住,当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件,同一次事件循环中,微任务永远在宏任务之前执行
以下代码的输出顺序是什么?
setTimeout(() => {
console.log(4);
})
new Promise(resolve =>