Skip to content
On this page

线程机制与事件循环

线程与进程

进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间。

把这些概念拿到浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

一些概念:

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程

  • 并行与并发:

    • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
    • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
    • 并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,
    • 我们会用TPS或者QPS来反应这个系统的处理能力。
  • 进程

    • 程序的一次执行,它占有一片独有的内存空间
    • 可以通过 window 任务管理器查看进程
  • 线程

    • 是进程内的一个独立执行单元
    • 是程序执行的一个完整流程
    • 是CPU的最小的调度单元
  • 关系

    • 一个进程至少有一个线程(主)
    • 程序是在某个进程中的某个线程执行的

执行栈

执行栈就是存储函数调用的栈结构,遵循先进后出的原则

js
function a() {
  console.log(1)
}

function b() {
  console.log(2)
  a()
}

console.log(b())

看到上面的代码,在调试阶段可以看到执行栈的执行顺序。首先执行全局代码,根据先进后出的原则,后执行的函数 a 会先弹出栈。

或者在一些报错信息中,也可以找到执行栈的痕迹

js
function a() {
  throw new Error('error')
}

function b() {
  console.log(2)
  a()
}

console.log(b())

我们可以在上图中看到报错在 a 函数,a 函数又是在 b 函数中调用的。

当我们使用递归的时候,因为执行栈的内存空间是有限的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题。

浏览器内核模块

  • 主线程
    • js 引擎模块:负责js程序的编译与运行
    • html,css文档解析模块:负责页面文本的解析
    • DOM/CSS模块:负责dom/css在内存中的相关处理
    • 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)
  • 分线程
    • 定时器模块:负责定时器的管理
    • DOM事件响应模块:负责事件的管理
    • 网络请求模块:负责Ajax请求

js线程

  • js是单线程执行的(回调函数在主线程)
  • h5提出了实现多线程的方案:Web Workers
  • 只能是主线程更新界面

定时器问题

  • 定时器并不是完全定时
  • 如果在主线程执行了一个长时间的操作,可能导致延时才处理

任务

我们都知道JS的单线程,就是说只能从上往下,只能执行一个任务;其他的任务要执行都必须在后面排队。

  • 同步任务:那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

  • 异步任务:那些被引擎放在一边,不进入主线程、而进入任务队列的任务。

常见的用的多的一些异步任务

  • ajax 请求
  • settimeout
  • setinterver
  • 事件
  • promise

而异步任务里又分了 宏任务,微任务

  • 同步任务优先执行
  • 微任务优先宏任务执行
  • 微任务 promise
  • 宏任务 setTimeout

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?

答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。

事件处理机制

  • 代码分类
    • 初始化执行代码:包含绑定dom事件监听,设置定时器,发送 ajax 请求的代码
    • 回调执行代码:处理回调逻辑
  • js引擎执行代码的基本流程
    • 初始话代码 ==> 回调代码
    • 先执行初始化代码:包含一些特别的代码回调函数(异步执行)
      • 设置定时器
      • 绑定事件监听
      • 发送 ajax 请求
    • 后面在某个时刻才会执行回调代码
  • 模块的2个重要组成部分
    • 事件管理模块
    • 回调队列
  • 模块的运转流程
    • 执行初始化代码,将事件回调函数交给对应的模块管理
    • 当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
    • 只有当初始化代码执行完后(可能要一定时间),才会遍历读取回调队列中的回调函数执行

Web Workers

javascript
// 可以让js在分线程执行
// main.js
var worker = new Worker('worker.js')
worker.onMessage = function(event) {
  console.log(event.data) // 主线程接收分线程发送过来的数据
}
worker.postMessage(data) // 向分线程发送数据
// worker.js
var onmessage = function(event) {
  console.log(event.data) // 接口主线程发送数据
  // 向主线程发送数据
  postMessage(reslut)
}

缺点

  • worker 内代码不能操作 DOM 更新视图
  • 浏览器兼容问题
  • 不能跨域加载 Js