JavaScript是一门单线程语言
我相信绝大部分前端度知道,JavaScript是一种单线程语言。但是,什么是单线程多线程(what)?单线程是怎么运作的(how)?以及JavaScript为什么要设计成单线程(why)?先来解答这3个问题
what(什么是单线程)
先来了解下进程与线程。
- 进程
我们可以理解一个程序开始执行后,就是一个进程
- 线程
线程是程序执行的执行流,每个线程都有自己的执行栈,指针,存储空间。不同的线程可以共享同一块代码。
- 单线程
程序执行的时候,都是按照编写时候设定好的执行顺序,按照由前到后的顺序,一路往下执行。前面的执行完了,才会开始执行后面的。
how(单线程如何执行)
看一下非常简单的一段代码
console.log(1)
console.log(2)
cobsole.log(3)
复制代码
在单线程语言中,以上代码按照设定好的顺序,由前到后依次执行,没法同时执行console.log(1)和console.log(2)。输出结果:
1;
2;
3;
复制代码
why(JS为什么设计成了单线程)
单线程执行的时候是能够提高效率的,而且作为浏览器的脚本语言,它当初被设计来的作用就是用来操作dom,进行用户交互,如果js是多线程的话,可能存在的问题就是多个线程同时操作一个dom,多个线程结果各不相同时无法决定最终页面上如何显示。
什么是执行上下文
js程序在运行的时候,会为这段准备执行的代码准备一个执行的环境,包括变量对象,作用域链、this、参数等,这些环境称为它的执行上下文。全局上下文也细分为全局上下文、函数上下文、局部上下文。写到这里,总感觉任何一个词都能写出一篇文章来,网上也确实有好多专门解析作用域,this,上下文的文章。所以前端的知识体系确实庞大,做前端好累,头发好伤…
什么是堆栈
堆与栈的概念在js中是用来存储的,栈内存中存储基本数据类型如string,number,init等,堆存储引用数据类型如对象,数组等。但是栈中存放着引用类型的地址,通过这个地址可以获取到堆内存中真正的对象。需要注意的是,这里的栈与js运行时的执行栈还不是同一个概念。
JS的执行栈
js代码在执行的时候,需要一个顺序,当我们打开浏览器时,浏览器解析器需要一种机制来追踪函数的执行,这样当执行环境中调用多个函数时,通过这种机制可以追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。
- 每调用一个函数,就会将该函数推入到执行栈
- 如果正在调用的函数A调用了其他函数B,那么,函数B也会被推入到执行栈,函数B一旦被调用会马上被执行,也就是执行栈后进先出的逻辑
- 当函数B调用完成后,解析器会将它清除出执行栈,继续往下执行函数A运行环境中其他的代码
上代码:
function a() {
console.log('a函数进入执行栈开始执行')
b()
console.log('b函数被清除出执行栈,继续执行a函数的代码')
}
function b(){
console.log('b函数进入执行栈开始执行')
}
a()
复制代码
上面代码执行的逻辑:
-
执行栈忽略function a,function b的定义,只是将他们作为执行上下文;
-
代码执行a()函数的执行,a被推入到执行栈调用;
-
执行 console.log(‘a函数进入执行栈开始执行’)这一句
-
遇到了b()函数,于是b函数被推入到执行栈调用,进入b函数中的代码逻辑
-
console.log(‘b函数进入执行栈开始执行’)
-
b函数执行完,b函数被清除出执行栈,又回到执行栈中的a函数继续执行
-
console.log(‘b函数被清除出执行栈,继续执行a函数的代码’)
-
a函数也执行完毕,被清除出执行栈
以上就是一个非常简单的js的执行栈调用过程。你可以将执行栈想象成一个羽毛球筒,遇到函数a的时候,就往球筒里面放一个羽毛球a,如果a函数中有调用了b函数,那么,b函数作为一个新的球也放进球筒里边,这个时候,只有等待b函数执行完毕被清除出执行栈(羽毛球b从球筒中拿出来后)才能继续执行a函数(拿出羽毛球a),这就是js执行栈后进先出的逻辑。
任务队列
上面在讲解执行栈的时候,逻辑非常简单,没有遇到任何异步的请求,如果遇到异步请求的时候,js解析器如何执行呢?稍微改进一下上面的代码:
function a(){
console.log('a函数进入执行栈开始执行')
b()
setTimeout(() => {
consolelog('异步任务')
},0)
console.log('b函数被清除出执行栈,继续执行a函数的代码')
}
function b(){
console.log('b函数进入执行栈开始执行')
}
a()
复制代码
上面代码中,在第4行增加了一个异步函数setTimeout,这个时候代码如何执行呢?都知道JavaScript是单线程的,如果遇到异步任务,不可能让线程一直等待异步返回结果,如果setTimeout方法的第二个参数设置大一点,setTimout(() => {}, 10000),那程序一直在等待挂起中被阻塞,玩不下去了。所以,为了处理异步任务,必须换一种玩法。
这个时候,浏览器的事件机制就上场了。
首先提一下什么是异步任务,异步任务是指不会进入主线程,而是进入任务队列的任务。异步任务在回调完成后,会在队列中排队等待主线程有空时的召唤,才进入主线程得以执行。
单线程的JavaScript是怎么生成这样一种事件机制的呢?其实,这个跟js关系不大,主要是依赖于浏览器。浏览器是js的运行时环境,它是多线程的。包括:
- 主线程按顺序执行同步任务,并形成执行栈
- 遇到异步任务时,将它扔给定时器触发线程去处理,异步任务处理完成之后,不会马上回到主线程的执行栈,而是进入任务队列进行等待
- 主线程的执行栈空闲时,会去查看一下任务队列是否有任务在等待,如果有,就把队列中的所有任务按照先进先出的顺序取出到主线程执行
- 返回继续执行同步任务
- 对以上步骤进行循环
浏览器的这种机制称为浏览器的EventLoop,nodejs也有一套自己的事件循环机制,还不太相同,这个以后再看。
宏任务、微任务
那么,异步任务有这么多,平时我们看到的有setTimeout,promise异步请求,setInterval等,它们之间又要怎么分清楚个先后呢?先来看一段代码:
setTimeout(() => {
console.log('宏任务1')
}, 0)
console.log('主线程1')
new Promise((resolve, reject) => {
console.log('我不是微任务1')
resolve('promise resolved')
}).then(res => {
console.log(res)
})
console.log('主线程2')
复制代码
先说过程,再说答案,说过程前先来看下哪些是宏任务,哪些是微任务
- 主线程开启,开始执行,先遇到setTimeout,分配给宏任务处理,往下执行;
- 遇到console.log(‘主线程’),直接执行,输出结果:‘主线程’,往下;
- 遇到new Promise是微任务,promise直接执行,输出结果:‘我不是微任务1’。注意,promise的then才是微任务,作为主线程中的微任务,分配给主线程的微任务,等待执行;
- 往下执行console.log(‘主线程2’),输出结果:‘主线程2’。到这里主线程中的宏任务已经执行完毕了,开始执行微任务队列,我们之前将promise的then分配给了MicroTask队列,因此第五步开始执行主线程中的MicroTast队列;
- 执行promise的then中的代码;输出res结果:‘promise resolved’。到这里主线程中的微任务队列清空,接下来开始进入宏任务队列MocroTask;
- 执行宏任务队列中的setTimeout中的代码,输出结果:‘宏任务1’。
- 宏任务队列清空,交回给主线程。
- 循环以上过程。
根据上面的步骤,我们的输出结果是:
主线程 => 主线程2 => 我不是微任务1 => 主线程2 => promise resolved => 宏任务1