JavaScript 线程机制与事件循环机制

文章目录

  • JavaScript 线程机制与事件机制
    • 进程与线程
      • 面试题
      • 进程和线程
        • 进程的通信方式
      • 浏览器多进程架构
        • 如何实现浏览器多标签之间的通讯
        • H5 Web Workers JS多线程运行
    • 事件循环机制
      • 面试题
      • 概念
      • 浏览器的事件循环机制
      • Node的事件循环机制
        • Node和浏览器是不同的JS执行环境
        • 阶段概述
        • node事件循环代码输出题 用于理解
    • 代码输出题

JavaScript 线程机制与事件机制

进程与线程

面试题

  • 浏览器标签页是进程还是线程
  • 进程和线程的区别
  • 进程之间通信的方式
  • 多标签之间如何通讯

进程和线程

进程
启动一个程序时,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这个运行环境叫进程。
线程
是进程内的一个独立执行单元,一个进程中可以有多个线程,多个线程共享进程的数据。进程中的任意一线程执行出错,都会导致整个进程的崩溃。

进程的通信方式

  • 管道: 本质上就是内核中的一个缓存,一个进程往管道中写,另一个进程去管道中读。
    特点: 通信方式效率是低下的,不适合进程间频繁的交换数据。一个进程往管道输入数据,则会阻塞等待别的进程从管道读取数据。
  • 消息队列:A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
    特点:避免管道的堵塞问题,但是存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
  • 共享内存: 系统加载一个进程的时候,分配给进程的是虚拟内存空间。共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
    特点:共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。
  • Socket:Socket是操作系统提供给程序员操作网络的接口
    特点:不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

内核态和用户态
用户态与内核态的概念就是CPU指令集权限的区别。
CPU指令集操作的权限由高到低划为4级

  • ring 0 可以使用所有 CPU 指令集,

  • ring 1

  • ring 2

  • ring 3 仅能使用常规CPU指令集,不能使用操作硬件资源的 CPU 指令集,比如 IO 读写等

  • ring 0 被叫做 内核态,完全在操作系统内核中运行,由专门的内核线程在 CPU 中执行其任务

  • ring 3 被叫做 用户态,在应用程序中运行,由用户线程在 CPU 中执行其任务

浏览器多进程架构

JavaScript 线程机制与事件循环机制_第1张图片

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页
    • JS引擎线程:JavaScript引擎V8,负责处理JavaScript脚本程序。依靠任务队列来进行js代码的执行,所以js引擎会一直等待着任务队列中任务的到来,然后加以处理。
    • GUI渲染线程:负责渲染浏览器界面,解析 HTML,CSS,构建render树,布局和绘制等。
    • 计时器线程:因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响计时的准确。当使用setTimeout或者setInterval时,需要定时器线程计时。计时到了之后,将对应的回调放入事件队列中,等待JS执行。
      规定要求setTimeout中低于4ms的时间间隔算为4ms。
    • 异步http请求线程:负责异步请求管理,XMLHttpRequest在连通后通过浏览器新起一个线程请求。检测到状态变化时,如果有设置回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,等到JS执行
    • 事件触发线程控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理。
  • GPU进程: GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制。
  • 网络进程:主要负责页面的网络资源加载。
  • 插件进程: 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

JS是单线程还是多线程?
JS是单线程运行的,但使用H5中的Web Workers可以多线程运行

如何实现浏览器多标签之间的通讯

方法 通讯范围 优缺点
localStorage 相同浏览器的同源窗口都会共享
可以使用storage事件监听localStorage变化
优点: 操作简单,易于理解
缺点: 存储大小限制,使用范围是相同浏览器的同源窗口
websocket 与服务器建立websocket连接的页面
如果pageA更改了数据,那么向服务端发送一条消息或数据,服务端在将这条消息或数据发送给pageB即可,这样就简单实现了两个标签页之间的通信
优点: 理论上可是实现任何数据共享跨域共享
缺点: 需要服务端配合增加服务器压力
sharedWorker sharedWorker就是webWorker中的一种,它可以由所有同源页面共享
sharedWorker的原理和websocket有点类似,都是广播和接收的原理
优点:没有大小限制 缺点: 跨域不共享 调试不方便 兼容性不好


参考文章

H5 Web Workers JS多线程运行

是什么?
Web Workers 是Html5提供的一个多线程解决方案,Web Workers可以在独立于主线程的后台线程中,运行一个脚本操作。但是该脚本程序不能操作DOM,主要用于计算
有什么用?
可以在独立线程中执行费时的处理任务,避免JS引擎线程阻塞线程渲染视图。
特点

  1. DOM限制:没有WebAPI,该脚本程序不能操作DOM,主要用于计算
  2. Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成
  3. 同源限制:分配给 Worker 线程运行的脚本,必须与主线程的脚本文件同源,否则存在跨域问题。

JavaScript 线程机制与事件循环机制_第2张图片1.主线程postMessage通知worker线程
2.worker线程onMessage方法接收到消息,去安排工作,完成工作后,用postMessage方法通知主线程
3.主线程onMessage方法监听消息

应用场景
适合做非常耗时的计算工作

  • 预取数据:为了优化网站或者网络应用及提升数据加载时间,你可以使用 Workers 来提前加载部分数据
  • 加密:加密有时候会非常地耗时,特别是如果当你需要经常加密很多数据的时候。

事件循环机制

面试题

  • 什么是事件循环机制,node的事件循环机制和浏览器的事件循环机制
  • 宏任务和微任务有哪些?
  • dom渲染在什么时候执行的?
  • 微任务会一次性清空一个,还是全部清除?执行微任务的过程中又产生了微任务会什么时候执行?
  • async await的事件循环执行方式
  • 如果微任务队列有三个任务,宏任务队列有三个任务,请问会执行几次事件循环
  • 代码输出题
  • html5的DOM操作(渲染进程)和微任务、宏任务关系
  • 为什么需要分宏任务和微任务
  • setTimeout时间准吗?解决办法:requestAnimationFrame

概念

是什么?
JS是单线程,当遇见异步任务时,在执行异步任务时需要等待任务完成,浪费大量的时间。所以将异步任务交给任务队列中,当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务,将其取出执行。整个执行过程,我们称为事件循环过程。

由于所有任务的优先级不一样,将任务分为微任务和宏任务。这种设计是为了给紧急任务插队的机会,否则新入队的任务永远被放在队尾。

微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。所以有些时候也有人将同步代码看成一个宏任务。

事件循环机制主要的作用
事件循环机制用于管理异步回调函数什么时候回到主线程中执行

宏任务

  • setTimeout、setInterval
  • setImmediate(node 独有)
  • DOM事件、Ajax事件
  • script(整体代码)

微任务

  • process.nextTick(node 独有 与普通微任务有区别,在微任务队列执行之前执行)
  • Promise一些方法,如.then
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

浏览器的事件循环机制

这里DOM渲染的时机可以先说完循环机制再添加

  1. 同步任务和异步任务进入不同的执行环境,同步任务放入执行栈中,异步任务放入任务队列中。
  2. 先执行同步代码,执行完后,会先将微队列中的所有微任务依次执行完毕。
  3. 然后检查有没有DOM渲染需要执行,有就执行。
  4. 没有就开启下一轮循环,取出一个宏任务执行。一个宏任务执行完毕后就清空微队列,然后见检查有没有DOM渲染需要修改。循环这个过程

JavaScript 线程机制与事件循环机制_第3张图片
DOM的修改不会立马导致渲染,渲染线程和Javascript线程是互斥的,必须等待Javascript的这次调度执行完或线程挂起了,才能执行渲染。

这次调度可以看成是一轮事件循环完,一次事件循环=宏任务(第一次是同步代码)+微任务
在这里插入图片描述

显示器的刷新频率是60Hz,浏览器也会尽量保持60Hz的刷新率运行,也就是16.7ms刷新一帧所以 浏览器渲染的触发并不是每次事件循环都会触发,他会以大约60帧每秒的频率来触发。如果一次循环内没有触发render,那requestAnimation回调也会执行

Node的事件循环机制

Node和浏览器是不同的JS执行环境

浏览器和nodejs是不同的JS运行环境

1.V8引擎负责解析和执行JavaScript代码
2.内置API是由运行环境提供的特殊接口,只能在所属的运行环境中被调用
JavaScript 线程机制与事件循环机制_第4张图片
node:v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。
JavaScript 线程机制与事件循环机制_第5张图片

node的事件循环机制是处理非阻塞 I/O 操作的机制。

将整个流程分为多个阶段,每个阶段都有一个先进先出执行回调的队列,不同的事件放在不同的队列中等待主线程执行。

阶段概述

  • 定时器检测阶段(timers):本阶段执行 setTimeout、setInterval 里面到时间的回调函数。
  • I/O事件回调阶段(pending callbacks):执行上一轮循环中未被执行的一些I/O回调。
  • 闲置阶段(idle, prepare):仅系统内部使用。
  • 轮询阶段(poll):检索新的 I/O 事件,执行与 I/O 相关的回调(除了关闭的回调函数,那些由计时器和 setImmediate() 之外的)
    如果事件队列中有回调函数,执行直到清空队列
    如果队列为空
    • 如果此时setImmediate队列中存在要执行的回调函数,进入check阶段
    • 如果此时timer队列中存在要执行的回调函数,进入timers阶段。
    • 如果都没有, 将停留一段事件等待新的回调函数进入,直到等待计时器超时后进入check阶段
  • 检查阶段(check):setImmediate() 回调函数在这里执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on(‘close’, …)。
//事件循环的每一轮循环,会按照下图给定的优先级顺序进入七个阶段的执行,
/*
   ┌───────────────────────────┐
┌─>│           timers          │     -> 定时器,延时器的执行    
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │     -> i/o
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
*/

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

在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

总结
node11 一旦执行完一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列。

在 node11 之前执行完一个阶段后才开始执行微任务。

浏览器和Node环境下事件循环的区别

  • Node端,microtask 在事件循环的各个阶段之间执行,也就是一个阶段的一个宏任务执行完毕,就会去执行microtask队列的任务。
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

node事件循环代码输出题 用于理解

练习题1
执行同步代码
setTimeout 1ms后将回调放入到timer阶段
setImmediate回调注册到check阶段
sleep(1000); 阻塞线程1s秒
之后开始事件循环,此时定时器已经到事件了
所以先输出1,后输出2

setTimeout(()=>console.log("1"),0);
setImmediate(()=>console.log("2"));

function sleep(delay){
  let start = new Date().getTime();
  while(new Date().getTime()-start < delay){
    continue;
  }
}
sleep(1000); //阻塞线程1s秒

练习题2

执行同步代码
setTimeout 1ms后回调注册到timer阶段
setImmediate回调注册到check阶段
然后进入事件循环,但是此时并不知道是否已经过了1ms,所以输出是不确定的

setTimeout(()=>console.log("1"),0);
setImmediate(()=>console.log("2"));

在nodejs中, setTimeout(demo, 0) === setTimeout(demo, 1)
在浏览器里面 setTimeout(demo, 0) === setTimeout(demo, 4)

练习题3

先执行同步代码,将fs.readFile的回调函数放入poll轮询中。
进入事件循环当中,从timer阶段开始,到poll轮询阶段,执行回调函数,将setImmediate回调放入check阶段,将setTimeout回调放入timers回调。poll队列为空,发现check阶段有回调函数,进入check阶段,输出setImmediate。进入下一轮事件循环进入timers阶段,输出setTimeout

var path = require('path');
var fs = require('fs');
​
fs.readFile(path.resolve(__dirname, '/read.txt'), () => {
	 setTimeout(() => {
        console.log('setTimeout')
    }, 0)
    setImmediate(() => {
        console.log('setImmediate');
    })
});

练习题4

开始异步读文件,然后执行setImmediate,读文件成功后放入poll队列等待执行

var fs = require('fs');
fs.readFile(__filename, () => {
  console.log('poll');
})
setImmediate(() => {
  console.log('immediate');
});
/*
immediate
poll
*/

代码输出题

**重点:将async/await改写成promise **

知识点1: await是.then的语法糖。await A函数表示先执行A函数、A函数返回一个promise对象,通过.then获取promise的结果。 函数的执行是同步的,但是获取promise的结果是异步的。

知识点2:await是一个让出线程的标志, await函数后面的代码,需要等到await函数执行完毕后才执行。await修饰的函数执行完毕后,会跳出async修饰的函数,执行其他代码。

async function async1() {
	await async2();
	console.log('async1 end')
}
async function async2() {
	console.log('async2 end') 
}
async1();

//改写后
function async1() {	
	//await函数前的代码
	new Promise(
	(resolve)=>{console.log('async2 end')}//await后面的函数
	)
	.then(//await函数后的代码
	res=>console.log('async1 end')
	)
}

练习题1

console.log('script start') 

async function async1() {
	await async2()
	console.log('async1 end'); //放入微队列①
}
async function async2() {
	console.log('async2 end')
}

async1()

setTimeout(function() {
	console.log('setTimeout'); //放入宏队列①
}, 0)

new Promise(resolve => {
	console.log('Promise');
	resolve()
})
.then(function() {
console.log('promise1'); //放入微队列②
})
.then(function() {
console.log('promise2')//放入回调函数数组中存起来,等待前一个.then的结果
})

console.log('script end')
/*
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
*/

面试题:微任务会一次性清空一个,还是全部清除?执行微任务的过程中又产生了微任务会什么时候执行?
根据promise1promise2setTimeout的输出顺序可以得出答案: 全部清除,产生了微任务后放入微队列,等待前面微任务执行完毕后出队执行

练习题2

const promise = new Promise((resolve, reject) => {
  console.log(1); 
  setTimeout(() => {//放入宏队列1
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => { //回调函数被保存,等待promise的状态改变后执行
  console.log(res);
});
console.log(4);
/* 1 2 4 timerStart timerEnd success */

练习题3

setTimeout(() => {
     console.log("0") //宏队列1
}, 0)
new Promise((resolve,reject)=>{//①
    console.log("1") //同步代码输出
    resolve()		//状态改变  先改变状态再执行回调
}).then(()=>{    //微队列1     //①.then
    console.log("2")  
    new Promise((resolve,reject)=>{//②
          console.log("3")
          resolve()//状态改变 
    }).then(()=>{   //微队列3 //②.then
          console.log("4") 
   }).then(()=>{  //微队列5 .then是同步的先指定回调会缓存起来,先指定回调后改变状态
          console.log("5")
   })
}).then(()=>{
   console.log("6") //微队列4
})
new Promise((resolve,reject)=>{
   console.log("7") //同步代码输出
   resolve() //状态改变
}).then(()=>{  //微队列2       
	console.log("8")
})
//1 7 2 3 8 4 6 5 0

容易出错的是6和5的顺序
4的.then先执行了,4放入微队列,但是5的.then是同步执行的,也就是此时先指定回调函数,再改变状态,所以5的.then里面的回调会缓存起来,等4的状态改变时再执行。
在下面添加了一个6.then会更清晰。

new Promise((resolve,reject)=>{
   console.log("1") 
   resolve()		
}).then(()=>{    
   console.log("2")  
   new Promise((resolve,reject)=>{
          console.log("3")
          resolve()
   }).then(()=>{   
          console.log("4") 
  }).then(()=>{       
          console.log("5")
          reject(8);
   }).then(()=>{       
          console.log("6")
   })
}).then(()=>{  
        console.log("7") 
})
//123475

对于4.then把回调函数放入微队列,同步执行5.then,缓存回调函数,同步执行6.then缓存回调函数。相当于内层的promise已经执行完了,状态是成功,6.then只会影响4.then.then的结果,对于内层promise来说,同步代码执行完后没有抛出异常或者调用resolve、reject,此时内层promise就是成功。同步执行7.then,7.then的回调函数放入微队列

练习题4

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2() {
    console.log('async2')
}

console.log('script start')

setTimeout(function () {
    console.log('setTimeout0')
    process.nextTick(() => console.log('nextTick3'));
}, 0)

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

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function () {
    console.log('promise3')
})

console.log('script end')
  1. 开始执行第一轮,先找到同步任务,输出script start
  2. 遇到第一个setTimeout,1ms后将里面的回调函数setTimeout0放入timer队列中
  3. 遇见第二个setTimeout,300ms后将里面的回调函数setTimeout2放到timer队列中
  4. 遇到第一个setImmediate,将里面的回调函数setImmediate放到 check 队列中
  5. 遇到第一个 nextTick,将其里面的回调函数nextTick1放到本轮同步任务执行完毕后执行
  6. 执行执行 async1函数,输出 async1 start,执行 async2 函数,输出 async2,async2 后面的输出 async1 end进入微任务,等待下一轮的事件循环
  7. 遇到第二个nextTick,将其里面的回调函数nextTick2放到本轮同步任务执行完毕后执行
  8. 遇到 new Promise,执行里面的立即执行函数,输出 promise1、promise2
  9. then里面的回调函数进入微任务队列
  10. 遇到同步任务,输出 script end 此时同步代码执行完毕
  11. 开始新一轮了,先依次输出 nextTick 的函数,分别是 nextTick1、nextTick2
  12. 开始执行微任务队列,依次输出 async1 end、promise3
  13. 执行timer 队列,输出 setTimeout0,遇见nextTick3,等本轮结束完后,执行。
  14. 本轮执行完了,执行nextTick3
  15. 接着执行 check 队列,依次输出 setImmediate
  16. 300ms后,timer 队列存在任务,执行输出 setTimeout2

15-16不是绝对的,需要看执行前面执行的时间有没有超过300ms

你可能感兴趣的:(JavaScript,面试题,javascript)