likes
comments
collection
share

人人都能看懂的JavaScript单线程的那点事

作者站长头像
站长
· 阅读数 12

如果你已经对 Javascript 很熟悉了,那么相信你一定知道它是一门“单线程”语言。

那你知道单线程意味着什么吗?

Javascript 在 V8 引擎上运行,V8 引擎具有内存堆和调用堆栈的特性。

JavaScript 是单线程的,意味着每次只执行一条语句。

在深入探讨在单线程上运行意味着什么之前,我想先解释一下基本的术语。

你首先需要了解一种叫做栈(stack) 的数据结构,它具有后进先出的特性。

同步 (sync) 执行通常是指代码按顺序执行。在同步编程中,程序会逐行执行,一次一行。每次调用函数时,程序执行都会等待,直到这个函数返回(return),然后再继续执行下一行代码。

举个例子,你正在给某人打电话,你需要等他接听之后可以和他们说话。在对方接通电话之前,你不会做任何其他事情。

这应该很好理解。

我们来下看下面的例子:

const one() => {
  const two() => {
    console.log('4');
  }   
  two(); 
} 
one();

在这段代码中,函数调用栈会发生什么?

它会像下面这样: 人人都能看懂的JavaScript单线程的那点事

函数调用栈的工作是填充指令并在指令执行时弹出指令。

Javascript 虽然是一种单线程语言,但也可以是非阻塞的

单线程是指它只有一个函数调用栈,并且这个函数调用栈永远都会先运行栈顶的函数。

在上面的代码中,函数是按顺序运行的。

如果我们有一个功能需要做繁重的工作怎么办?我们应该让用户一直等到这个过程结束吗?

再来看另一个例子:

const one() {       
  console.log("Hello"); 
} 
const two () {     
  for(i=0; i<= 100000000000000000000000; i++){ } 
}
const three(){        
  console.log("World"); 
}
one(); 
two(); 
three(); 

在这个例子中,如果我们的 two 函数必须循环遍历很久的时间,这是否意味着 three 函数必须等到 two() 被执行?从技术上讲,是的!

在我们的例子中,它可能没什么意义,但如果我们必须在实际项目中实现类似的逻辑时,那么在这个过程完成之前用户可能无法做任何事情。

异步 (async) 执行是指不按照它在代码中出现的顺序来实际运行。在异步编程中,程序不会等待当前任务完成,而是可以继续执行下一个任务。

举个例子:你打电话给某人,在等待他接电话的同时,你也可以做点别的事情。

不同的语言有不同的方式来实现异步,最流行的是通过多线程。

Java 就是通过创建一个子线程来实现多线程,这个子线程拥有自己的函数调用栈,可以单独执行,然后再和父线程合并。

但是,这可能会遇到死锁问题,可以通过各种死锁预防机制来处理。

具体的内容不在本文讨论范围内。

我们关心的是在 Javascript 中实现异步,我们来看看它是如何做到的。

运行下面的代码,看看会发生什么。

console.log('1');
setTimeout(()=> {
  console.log('2')
}, 3000);
console.log('3');

你会先看到 1 和 3,短暂延迟后,才可以看到 2。为什么会这样?

简而言之,Javascript 中的异步实现是通过函数调用栈、回调队列和 Web API 以及事件循环来完成的。

上面我有讲到,函数调用栈的工作是检查栈顶部的指令并执行它。如果有像 setTimeout() 这样的异步任务需要额外的时间来执行,那么函数调用栈会将它弹出并把它发送到 Web API。

事件循环的工作是不断检查是否发生了某种事件,比如鼠标单击或键盘敲击,然后把它发送给函数调用栈。当然,用户的鼠标单击会比图像加载这种任务具有更高的执行优先级。

在 Javascript 中,所有指令都放在函数调用栈上。当函数调用栈碰到 setTimeout 时,引擎会把它视为 Web API 指令并把它弹出,并将其发送到 Web API。一旦 Web API 完成执行,它会重新被达回调队列。

引擎检查调用堆栈是否为空。如果它是空的,那么我们检查回调队列中包含指令 setTimeout。回调队列把它发送到函数调用栈并执行指令。

具体的流程可以参照下图:

人人都能看懂的JavaScript单线程的那点事

我们再来看另一种情况,当你发送了一个 API 请求时。例如,你的网站需要从服务器获取一张图像。在图像返回之前如果网页不能加载其他内容,那将是一个糟糕的用户体验。

当函数调用栈发现它需要获取图像时,它会把这个函数弹出,并把它发送到 Web API 并继续执行剩下的函数。对图像请求的回调事件会存储在回调队列中。

当函数调用栈为空时,持续运行的事件循环会查看回调队列中是否有任务。

执行这个过程时,JavaScript 不用考虑程序是运行在具有多少个核心的 CPU 上。所以 V8 实现 JavaScript 异步只需要一个调用堆栈就够了。