剖析JavaScript运行机制

162 阅读6分钟

这篇学习笔记是在学习过程中参照了一些文章总结出来的,仅供参考学习

1. 进程和线程

首先要了解 JavaScript 的执行机制就要先了解一下计算机中进程和线程的相关概念

【 进程(process)】

  • 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系>统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式 的描述,进程是程序的实体

多进程:

  • 启动多个进程,多个进程可以一块来执行多个任务;

【 线程(thread)】

  • 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程

  • 启动一个进程,在一个进程内部启动多个线程,这样,多个线程也可以一块执行多个任务;(也是通过调度来做的,如同多进程 能做多任务一样,多线程也能做多任务)

【简单概括】

  • 进程: 在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是内存资源分配的最小单位

  • 线程: 内存分配资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程是程序执行的最小单位。

【一个比喻】

  • 工厂的资源 -> 系统分配的内存(独立的一块内存)
  • 工厂之间的相互独立 -> 进程之间相互独立
  • 多个工人之间协作完成任务 -> 多个线程在进程中协作完成任务
  • 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成

2. 浏览器的多进程架构

参考:juejin.cn/post/709112…

Tip:2007年以前市面上浏览器都是单进程的


🌈目前多进程架构

【 目前浏览器的进程 】

  • 浏览器主进程(Browser)
  • GPU进程、网络(NetWork)进程
  • 多个渲染进程(浏览器内核
  • 多个插件进程

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中
  • GPU 进程:实现 3D CSS 的效果
  • 网络进程:主要负责页面的网络资源加载
  • 插件进程:主要是负责插件的运行

【渲染进程的多线程】

  1. JS引擎线程(单线程)
  2. GUI 线程(和 JS 引擎线程互斥)-> 绘制用户界面的
  3. Http 网络请求线程
  4. 定时器触发线程
  5. 浏览器事件处理线程

其中后三个统一叫做 webAPIs用来处理异步事件

JS 引擎是单线程的原因

  • JS 是为了与用户交互而设计的,设计之初就是为了操作DOM的,如果是多线程的,一个JS操作 button,一个JS 删除 button,会产生DOM冲突,所以设计为单线程。

单线程的 JS 处理 大量数据

  1. SSR(服务端渲染技术) 计算交给后端,前端只负责渲染(Vue + Node)
  2. webworker:向 JS 引擎申请一个子线程,这个子线程是浏览器开辟的。该子线程完全不能访问DOM ;实际上并没有解决单线程的问题,引入只是为了解决数据量大的问题

当处理的数据量没那么多时:异步就是最好的解决方案


3. JS 的运行原理

异步: 异步是通过事件驱动(web APIs)模拟的

三个异步场景:

  1. Http 网络请求线程 ->ajax
  2. 定时器触发线程 -> setTimeout/setTimeInterval
  3. 浏览器事件处理线程 -> addEventListener

需要注意的是:

  • 所有的异步代码基本上都是以回调函数的方式出现的, 但是并不是所有的回调函数都是异步代码如sortfilter
  • 不同的环境,基于事件驱动的模型可能会不一样,如浏览器和 Node

🌈执行流程

参考文档:segmentfault.com/a/119000001…

console.log('Hi');

setTimeout(()=>{
  console.log('cb1');
}, 5000);

console.log('End');

// 依次输出 Hi End cb1
  1. 初始化状态都为空,浏览器控制台是空的的,调用堆栈也是空的

  1. console.log('Hi') 添加到调用堆栈中

  1. 执行 console.log('Hi')

  1. console.log('Hi') 从调用堆栈中移除。

  1. setTimeout(function cb1() { ... }) 添加到调用堆栈。

  1. setTimeout(function cb1() { ... }) 执行,浏览器创建一个计时器计时,这个 作为Web api的一部分

  1. setTimeout(function cb1() { ... })本身执行完成,并从调用堆栈中删除。

  1. console.log('End') 添加到调用堆栈

  1. 执行 console.log('Bye')

  1. console.log('Bye') 从调用调用堆栈移除

  1. 至少在5秒之后(>5s),因为可能同步任务会阻塞,计时器完成并将 cb1 回调推到回调队列。

  1. 事件循环 从回调队列中获取cb1并将其推入调用堆栈。

  1. 执行 cb1 并将 console.log('cb1') 添加到调用堆栈。

  1. 执行 console.log('cb1')

  1. console.log('cb1') 从调用堆栈中移除

  1. cb1 从调用堆栈中移除

调用栈轮询,先看同步队列中是否有任务没有执行,有,执行,没有,看异步队列 -> 反复监听异步队列

Tip:同步代码优先执行

4. setTimeout(…) 是怎么工作的

setTimeout(…) 不会自动将回调放到事件循环队列中。它设置了一个计时器。当计时器过期时,环境将回调放到事件循环中

setTimeout(myCallback, 1000);

这并不意味着 myCallback 将在1000毫秒后就立马执行,而是在1000毫秒后,myCallback 被添加到队列中。但是,如果队列有其他事件在前面添加回调刚必须等待前面的执行完后在执行myCallback。

Tip: Html5 中规定,setTimeout 的最小间隔为4ms,填了0之后,会默认改为 4ms

人眼能识别的是30ms, 30ms以内的会认为是连续的,所以setTimeout一般会写30ms

console.log(1);
setTimeout(() => { // 异步代码首先就会挂起
    console.log(2);
}, 0)
console.log(3)

// 1 3 2
setTimeout(() => {
    console.log(1); // 实际上并不一定是执行了1000ms,因为它要等同步代码执行完才执行,所以会因同步代码引起阻塞
}, 1000)
setTimeout(() => {
    console.log(2);
})
console.log(3)
// 3 2 1