浅谈JS运行机制

315 阅读9分钟

本文仅是我个人的见解,如理解有误,还望帮忙及时指出,方便及时更正。

正文开始~

进程与线程

这里先贴上阮大神的文章:进程与线程的一个简单解释

我是这样理解的:

  • 一个进程就好比工厂的一个车间,一个线程就好比车间里的一个工人,对应一个进程由一个或多个线程组成
  • 一个车间有它的独立资源,对应系统分配的独立内存
  • 每一个车间是相互独立的,对应每个进程之间是相互独立的
  • 每个车间有一或多个工人协同完成任务,对应多个线程在进程中协同完成任务
  • 每个车间的空间是工人们共享的,对应一个进程的内存空间是每个线程都可以共享
  • 车间里的房间大小不一,能容纳的工人数也不一样,当别的工人占用该房间时,其他人就不能使用。对应当一个线程使用某些内存时,其他线程必须等它结束,才能使用这块内存
  • 为了防止多个线程之间产生冲突,就有了很多协调机制,这里就不赘述了

最后,再用官方的话解释下:

进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)

线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

浏览器是多进程的

稍微深入了解了进程与线程后,就得对我们js最初运行的环境——浏览器——有点新的认识了。

  • 浏览器之所以可以运行,是因为操作系统给它分配了CPU和内存
  • 浏览器是多进程的
  • 一般来说,一个标签页就是一个独立的浏览器进程

上张图:

从上图我们可以看出,浏览器是多进程的。 另外,由于浏览器的优化,有些进程会合并,所以一个便签页对应一个进程并不是绝对的。

浏览器多进程有很多好处,比如当我们打开很多个网页,就相当于打开了多个进程,其中一个网页的卡顿不会对别的网页造成影响,让用户的体验更佳。

浏览器内核(渲染进程)

终于到了重点!前面讲了那么多进程,然而对于前端开发工作人员,最重要的就是渲染进程

js的执行,页面的渲染等操作都在这个进程中进行,而它是多线程的

一个浏览器内核通常包括以下线程:

  • GUI 渲染线程
    • 负责页面的渲染,包括重绘
    • 它与JS引擎线程是互斥的,当JS引擎执行时GUI渲染线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中直到JS引擎空闲时立即被执行
  • JavaScript引擎线程
    • 负责处理js程序,运行js代码
    • 同样,它与GUI渲染引擎也是互斥的,所以当js代码执行时间过长时,就会造成页面阻塞
  • 定时触发器线程
    • setTimeout和setInterval所在的线程
    • 浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此通过单独线程来计时并触发定时是更为合理的方案
  • 事件触发线程
    • 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理
  • 异步http请求线程
    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理

执行栈与任务队列

事件循环

javascript是单线程的,这是由于它的诞生就是浏览器脚本语言,就是为了与用户交互以及操作DOM。如果它是多线程的话,当多个线程同时操作DOM时,浏览器应该以哪个线程为准呢?所以,它生来就是单线程。

当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象;而栈中则存放着一些基础类型变量以及对象的索引。我们这里说的执行栈和上面这个栈的意义却有些不同。我们知道,当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),也叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

单线程就意味着在同一时间只有一个任务能被执行,所有的任务需要排队,只有前一个任务执行完毕后面的任务才能被执行。如果因为计算量比较大,CPU忙不过来,也还能忍受;可是大部分情况下CPU是空闲的,比如ajax读取数据很慢,必须等结果出来才继续往下执行。这样性能就很低,于是便有了同步任务和异步任务。同步任务是指在主线程上排队的任务,当前一个任务执行完毕就会执行后一个任务;异步任务是指不进入主线程,而是进入“任务队列”,只有“任务队列”通知主线程某个异步任务可以执行了,它才会进入主线程。

可总结如下:

  • 所有同步任务都在主线程上执行,形成执行栈
  • 所有异步任务都在“任务队列”上,只要异步任务有了结果,就在异步任务中添加一个事件
  • 当执行栈中所有任务都执行完毕,主线程处于闲置状态时,从“任务队列”的队首读取事件加入到执行栈执行
  • 一直循环以上步骤,这个过程就叫做“事件循环(Event Loop)

下图可以很好的展示这个情况:

图中的stack就是我们说的执行栈,WebAPIs代表一些异步事件,callback queue代表事件队列。

micro task 和 macro task

我们先看一段代码:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

以上代码的执行结果是什么呢?不卖关子了,直接上结果吧

script start
script end
promise1
promise2
setTimeout

你答对了吗?没答对也不要紧,下面我们来分析一下

之前介绍的事件循环只是大概的一个过程,实际上不同的异步任务,它们的执行优先级也不一样。异步任务分为两类:微任务(micro task)和宏任务(macro task)

  • macro task:每次执行栈执行的代码就是一个宏任务

    • 主代码块,setTimeout,setInterval等
  • micro task:当前宏任务执行结束后立即执行的任务

    • Promise,MutaionObserver,process.nextTick等

我们知道,在一个事件循环中,异步事件返回结果后会添加一个事件到任务队列。实际上,会根据这个异步事件的类型,会被添加到对应的宏任务队列或者微任务队列上。当执行栈为空时,主线程会查看微任务队列是否有事件存在。如果不存在,再去宏任务队列取出事件加入到当前执行栈;如果存在,则依次执行队列中的事件,直到队列为空,再去执行宏任务队列中的事件。我们只需要记住:微任务队列中的事件优先级大于宏任务队列,微任务永远在宏任务之前执行

定时器

前面我们说过定时器(setTimeout,setTimeInterval)并不是由JS引擎计数的,而是在由单独线程计数。定时器功能主要由setTimeout() 和 setInterval()两个函数来完成,这两个函数内部运行机制完全一样,唯一的区别在于前者指定的代码只执行一次,而后者反复执行。这里主要用前者举例。

setTimeout(() => {console.log(1)}, 0);
console.log(2);

以上代码的执行结果永远是2,1;因为第二行代码是同步任务,在主线程中,而第一行是异步任务,在任务队列(宏任务队列)中;只有主线程任务全部执行完毕后才会执行任务队列中的回调。setTimeout(fn, 0)就意味着fn会尽可能早的执行,但它永远在同步任务、微任务队列以及现有的宏任务队列(已经完成的异步任务)之后才会执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。

注意:setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

到这里,我们再看一下这段代码:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

现在就可以解释上面代码的执行结果了。

参考资料

JavaScript 运行机制详解:再谈Event Loop

Tasks, microtasks, queues and schedules

原文链接:https://jx-zyf.github.io/posts/338a161/