JavaScript是一门单线程语言,具体该怎么理解呢?
我们先看看JavaScript是如何在浏览器中执行的。
既然JavaScript是一门单线程语言,那么它在执行的时候,肯定是逐行执行代码,也就是说,JavaScript中的一切都发生在执行上下文中。
我们可以把JavaScript的执行上下文想象成一个大容器,容器内分两个部分,一个用来存储数据,一个用来执行代码。
存储数据的部分,我们把它叫做"内存组件"。
执行代码的部分,我们把它叫做"代码组件"或者“执行线程”。
举个简单的例子:
var a = 2;
var b = 4;
var sum = a + b;
console.log(sum);
在这个简单的例子中,我们初始化了两个变量a和b,并分别存储2和4。
然后我们将a和b的值相加并存储在sum变量中。
那么JavaScript将在浏览器中如何执行这段代码呢?请继续看:
浏览器创建了一个有两个组件的全局执行上下文,这两个组件分别是内存组件和代码组件。
浏览器将分两阶段执行这段JavaScript代码:
1> 在内存创建阶段,JavaScript会扫描所有代码,为代码中的所有变量和函数分配内存。
2> 在代码执行阶段,JavaScript将在内存创建阶段分配的变量定义为undefined,对于函数,则保留整个函数代码。
现在,在第二阶段,即代码执行阶段,开始逐行遍历整个代码。
当遇到var a = 2时,2会分配给内存中的a。所以之前,a的值是未定义的。
同理b变量也如此,4分配给b。然后计算sum的值并存储在内存中,即6。最后一步,在控制台中打印sum值,然后在完成代码时销毁全局执行上下文。
上面我们只是看了一个很简单的例子,但是实际应用中,业务逻辑是很复杂的,那么对于复杂的代码,JavaScript是如何在“容器”内执行的呢?
调用堆栈
我们通过一个没有实际意义的代码,来理解这个问题。
function a() {
function insideA() {
return true;
}
insideA();
}
a();
我们创建了一个函数a,它调用另一个返回true的函数insideA。
JavaScript创建了一个全局执行上下文。
全局执行上下文入栈。
全局执行上下文将在代码执行阶段为函数a分配内存并调用函数a。
a函数再入栈。
a函数中调用函数insideA。
insideA函数再入栈。
执行insideA,返回true。
根据堆栈空间,先入后出的原则。insideA先出栈,a再出栈,最后全局上下文全部执行完毕,销毁整个执行上下文。
通过上面的两个例子,我们可以更加清楚地认识到,“JavaScript中的一切都发生在执行上下文中。”
JavaScript 同步和异步
JavaScript是单线程语言,但是对于实际业务场景中,有些代码的执行并不是立刻就有返回结果的,比如,请求api数据,文件IO,延时任务等等。如果JavaScript都按照逐行执行的机制去执行,那么会很影响代码的执行效率和性能。
那么面对请求api数据,文件IO,延时任务等等这些任务时,我们改怎么处理,又不影响性能呢?
在这里我们需要了解两个名词,“同步”和“异步”。
在JavaScript中,“同步”并不是所有代码一起执行,而是代码逐行执行。“异步”并不是完全逐行执行代码,对于某些特定的操作,比如请求api数据,文件IO,延时任务等等这些任务,则先挂起,等到后面的同步代码执行完后,再回头执行异步。
举个例子来理解异步:
console.log('1');
setTimeout(function () {
console.log('2');
},1000)
console.log('3');
/* 打印出 1 3 2 */
如果上面的代码完全按照逐行执行,那么打印的顺序应该是 1 2 3。
但是实际上输出的顺序是1 3 2。
为什么3会先于2输出呢?这就是异步的作用,因为2是在异步方法setTimeout中的,所以console.log(‘3’)优先执行。
JavaScript Event Loop
我们先通过一张图来了解javascript的整个运行机制:
通过上图,我们可以看到,javascript实际上是分为两条任务线的。
第一条主线程,在这里执行的都是同步任务,也就是逐行执行代码。
遇到需要异步执行的代码时,就交给第二条任务线“异步任务”线程去执行。
完整过程入下:
所有同步任务都在主线程上执行,形成一个执行栈,当执行栈遇到异步任务时(请求api数据,文件IO,延时任务等等),不会等待,而是继续执行往下执行。异步任务会交给另一个任务线程去执行,执行后返回的结果,会返回到任务队列中。一旦主线程中的所有同步任务执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行(任务队列中,按照先入先出的原则进入主线程)。同样的,等从任务队列中取到任务被主线程执行完,主线程清空后,会再去任务队列内取下一个任务,而这种循环的机制,就称之为事件循环(Event Loop)。
JavaScript 宏任务和微任务
我们再看一个例子:
console.log(1)
setTimeout(function(){
console.log(2)
},0)
new Promise(function(resolve){
resolve()
}).then(function(){
console.log(3)
})
console.log(4)
/* 打印出 1 4 3 2 */
按照之前的逻辑,输出顺序应该1,4,2,3。 但是当我们实际运行的时候,发现返回结果是 1,4,3,2。这是为什么呢?因为任务队列中的任务,也是有优先级的。
这时我们就需要引入了解两个新概念–宏任务和微任务。
宏任务:
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
微任务:可以理解是在当前任务执行结束后立即执行的任务。
如果你不能马上理解,没关系,我们给出一些常见的宏任务和微任务实例。
宏任务包含:
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
微任务包含:
Promise.then
process.nextTick
宏任务和微任务的执行过程如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前所有宏任务和微任务执行完毕,开始渲染
- 渲染完毕后,开始下一个宏任务