进程

运行以后的程序叫做"进程"(process),一般情况下,一个进程一次只能执行一个任务。
如果有很多任务需要执行,不外乎三种解决方法。

(1)排队。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。

(2)新建进程。使用fork命令,为每个任务新建一个进程。

(3)新建线程。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。

浏览器中很多异步行为都是由浏览器新开一个线程去完成,一个浏览器至少实现三个常驻线程:

  • GUI渲染线程
    负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等;
    当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
    注意:GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起。
  • JS引擎线程
    JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本
    GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程
    用来控制事件循环
    当JS引擎遇到异步代码时,会将对应任务添加到事件触发线程中

JS引擎

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,比如最出名的就是Chrome浏览器的V8引擎,如下图所示,JS引擎主要有两个组件构成:

堆-内存分配发生的地方
栈-函数调用时会形一个个栈帧(frame)

image.png

解决JavaScript单线程带来的阻塞问题

JavaScript 在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事。为什么要这么设计,跟JavaScript的应用场景有关。
JavaScript 初期作为一门浏览器脚本语言,通常用于操作 DOM ,如果是多线程,一个线程进行了删除 DOM ,另一个添加 DOM,此时浏览器该如何处理?

虽然JS运行在浏览器中是单线程的,但是浏览器是事件驱动的(Event driven),浏览器中很多行为是异步(Asynchronized)的,会创建事件并放入执行队列中。

为了解决单线程运行阻塞问题,JavaScript用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop)。

什么是事件循环?

"Event Loop是一个程序结构,用于等待和发送消息和事件。简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。
image.png

上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,不存在等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

这种运行方式称为"异步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。

JS在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,JS引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈不同的另一个任务队列中等待执行。

简单描述:
(1)函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs。
(2)接着执行同步任务,直到Stack为空。
(3)在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待。
(4)当执行栈为空时,Event Loop 监听到调用栈为空了,就去取任务队列的一个任务放入调用栈。
image.png

  • Event Loop是由javascript宿主环境(像浏览器)来实现的;
  • WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
  • JavaScript 的并发模型基于"事件循环";
  • Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数

什么是事件队列和执行栈

事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。

执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。

微任务和宏任务是什么?

任务队列可以分为宏任务对列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的所有任务都执行完成后再去判断宏任务对列中的任务,如果有就将宏任务队首的事件压入栈中执行,当这个宏任务执行完了后,又去检查是否有微任务待执行。如此循环的过程就称为事件循环。

异步任务还可以细分为微任务与宏任务
微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
宏任务:时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。

微任务包括了
Promise.then、node 中的 Process.nextTick 、对 Dom 变化监听的 MutationObserver。

宏任务包括了
script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

图解宏任务和微任务

每一个宏任务结束后,都会判断是否有待执行的微任务,如果有,执行全部的微任务,然后执行下一个宏任务。

事件循环.png

举例

// 例题
Promise.resolve().then(function promise1 () {
       console.log('promise1');
    })
setTimeout(function setTimeout1 (){
    console.log('setTimeout1')
    Promise.resolve().then(function  promise2 () {
       console.log('promise2');
    })
}, 0)

setTimeout(function setTimeout2 (){
   console.log('setTimeout2')
}, 0)

1.执行整段代码,遇到Promise.resolve()这里,直接执行,下面遇到then(),它是微任务,放入到微任务列表中等待执行
2.遇到定时器,它是宏任务,放入宏任务列表等待执行
3.又遇到定时器,它是宏任务,放入宏任务列表
4.同步代码执行完了,开始执行微任务,即执行then的回调,打印promise1
5.微任务全部执行完了,执行宏任务,执行第一个定时器的回调,打印setTimeout1,遇到then()加入到微任务列表中
6.上一个宏任务执行完了 检查微任务列表,执行then的回调,打印promise2
7.微任务全部执行完了,执行宏任务,执行第二个定时器的回调,打印setTimeout2
//  结果 promise1 setTImeout1 promise2 setTimeout2。
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
}, 0)
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
}, 0)
// 1,7,6,8,2,4,3,5,9,11,10,12
async function getData() {
    console.log(1)
    let data = await fn()
    console.log(data)
}
function fn () {
    console.log(4)
    return new Promise((resolve, reject) => {
        console.log(5)
    }).then(() => {
        console.log(6)
    }, () => {
        console.log(7)
    })
}
console.log('start')
setTimeout(() => {
    console.log(2)
}, 0)
getData()
console.log('end')

// start 1 4 5 end 2

详细资料可以参考:
《进程和线程的一个简单解释》
《浏览器事件循环机制(event loop)》
《详解 JavaScript 中的 Event Loop(事件循环)机制》
《什么是 Event Loop?》
《这一次,彻底弄懂 JavaScript 执行机制》
《面试官:说说你对JavaScript中事件循环的理解》


清风
1 声望0 粉丝