js的单线程(为什么是单线程)
JavaScript语言的最大特点就是单线程,同一时间只能做一件事。
1.js的单线程与它的用途有关,作为浏览器脚本语言,js的主要用途就是与用户互动。(如果js有两个线程,一个线程操作dom,另一个线程也操作此dom,浏览器无法识别到底以哪个为准)
2.即使html5提出了Web Worker,允许js脚本创建多个线程,但是子线程完全受控于主线程,且不得操作dom,所以js单线程的本质并未改变。
任务队列
1.单线程就意味着所有任务都得排队,前一个结束,后一个才能执行。
2.排队是因为计算量大,CPU忙不过来,但是很多时候CPU是闲着的,因为IO(输入输出设备)很慢,比如ajax请求。因此JavaScript语言设计者意识到,主线程可以先不管IO设备,将等待任务挂起,先执行后面无需等待的任务。
3.因此出现两种任务,同步任务(在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务)和异步任务(不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行)。
具体的执行机制
1.所有同步任务都在主线程上执行,形成执行栈。
2.主线程之外存在一个任务队列(task queue),只要异步任务有了结果,就在“任务队列”中放置一个事件。
3.一旦执行栈中同步任务执行完毕,系统会读取“任务队列”,看看有哪些事件,那些对应的异步任务,结束等待状态进入执行栈,开始执行。
4.主线程不断重复以上三个步骤
(以上4条执行机制摘自阮一峰老师博客)
- 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入Event Table并注册函数
- 指定的事情完成后,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕后,会去Event Queue读取对应的函数,进入主线程执行
- 上述过程不断重复,也就是常说的Event Loop(事件循环)
举个栗子
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
具体执行步骤
- ajax进入Event Table,注册回调函数success
- 执行console.log('代码执行结束');
- ajax事件完成,回调函数进入Event Queue
- 主线程从Event Queue读取回调函数success并执行。
一些重要的、特殊的事件
1.setTimeout
普遍认知:异步,延迟执行
但是,实际操作中,很多时候延迟的时间往往比我们实际写的时间要多
举栗子
第一部分
for(var i=0;i<10000;i++){
console.log('我是for循环,需要循环一万次')
}
setTimeout(() => {
console.log('延时2秒');
},2000)
// 大家猜,我们两秒后会看到‘延时2秒’的字段吗?
第二部分
setTimeout(() => {
task()
},3000)
sleep(10000000)
// 3秒后能否执行task函数呢?
给大家梳理下过程(以上两部分为分开的独立代码):
第一部分
- 执行for循环,时光慢慢~需要很久(具体多久,不一定,每次试验时间都不一样,快的时候大约一秒多,慢的时候大概3秒。)
- 大概一两秒多过去了,setTimeout内的console.log('延时2秒')进入Event Table并注册,计时开始
- 2秒到了,计时事件完成,console.log('延时2秒')进入Event Queue
- 由于主线程已无任务,console.log('延时2秒')从Event Queue进入主线程执行 (因此,两秒后是看不到延时2秒字段的。)
第二部分
- task()进入Event Table并注册,计时开始
- 执行sleep()函数,计时仍在继续......
- 3秒到了,计时事件timeout完成,task()进入Event Queue,但是slepp还在执行,只能暂且等着
- sleep不知道经历了多久执行完了,task()终于从Event Queue进入了主线程执行
我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。
2.setInterval
- 与setTimeout类似,不同的是setInterval是每隔指定时间将注册的函数置入到Event Queue,如果主线程任务耗时过多,同样是需要等待的
- 需要注意的是,对于setInterval(fn,ms)来说,是每隔指定时间将注册的函数置入到Event Queue。一旦setInterval的回调函数fn执行时间超过了ms,就看不出来有时间间隔了。。。
3.Promise与process.nextTick(callback)
- Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象,具体参考阮一峰老师详细讲解
- process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
除了同步和异步,你不得不知的-->对任务有更精确的定义
1.macro-task(宏任务):包括整体代码script,setTimeout,setInterval
2.micro-task(微任务):Promise,process.nextTick
不同的任务类型会进入不同的任务队列。
事件循环的顺序决定js代码执行的顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。
举栗
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段js代码为宏任务
- 首先遇到setTimeout,将其回调函数注册后,发送到宏任务的Event Queue。
- 接下来遇到了new Promise,立即执行,then函数分发到微任务的Event Queue。
- 遇到console.log('console'),立即执行。
- 第一个宏任务结束,接下来看看有哪些微任务没有执行,我们发现then函数在微任务中,立即执行
- 第一轮的事件循环到此结束,接下来开始第二轮,从宏任务开始。发现setTimeout对应的回调函数,执行。
- 结束
个人理解(如有错误,劳烦指正)
- 一段代码就是一个宏任务,会有同步和异步代码。
1.同步的进入主线程,逐一执行
2.异步的进入Event Table并注册函数,指定的事情完成后,Event Table将这个函数移入宏任务的Event Queue。简单来说就是异步代码被分发到了宏任务的任务队列中。
3.这里需要注意的是promise的then函数会被分发到微任务的任务队列中,process.nextTick也是。
4. 当主线程任务执行完后,去执行微任务队列中的代码 5. 微任务代码执行完毕,第一轮事件循环也就结束了,接下来去到第二步中,异步代码被分发到了宏任务的任务队列。现在开始执行这个里面的宏任务,以此往复。
console.log('1');
setTimeout(function() { //=> 记为task1
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() { //=> 记为wei1
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() { //=> 记为wei2
console.log('8')
})
setTimeout(function() { //=> 记为task2
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
-
第一轮事件循环
1.console.log('1')为此轮宏任务的同步代码,直接执行。
2.setTimeout(task1),将函数注册完毕后,分发到宏任务的任务队列
3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列(wei1)
4.new Promise函数 console.log('7')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列(wei2)
5.setTimeout(task2),将函数注册完毕后,分发到宏任务的任务队列
6.主线程任务执行完毕,去到微任务队列。发现wei1和wei2两个微任务,相继执行 console.log('6')和console.log('8')
第一轮事件循环的结果:1,7 , 6 , 8 -
第二轮事件循环
1.先来看看第一个宏任务task1
2.console.log('2'),同步,直接执行。
3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列,即console.log('3');
4.new Promise函数 console.log('4')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列,即console.log('5')
5.主线程任务执行完毕,去到微任务队列。发现console.log('3')和console.log('5')两个微任务,相继执行
第二轮事件循环的结果:2,4 , 3 , 5 -
第三轮事件循环
1.再来看看第二个宏任务task2
2.console.log('9'),同步,直接执行。
3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列,即console.log('10');
4.new Promise函数 console.log('11')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列,即console.log('12')
5.主线程任务执行完毕,去到微任务队列。发现console.log('10')和console.log('12')两个微任务,相继执行
第三轮事件循环的结果:9,11 , 10 , 12
汇总结果:1,7,6,8,2,4,3,5,9,11,10,12(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
javascript是一门单线程语言
Event Loop(事件循环)是javascript的执行机制
文章参考:
1.阮一峰
2.ssssyoki
3.StarryLake
先总结到这里,如有错误,欢迎指正,互相学习~