# js执行机制

# 同步任务与异步任务

同步任务:在主线程上排队执行的任务,前一个任务完整地执行完成后,后一个任务才会被执行。

异步任务:不会阻塞主线程,在其任务执行完成之后,会再根据一定的规则去执行相关的回调

从上图中可以分析出这段代码的运行轨迹。首先 console.log('Hi') 是同步代码,直接执行并打印出 Hi 。接下来继续执行定时器 setTimeout ,定时器是异步代码,所以这个时候浏览器会将它交给 Web APIs 来处理这件事情,因此先把它放到 Web APIs 中,之后继续执行 console.log('Bye') , console.log('Bye') 是同步代码,在调用堆栈 Call Stack 中执行,打印出 Bye 。

到这里,调用堆栈 Call Stack 里面的内容全部执行完毕,当调用堆栈的内容为空时,浏览器就会开始去任务队列寻找下一个任务,此时任务队列就会去 Web API 里面寻找任务,遵循先进先出原则,找到了定时器,且定时器里面是回调函数 cb1 ,于是把回调函数 cb1 传入任务队列中,此时 Web API 也空了,任务队列里面的任务就会传入到调用堆栈里Call Stack 里执行,最终打印出 cb1 。

浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

  • GUI 渲染线程
  • js引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程

js是单线程,H5中提出了Web-Worker,但js单线程这一核心仍未改变。一切js版的"多线程"都是用单线程模拟出来的!

既然js是单线程,js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么假如浏览新闻,新闻包含的超清图片加载很慢,难道要一直卡着直到图片完全显示出来?因此任务分为两类:

  • 同步任务
  • 异步任务

网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

同步和异步任务分别进入不同执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,这就是Event Loop(事件循环)。

怎么知道主线程执行栈为空?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

let data = [];
$.ajax({
    url:www.javascript.com,
    data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');

ajax进入Event Table,注册回调函数success。执行console.log('代码执行结束')。

ajax事件完成,回调函数success进入Event Queue。主线程从Event Queue读取回调函数success并执行。

# setTimeout

有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?

setTimeout(() => {
    task()
},3000)

sleep(10000000)

task()进入Event Table并注册,计时开始。执行sleep函数,很慢,计时仍在继续。3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没执行完,只好等着。sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述流程走完,setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那只能等着,导致时间大于3秒。

即便主线程为空,0毫秒实际上也达不到。根据HTML标准,最低4毫秒。

# setInterval

他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于setInterval(fn,ms)来说,已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。


数据大,setTimeout和ajax返回的时间顺序不一致,因为循环的时间远超3S,所以两个定时器都已经到可以执行的时间,而3s的又在前面,所以先执行。至于ajax,有时返回的快点,有时返回的慢些。

<script src="https://code.jquery.com/jquery-3.7.1.min.js" 
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous">
</script>
setTimeout(() => {
    console.log('延时3秒');
	console.log(new Date().valueOf())
},3000)
$.ajax({url:'/asdji'}).then(res=>{
	console.log(`res`,res)
	console.log(new Date().valueOf())
})

for(let i=0 ;i< 100000000;i++){
	if(i%500 === 0 ){
		console.log(i)
	}
}
setTimeout(() => {
    console.log('延时2秒');
	console.log(new Date().valueOf())
},2000)
console.log(10000)

# 宏任务与微任务

process.nextTick(callback)类似node.js版的"setTimeout" ,在事件循环的下一次循环中调用 callback 回调函数。除了广义的同步任务和异步任务,对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate ()-Node,requestAnimationFrame (存在争议,但是符合宏任务特征)-浏览器,I/O 操作、UI 渲染(浏览器).在ECMAScript中,macrotask也被称为task,我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他
  • micro-task(微任务):Promise,process.nextTick(Node.js),Object.observe,MutationObserver,在ECMAScript中,microtask也被称为jobs

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

微任务是宏任务的组成部分,微任务与宏任务是包含关系,并非前后并列,宏任务包含微任务.如果要谈微任务,需要指出它属于哪个宏任务才有意义.

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完

宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';

上面动图背景直接渲染了粉红色,根据上文里讲浏览器会先执行完一个宏任务,再执行当前执行栈的所有微任务,然后移交GUI渲染,上面四行代码均属于同一次宏任务,全部执行完才会执行渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉上,只会看到页面变成粉红色

document.body.style = 'background:blue';
setTimeout(()=>{
    document.body.style = 'background:black'
},200)

上述代码中,页面会先卡一下蓝色,再变成黑色背景,页面上写的是200毫秒,可以把它当成0毫秒,因为0毫秒的话由于浏览器渲染太快,录屏不好捕捉.

回归正题,之所以会卡一下蓝色,是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:pink'
});
console.log(3);

控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出

页面的背景色直接变成粉色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了粉色,然后才执行的渲染

事件循环,宏任务,微任务的关系如图所示:

setTimeout(()=>{ //宏任务2
    console.log(1);
}, 0)
setTimeout(()=>{  //宏任务3
    console.log(2); 
}, 0)
console.log(3);
  • 浏览器开始执行代码时启动了第一个宏任务(script)并开始执行.
  • 在执行宏任务1途中遇到了第一个定时器,浏览器便会启用一个新线程去跑定时器的逻辑,而当前的js线程不会停直接跳过定时器继续往下执行.当定时器的那条线程跑完后,它的回调函数被添加到js线程的宏任务队列中等待,这就是宏任务2.
  • 而js线程这边又遇到了定时器又开启一条线程跑定时器的逻辑,js线程跳过这段继续往下执行.当定时器线程跑完后,它的回调函数被添加到宏任务队列等待,这就形成了宏任务3,宏任务3排在宏任务2的后面.
  • js线程走到最后输出了3,此时宏任务1就结束了.浏览器此刻就会去 宏任务队列 中寻找,排在最前面的是宏任务2,执行输出1.宏任务2结束又执行宏任务3输出2.
setTimeout(()=>{ //定时器1
    console.log(1);
}, 1000)
setTimeout(()=>{  //定时器2
    console.log(2); 
}, 0)
console.log(3);

这时的执行输出顺序是: 3 --> 2 --> 1

  • 这就是浏览器开始执行代码时启动了第一个宏任务(script)并开始执行.
  • 遇到了定时器1 开启了定时器线程去跑,
  • 再遇到定时器2,也放到了定时器线程去跑,
  • 由于定时器2的时间间隔比定时器1的短,它先跑完了,就把它的回调函数放到js线程的宏任务队列中
  • 定时器1要等一秒后才跑完,才把它的回调函数放到js线程的宏任务队列中
  • 定时器2的回到函数先放入js线程的宏任务队列中,定时器1后放,所以打印的结果,就是 3 --> 2 --> 1

从上图可知,EventLoop(事件循环)机制,把宏任务形成了一个拥有先后顺序的宏任务队列.每个宏任务中分为同步代码和微任务队列.

  • 假设js当前的线程执行宏任务1,先执行宏任务1中的同步代码.
  • 如果碰到Promise或者process.nextTick,就把它们的回调放入当前宏任务1的微任务队列中.
  • 如果碰到setTimeout, setInterval之类就会另外开启线程去跑相应的逻辑,而js线程跳过这段继续往下执行.另起的线程执行完毕后再在当前宏任务1的队列后面创建新的宏任务并将定时器的回调函数放入其中宏任务队列中.
  • 同步代码执行完,开始执行当前宏任务的微任务队列,直到微任务队列的所有任务都执行完.
  • 微任务队列的所有任务执行完毕,宏任务1再看没有其他代码了,当前的宏任务循环结束后,js线程开始执行下一个宏任务,直到所有宏任务执行完毕.如此整体便构成了事件循环(EventLoop)机制.
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
	new Promise(function(resolve) {
	    console.log('4.1');
	    resolve();
	}).then(function() {
	    console.log('5.1')
	}).then(res=>{
		console.log(5.11)
	})
	
    new Promise(function(resolve) {
        console.log('4.2');
        resolve();
    }).then(function() {
        console.log('5.2')
    }).then(res=>{
		console.log(5.22)
	})
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

// 1
// 7
// 6
// 8
// 2
// 4.1
// 4.2
// 3
// 5.1
// 5.2
// 5.11
// 5.22

# 注意在promise里嵌套promise情况

new Promise(resolve => {                  	// 1
  setTimeout(()=>{                        	// 2
      console.log(666);                   	// 3
      new Promise(resolve => {            	// 4
        resolve();                        	// 5      
      })                                  	// 6       
      .then(() => {console.log(777);})    	// 7
  })                                      	// 8       
  resolve();                              	// 9
 })                                       	// 10
 .then(() => {                            	// 11
	     new Promise(resolve => {         	// 12
	       resolve();                     	// 13
	     })                               	// 14
	     .then(() => {console.log(111);}) 	// 15
	     .then(() => {console.log(222);});	// 16
 })                                       	// 17
 .then(() => {                            	// 18
	     new Promise((resolve) => {       	// 19
	       resolve()                      	// 20
	     })                               	// 21
	    .then(() => {                     	// 22
		     new Promise((resolve) => {   	// 23
		       resolve()                  	// 24
		     })                           	// 25
		    .then(() => {console.log(444)}) // 26
	     })                                	// 27
	    .then(() => {                      	// 28
	       console.log(555);               	// 29
	    })                                 	// 30
})                                         	// 31
.then(() => {                              	// 32
  console.log(333);                        	// 33
})                                         	// 34
  • 线程执行第一行代码,同步执行Promise包裹的函数.
  • 在第二行发现定时器,启动一个宏任务,将定时器的回调放入宏任务队列等待,线程直接跳到第9行执行
  • 第9行执行完开始执行第11行代码发现then函数,放入当前微任务队列中.线程往后再没有可以执行的代码了,于是开始执行微任务队列.
  • 执行微任务队列进入第12行代码,运行到第15行代码时发现then函数放入微任务队列等待.随后线程直接跳到第18行,碰到then函数放到微队列中.后续没有可执行的代码了,再开始执行微任务队列的第一个任务也就是第15行代码输出111.
  • 15行执行完执行到16行碰到then回调放入微任务队列等待.随后线程跳到18行的微任务开始执行,一直执行到22行碰到then函数又放入微任务队列等待.此时线程继续往下跳到第32行碰到then函数放入微任务队列等待.后续没有可执行的代码了,再开始执行微任务队列的第一个任务.
  • 线程跳到第16行执行微任务输出 222,随后又跳到22行执行下一个微任务,在26行处碰到then函数放入微任务队列等待.线程继续执行下一个微任务跳到32行输出 333.至此这一轮的三个微任务全部执行完毕清空,又开始执行微任务队列的第一个任务,线程跳到第26行输出 444.
  • 线程执行到28行碰到then函数回调放入微任务队列等待.后续没有可执行的代码了,再开始执行微任务队列的第一个任务即29行代码输出 555.
  • 所有微任务执行完毕,当前宏任务结束.线程开始执行下一个宏任务,线程跳到第三行输出 666.
  • 线程继续往后第7行碰到then回调放入微任务队列,后续没有可执行的代码了,再开始执行微任务队列的第一个任务输出 777.第二个宏任务执行完毕.
  • 综上所述:输出分别为 111 -- 222 -- 333 -- 444 -- 555 -- 666 -- 777

return的情况下不会进入下一个then(最外层的promise),如果上一个then(最外层的promise)还没结束,里面的微任务会继续执行直到结束。

new Promise((resolve,reject)=>{
	 console.log(1)
	 resolve()
 }).then(res=>{
	 console.log(2)
	 new Promise((resolve,reject)=>{
		 console.log(3)
		 resolve()
	 }).then(res=>{
		 console.log(4)
	 }).then(res=>{
		 console.log(5)
	 }).then(res=>{
		 console.log(5.1)
	 }).then(res=>{
		 console.log(5.2)
	 })
	 .then(res=>{
	 		 console.log(5.3)
	 })
 }).then(res=>{
	 console.log(6)
	 return new Promise((resolve,reject)=>{
		 console.log(7)
		 resolve()
	 }).then(res=>{
		 console.log(8)
	 }).then(res=>{
		 console.log(8.1)
	 })
	 .then(res=>{
	 		 console.log(8.2)
	 })
 }).then(res=>{
	 console.log(9)
	 return new Promise((resolve,reject)=>{
	 		 console.log(10)
	 		 resolve()
	 })
 }).then(res=>{
	 console.log(11)
 })
 
 //1 2 3 4 6 7 5 8 5.1 8.1 5.2 8.2 5.3 9 10 11 √
//①
 new Promise((resolve,reject)=>{
	 console.log(1)
	 resolve()
 }).then(res=>{
	 console.log(2)
	 //②
	 new Promise((resolve,reject)=>{
		 console.log(3)
		 resolve()
	 }).then(res=>{
		 console.log(4)
	 }).then(res=>{
		 console.log(5)
	 }).then(res=>{
		 console.log(5.1)
	 }).then(res=>{
		 console.log(5.2)
	 })
	 .then(res=>{
	 		 console.log(5.3)
	 })
 }).then(res=>{
	 console.log(6)
	 //③
	 new Promise((resolve,reject)=>{
		 console.log(7)
		 resolve()
	 }).then(res=>{
		 console.log(8)
	 }).then(res=>{
		 console.log(8.1)
	 })
	 .then(res=>{
	 		 console.log(8.2)
	 })
 }).then(res=>{
	 console.log(9)
	 return new Promise((resolve,reject)=>{
	 		 console.log(10)
	 		 resolve()
	 })
 }).then(res=>{
	 console.log(11)
 })
 
 //1 2 3 4 6 7 5 8 9 10 5.1 8.1 5.2 8.2 5.3 11 √

# 同步、异步、回调先后顺序

同步 => 异步 => 回调

let a = new Promise(
  function(resolve, reject) {
    console.log(1)
    setTimeout(() => console.log(2), 5)
    console.log(3)
    console.log(4)
    resolve(true)
  }
)
a.then(v => {
  console.log(8)
})

let b = new Promise(
  function() {
    console.log(5)
    setTimeout(() => console.log(6), 0)
  }
)
//1 3 4 5  8 6 2
//1 3 4 5 同步 
//8异步 
//6 2 回调

宏任务按先后顺序,谁先到时间谁先把回调函数放入队列中。

setTimeout(()=>{
	console.log(1)
	setTimeout(()=>{
		console.log(2)
	})
})
setTimeout(()=>{
	console.log(3)
})
// 1 3 2

# Event Loop事件循环

在事件驱动机制里面,主线程都会运行一个事件循环(EventLoop),有时候也叫 消息循环(MessageLoop) 或者 运行循环(RunLoop)

事件驱动的机制,通常还有事件或者消息队列,里面放着各种要处理的事件或者消息,通过这个循环不断的处理这些事件或者消息。

所以事件循环的背后就是事件驱动,而事件驱动是一种程序设计模型。

EventLoop(事件循环机制) 就是执行代码的过程中,先遇到script,多个script也是当作合成一个script标签,就把它丢到宏任务队列中,然后拿出script这个宏任务出来执行,宏任务列表这时候就是空的了, 执行的过程当中遇到setTimeout的话,就把它丢到宏任务队列中,要是遇到promise的then就把它丢到微任务中,继续往下执行,直到script这个第一个宏任务执行完了,它就先去微任务队列看看,有任务的话,就会先执行完微任务队列的任务,执行完微任务队列的任务后,再去宏任务中拿任务来执行,一直这样循环下去,就算某一阶段是没宏任务也没有微任务的,它也是一直在监听任务的,等到其他线程给过来的宏任务,它又开始执行任务。

根据 JavaScript 运行环境的不同,Event Loop 也会被分成浏览器的 Event Loop 和 Node.js 中的 Event Loop。

# 浏览器的 Event Loop

在浏览器里,每当一个被监听的事件发生时,事件监听器绑定的相关任务就会被添加进回调队列。通过事件产生的任务是异步任务,常见的事件任务包括:

  • 用户交互事件产生的事件任务,比如输入操作;
  • 计时器产生的事件任务,比如setTimeout;
  • 异步请求产生的事件任务,比如 HTTP 请求。

# Node.js 中的 Event Loop

Node.js 中的事件循环执行过程为:

当 Node.js 启动时将初始化事件循环,处理提供的输入脚本;

提供的输入脚本可以进行异步 API 调用,然后开始处理事件循环;

在事件循环的每次运行之间,Node.js 会检查它是否正在等待任何异步 I/O 或计时器,如果没有,则将其干净地关闭。

与浏览器不一样,Node.js 中事件循环分成不同的阶段

   ┌───────────────────────────┐

┌─>│           timers          │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

│  │     pending callbacks     │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

│  │       idle, prepare       │

│  └─────────────┬─────────────┘      ┌───────────────┐

│  ┌─────────────┴─────────────┐      │   incoming:   │

│  │           poll            │<─────┤               |

│  └─────────────┬─────────────┘      │   data, etc.  │

│  ┌─────────────┴─────────────┐      └───────────────┘

│  │           check           │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

└──┤      close callbacks      │

   └───────────────────────────┘

由于事件循环阶段划分不一致,Node.js 和浏览器在对宏任务和微任务的处理上也不一样。

# 线程和进程

本质上来说,两个名词都是 CPU 工作时间片的一个描述。

进程是CPU资源分配的最小单位,进程包括运行中的程序和程序所使用到的内存和系统资源CPU可以有很多进程,电脑每打开一个软件就会产生一个或多个进程,为什么电脑运行的软件多就会卡,是因为CPU给每个进程分配资源空间,但是一个CPU一共就那么多资源,分出去越多,越卡,每个进程之间是相互独立的,CPU在运行一个进程时,其他的进程处于非运行状态,CPU使用 时间片轮转调度算法 来实现同时运行多个进程.

线程是CPU调度的最小单位,线程是建立在进程的基础上的一次程序运行单位,通俗点解释线程就是程序中的一个执行流,一个进程可以有多个线程

一个进程中只有一个执行流称作单线程,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行

一个进程中有多个执行流称作多线程,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

把这些概念拿到浏览器中来说,当打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

JS 引擎线程和渲染线程,在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。这其实也是一个单线程的好处,得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

一个程序运行,至少有一个进程,一个进程至少有一个线程。

打个比方,进程好比一个工厂,线程就是里面的工人,工厂内有多个工人,里面的工人可以共享里面的资源,多个工人可以一起协调工作,类似于多线程并发执行。

# 进程和线程的区别

进程是操作系统分配资源的最小单位,线程是程序执行的最小单位

一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线

进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)

调度和切换:线程上下文切换比进程上下文切换要快得多

# 多进程和多线程

多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰

多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

# JS为什么是单线程

JS的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

还有人说js还有Worker线程,对的,为了利用多核CPU的计算能力,H5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程是完全受主线程控制的,而且不得操作DOM,所以,这个标准并没有改变JavaScript是单线程的本质

# 浏览器多进程

浏览器是多进程的,打开 windows 任务管理器,可以看到浏览器开了很多个进程,每一个 tab 页都是单独的一个进程,所以一个页面崩溃以后并不会影响其他页面

浏览器包含下面几个进程:

  • Browser 进程:浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU 进程:最多一个,用于 3D 绘制等
  • 浏览器渲染进程(浏览器内核)(Renderer 进程,内部是多线程的):默认每个 Tab 页面一个进程,互不影响

为什么浏览器要多进程

假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差,同理如果插件崩溃了也会影响整个浏览器

当然多进程还有其它的诸多优势,不过多阐述,浏览器进程有很多,每个进程又有很多线程,都会占用内存,这也意味着内存等资源消耗会很大,有点拿空间换时间的意思

# 浏览器渲染进程

浏览器渲染进程是多线程的,也是一个前端人最关注的,它包括下面几个线程:

  • GUI 渲染线程
    • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
      1. 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
      2. 解析css,生成CSSOM(CSS规则树)
      3. 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
  • JS 引擎线程
    • 也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
    • JS 引擎线程负责解析 Javascript 脚本,运行代码。
    • JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序 同样注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程
    • 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助),用来控制事件循环,并且管理着一个事件队列(task queue)
    • 当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会走事件触发线程将对应的事件添加到对应的线程中 (比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
    • 注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)
  • 定时触发器线程
    • setInterval 与 setTimeout 所在线程
    • 浏览器定时计数器并不是由 JavaScript 引擎计数的, (因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)
    • 注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。
  • 异步 http 请求线程
    • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

js 引擎是单线程的,代码从上而下顺序的预期执行,虽然降低了编程成本,但也有其他问题,如果某个操作很耗时间,比如,某个计算操作 for 循环遍历 10000 万次,就会阻塞后面的代码造成页面卡顿... ...

JS 线程执行的时候,渲染线程会被挂起;渲染线程执行的时候,JS 线程会挂起,所以 JS 会阻塞页面加载,这也是为什么 JS 代码要放在 body标签之后,所有html内容之前;为了防止阻塞页面渲造成白屏。

JS 是单线程的,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker,是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Web Worker 有几个特点:

  1. 同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
  2. DOM 限制:不能操作 DOM
  3. 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
  4. 脚本限制:不能执行 alert()方法和 confirm()方法
  5. 文件限制:无法读取本地文件

# 同步任务和异步任务

  • 同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调
  • 一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的同步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行
let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}

// 同步任务
console.log('我是同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get('/info',httpCallback);

// 同步任务
console.log('我是同步任务2');

JS是按照顺序从上往下依次执行的,可以先理解为这段代码时的执行环境就是主线程,也就是也就是当前执行栈

  • 首先,执行console.log('我是同步任务1')
  • 接着,执行到setTimeout时,会移交给定时器线程,通知定时器线程 1s 后将 setTimeoutCallBack 这个回调交给事件触发线程处理,在 1s 后事件触发线程会收到 setTimeoutCallBack 这个回调并把它加入到事件触发线程所管理的事件队列中等待执行
  • 接着,执行http请求,会移交给异步http请求线程发送网络请求,请求成功后将 httpCallback 这个回调交由事件触发线程处理,事件触发线程收到 httpCallback 这个回调后把它加入到事件触发线程所管理的事件队列中等待执行
  • 再接着执行console.log('我是同步任务2')
  • 至此主线程执行栈中执行完毕,JS引擎线程已经空闲,开始向事件触发线程发起询问,询问事件触发线程的事件队列中是否有需要执行的回调函数,如果有将事件队列中的回调事件加入执行栈中,开始执行回调,如果事件队列中没有回调,JS引擎线程会一直发起询问,直到有为止

浏览器上的所有线程的工作都很单一且独立,非常符合单一原则

定时触发线程只管理定时器且只关注定时不关心结果,定时结束就把回调扔给事件触发线程

异步http请求线程只管理http请求同样不关心结果,请求结束把回调扔给事件触发线程

事件触发线程只关心异步回调入事件队列

而JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这就是所谓的事件循环(Event Loop)

首先,执行栈开始顺序执行

判断是否为同步,异步则进入异步线程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行

执行栈空,询问任务队列中是否有事件回调

任务队列中有事件回调则把回调加入执行栈末尾继续从第一步开始执行

任务队列中没有事件回调则不停发起询问

# 执行栈

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。

平时在开发中,也可以在报错中找到执行栈的痕迹

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()
VM1004:2 Uncaught Error: error
    at foo (<anonymous>:2:9)
    at bar (<anonymous>:5:3)
    at <anonymous>:7:1

可以在上图清晰的看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。

当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

function bar() {
  bar()
}
bar()
VM1466:1 Uncaught RangeError: Maximum call stack size exceeded
    at bar (<anonymous>:1:13)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
    at bar (<anonymous>:2:3)
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("setTimeout")
},0)
async1()

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

console.log(`script end`)


//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeout

async1 end和promise2的先后可能随不同版本的v8引擎执行的不一致

# NodeJS中的运行机制中的宏任务与微任务

虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,但是,还是有一些与浏览器中的表现是不一样的

其实nodejs与浏览器的区别,就是nodejs的宏任务分好几种类型,而这好几种又有不同的任务队列,而不同的任务队列又有顺序区别,而微任务是穿插在每一种宏任务之间的

在node环境下,process.nextTick的优先级高于Promise,可以简单理解为在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分

node11以后的事件循环,执行结果与浏览器是一样

NodeJS的Event Loop相对比较麻烦

Node会先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(NextTick例外)

进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask

再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask

再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask

至此,完成一个 Tick,回到 timers 阶段

……

如此反复,无穷无尽……

# 写在最后

(1)js的异步

从最开头就说js是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

(2)事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

(3)javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

(4)setImmediate

微任务和宏任务还有很多种类,比如setImmediate等等。

(5)最后的最后

  • javascript是一门单线程语言
  • Event Loop是javascript的执行机制

原文 (opens new window) 宏任务微任务演示网站 (opens new window) 宏任务微任务 (opens new window) promise (opens new window) 参考 (opens new window)

最后更新: 4/17/2024, 7:46:39 AM