Skip to content

理解浏览器中js执行机制

一. 进程与线程

有这样一句话:进程是资源分配的最小单位,线程是CPU调度的最小单位

计算机里,调度任务和分配任务的单位是进程,每开启一个软件,就是开启一个进程,我们所有的的代码都是运行在进程里。

进程中包含着很多的线程,进程里面也可能有很多的子进程。

线程被包裹在进程中,是进程中你给的实际运作单位,一条线程指的就是进程中的一个单一顺序的控制流。也就是说,应用程序要做的事情都存储在线程中,可以这样认为,一条线程就是一个待办列表,待办列表里面会有很多指令,这些指令最终都是由CPU执行的。还有,线程是不能脱离进程当读存活的

操纵系统会按照进程为单位,给应用程序分配资源,比如内存,这样应用程序才能在操作系统中运作起来。

image.png

一个形象的例子: 计算机的核心是CPU,就像一座工厂,时刻在运行。 单个CPU一次只能运行一个任务。 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 一个车间里,可以有很多工人。他们协同完成一个任务。 线程就好比车间里的工人。一个进程可以包括多个线程。

二. 浏览器进程模型

浏览器是一个多进程模型

浏览器每个页签对应的就是每一个进程(基本上是这样,也有同一个网站的不同页签可能是同一个进程的情况),我们可以在任务管理器里面直观的看到我们当前电脑开启了那些进程

image.pngimage.png

Google Chrome创建三种不同类型的进程:浏览器进程,渲染器进程,插件进程。

  1. 浏览器进程:(主进程)浏览器进程只有一个,用于管理标签页、窗口和浏览器本身。这个进程同时负责处理所有跟磁盘、网络、用户输入和显示的交互,然而它不分析和渲染任何网页内容。
  2. 渲染器进程:(即内核 ,js、UI渲染都是在渲染进程完成的)++开发主要就是关注渲染进程++
  3. 插件进程:出于会用状态的插件才会创建一个进程,例如:Flash、Quicktime或Adobe reader。这些进程仅仅包含插件本身以及和浏览器进程、渲染器进程交互的胶水代码

1. 浏览器的渲染进程

首先需要明确一点:JS的主线程是单线程的(注意:是主线程)

我们的代码就是运行在渲染进程里的,但是,当我们js代码运行的时候,其他的操作就不能运行了。

js主线程是单线程,也就是说js还可以开出其他的一些子线程,只是说,它默认执行的线程是主线程,除了执行js,主线程还要执行UI渲染,也就是说,UI渲染和JS执行共用了一条线程(主线程),所以在JS执行时候UI会停止渲染。UI渲染时候,JS会停止执行,他们是互斥的

这样设计的原因,可以保证DOM操作时候不至于多条线程产生冲突,代码从上到下执行,指的都是主线程里面js是从上到下执行的。

除了主线程之外,还有别的子线程,例如:事件,Ajax请求,定时器,都是每个不同的线程,但是都包含在进程中,我们每次调用定时器,都会单独去开一个线程。每次发一个请求,也会单独开一个线程,因此,JS并不是纯单线程的,只是主线程是单线程的。

浏览器也提供一种工作线程(webworker),但是工作线程和主线程之间是不平等的,主线程能操作DOM,而工作线程不行,主线程只有一个。 拓展:new WebWorker()创建一个工作线程,可用于计算等工作,然后通过线程之间的通信将结果抛出来。。

2. 问:浏览器每个页签为什么是每个不同的进程?

若是同时开了十个不同的页签,其中一个陷入死循环被卡死了,但是其他的页签是不会受到影响的,依然能正常访问,为了保证每个页签之间没有冲突,所以每个页签之间应该是每个单独的进程,

Google Chrome可以有自己的任务管理器,你可以通过右击浏览器标题栏打开。这个任务管理器可以让你跟踪每个网络应用和插件的资源使用率,而不是针对整个浏览器。它也可以让你在不需要重启浏览器的情况下终止任何停止响应网络应用或插件。

3. 渲染器进程包含那些进程与线程

渲染器进程会创建多个进程,每个都负责渲染网页。渲染器进程中包含用于操作HTML,JavaScript,CSS,图片和其他内容的复杂的逻辑。我们使用了也同样被Apple Safari浏览器使用的开源的WebKit渲染引擎实现以上功能。每个渲染器进程都运行在沙箱内,这意味着它对磁盘、网络和显示器没有直接的访问权限。所有跟网络应用的交互,包括用户输入事件和屏幕绘制都必须通过浏览器进程。这可以让浏览器进程监视渲染器的可疑行为,一旦发现其从事破坏活动就将其终止。

1. GUI渲染线程

主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。

当界面需要重绘或者由于某种操作引发回流时,将执行该线程。

该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,JS引擎才会去执行GUI渲染。

2. JS引擎线程

该线程当然是主要负责处理Javascript脚本,执行代码。

也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待JS引擎线程的执行。

当然,该线程与互斥,当JS引擎线程执行 脚本时间过长,将导致页面渲染的阻塞。

3. 事件触发线程

主要负责将准备好的事件交给JS引擎线程执行。

比如setTimeout定时器计数结束,ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行。

4. 定时器触发线程

顾名思义,负责执行异步定时器一类的函数的线程,如:setTimeout,setInterval。

主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。

5. HTTP请求线程

顾名思义,负责执行异步请求一类的函数的线程,如:Promise,axios,ajax等。

主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。

三. 宏任务与微任务

因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任》务(micro task)和宏任务(macro task)。

宏任务(macro task):宿主环境提供的异步方方法 常见的宏任务有:script(整体代码)setTimeoutsetIntervalsetImmediateI/OUI rendering

微任务(macro task):语言本身提供的是微任务(js本身提供的) 比如:promise.thennew MutaionObserver()process.nextTick(Node中的)

主线程

MDN对主线程的解释:

主线程用于浏览器处理用户事件和页面绘制等。默认情况下,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。这意味着一个长时间运行的 JavaScript 会阻塞线程,导致页面无法响应,造成不佳的用户体验。 除非故意使用 web worker,比如 service worker,不然 JavaScript 只在线程中运行,所以脚本的运行时,很容易导致事件处理流程或绘制的延迟。主线程中运行的工作越少,就有越多的余地来处理用户事件,页面绘制和对用户保持响应。

四. 浏览器Event Loop机制

学习JS,Event Loop是一个绕不开的点。JS 的异步执行逻辑依赖 Event Loop 机制,但是这套机制却是定义在 HTML 标准中的。因为 Event Loop 本身并不属于 ES 层面的功能,是宿主环境给脚本提供了这一机制,才让脚步有了异步执行的能力。根据JS宿主环境的不同,可以分为浏览器的事件循环和node的事件循环,两者之间会有一些不同。这里只讲浏览器的事件循环,node的事件循环机制在服务端的文章里面查看。

  1. 微任务队列每次都会创建一个全新的队列、事件队列仅有一个
  2. 每循环一次会执行一个宏任务,并清空对应的微任务队列,每次事件循环完毕后会判断页面是否需要重新渲染 (大约16.6ms会渲染一次)

eventLoop.png

1. Event Loop描述

  1. 一个代码块推入执行栈,从上到下以此执行;
  2. 遇到异步代码,会开启一条线程当独处理, 是宏任务则会推进宏任务队列,微任务推入微任务队列;
  3. 栈中同步代码执行完毕,回去检查微任务队列中是否有微任务,有的话全部推入栈中执行;
  4. 期间产生宏任务继续推入宏任务队列,产生微任务也继续推入微任务队列;
  5. 上一步微任务执行完毕后会继续执行产生的微任务;
  6. 微任务执行完后,会检查是否达到16.6毫秒,若达到则渲染页面,暂停执行js,若没达到则,先将要渲染的内容缓存起来,向下一轮循环走;
  7. 从宏任务队列内取出一个时间到了或者满足条件的任务入栈执行;
  8. 若期间产生宏任务,继续推入宏任务队列,产生微任务则推入微任务队列;
  9. 执行完宏任务继续清空微任务队列...
  10. 微任务队列清空后检查据是否可以渲染,可以渲染则检查缓存内是否有要渲染的,有则一起渲染,没有自己渲染;
  11. 一直循环,直到代码执行完停止

image.png