JavaScript 执行机制

浏览量:294

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

宏任务和微任务的执行过程如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前所有宏任务和微任务执行完毕,开始渲染
  • 渲染完毕后,开始下一个宏任务

留下评论