线程机制与事件循环
线程与进程
进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间。
把这些概念拿到浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
一些概念:
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
- 并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,
- 我们会用TPS或者QPS来反应这个系统的处理能力。
进程
- 程序的一次执行,它占有一片独有的内存空间
- 可以通过
window
任务管理器查看进程
线程
- 是进程内的一个独立执行单元
- 是程序执行的一个完整流程
- 是CPU的最小的调度单元
关系
- 一个进程至少有一个线程(主)
- 程序是在某个进程中的某个线程执行的
执行栈
执行栈就是存储函数调用的栈结构,遵循先进后出的原则
function a() {
console.log(1)
}
function b() {
console.log(2)
a()
}
console.log(b())
看到上面的代码,在调试阶段可以看到执行栈的执行顺序。首先执行全局代码,根据先进后出的原则,后执行的函数 a
会先弹出栈。
或者在一些报错信息中,也可以找到执行栈的痕迹
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
请求的代码 - 回调执行代码:处理回调逻辑
- 初始化执行代码:包含绑定dom事件监听,设置定时器,发送
- js引擎执行代码的基本流程
- 初始话代码 ==> 回调代码
- 先执行初始化代码:包含一些特别的代码回调函数(异步执行)
- 设置定时器
- 绑定事件监听
- 发送
ajax
请求
- 后面在某个时刻才会执行回调代码
- 模块的2个重要组成部分
- 事件管理模块
- 回调队列
- 模块的运转流程
- 执行初始化代码,将事件回调函数交给对应的模块管理
- 当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
- 只有当初始化代码执行完后(可能要一定时间),才会遍历读取回调队列中的回调函数执行
Web Workers
// 可以让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