Javascript的执行机制 —— 第 1 篇

2023/7/27

# 单线程的JS

JS最大的特点就是单线程,这无须再多说了,我们要知道的是,为什么JS要设计成单线程的,多线程不是更符合现实场景么?

因为这跟JS的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这就决定了JS只能是单线程,否则会代理很复杂的同步问题

想象一个场景,在一个线程里,用户点击了一个button按钮,按钮事件正在触发的时候,另一个线程删除了这个button,那这时,浏览器到底要以哪个操作为主呢?

所以,为了避免这种复杂性,从一诞生,JS就是单线程,这已经成了这门语言的核心特征,永远也不会改变

为了利用多核CPU的计算能力,ES6提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

# JS的特色 —— 任务队列

也可以叫做消息队列(event queue)

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务还没结束,后一个任务就得一直等待...

这种一直等待的现象会造成时间资源的浪费,所以JS的设计者提出了一种思想,可以先挂起处于等待中的任务,先执行后面的任务,等待的任务返回来结果后,再回过头,把等待的任务继续执行下去(这种思想的实现就是借助了任务队列)

任务分成两类👇

  • 同步任务 (synchronous) —— 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务(asynchronous) —— 不立即进入主线程、而进入任务队列 (task queue)的任务,只有当任务队列通知主线程,某个异步任务可以执行了,该异步任务才会进入主线程执行

简单来描述JS异步执行机制就是

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack
  2. 如果存在异步任务,只要当异步任务有了运行结果,就在"任务队列"之中放置一个事件,开始等待执行栈调用
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取"任务队列",任务队列里对应的异步任务,就会结束等待状态,进入执行栈,开始执行
  4. 上面的过程会不断重复循环 —— event loop

任务队列是先进先出的,只要执行栈一清空,任务队列上第一位的事件就自动进入主线程

但是,由于JS中存在定时器,所以主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程

# 回调函数

任务队列中的事件,除了IO设备的事件以外,还包括一些用户操作的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数 ! ! !

甚至你可以想象成,异步任务是把他们的回调放到了任务队列里

# 定时器

定时器也是JS一个很有特色的东西,它也是会被放到任务队列中的

定时器功能主要由setTimeout()setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行

看一个例子就行👇

console.log(1);
setTimeout(() => {
  console.log(4);
}, 2000);
setTimeout(() => {
  console.log(2);
}, 1000);
console.log(3);

上面这行代码的执行结果是1 3 2 4

console.log()会直接放到执行栈中运行(它也是JS最优先的函数),所以会先顺序输出1 3,然后是第一个定时器,两秒后执行,第二个定时器,一秒后执行,,此时要注意,虽然第二个定时器放在了第一个定时器后面,但是它的定时时间到了之后,就会放入任务队列,此时执行栈为空,所以输出2,第一个定时器的执行时间还没到,所以它现在还没有放在任务队列里,直到它的执行时间到了,才会放入任务队列中,等待调用 —— 异步任务有了返回结果之后,才会放入消息队列中,等待调用

ES6规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加

同时,对于那些DOM的操作,涉及到重绘的,通常也不会立即执行,是每16毫秒执行一次 —— 这时使用requestAnimationFrame()的效果要好于setTimeout()

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完(空闲时),主线程才会去执行它指定的回调函数

所谓空闲的时候是指的是当前引擎执行的语句块上下文执行完毕时的正要执行下一个语句块时的状态

要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行

let i = 1;
setTimeout(() => {
  console.log(1);
}, 1000);
while (i) {
  i++;
}
setTimeout(() => {
  console.log(2);
}, 1000);

比如上面☝️这个例子,如果执行栈里面有一个死循环,那此时还在任务队列里的内容是永远不会执行的 —— 两个定时器都永远不会执行了

在这里可能会有一个疑问?

既然JS是单线程的,那么是谁来监督定时器的执行时间呢?

是浏览器,JS是单线程的,但浏览器是多线程的,当浏览器监控到"事件池"状态更新时会通知改变JS引擎 这时候JS引擎会在空闲的时候停下来去执行"事件池"里面的回调

关于JS执行机制的初始介绍到这里就差不多了,此时你应该理解了一些JS的代码是怎么运行的,那关于JS执行机制里最重要的部分 ——event loop ,请点这里😄

How to love
Lil Wayne