线程和事件机制

线程和事件机制

JS线程和事件机制

进程和线程

进程和线程

进程:

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

线程:

  1. 是进程内的一个独立执行单元
  2. 是程序执行的一个完整流程
  3. 是CPU的最小的调度单元

进程和线程:

  1. 应用程序必须运行在某个进程的某个线程上
  2. 一个进程中至少有一个运行的线程:主线程 –>进程启动后自动创建
  3. 一个进程中也可以同时运行多个线程:此时这个程序是多线程运行的
  4. 多个进程之间的数据是不能直接共享的 –>内存相互独立(隔离)
  5. 线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用
  • JS是单线程运行的 , 但使用H5中的 Web Workers可以多线程运行
  • 浏览器是多线程运行的
  • 单进程和多进程的浏览器:
    • 单进程
      • firefox
      • 旧ie
    • 多进程
      • chrome
    • 新版ie

浏览器内核

支撑浏览器运行的最核心的程序

主流浏览器内核

  • Chrome,Safari:[webkit]
  • firefox:[Gecko]
  • IE:[Trident]
  • 360等国产双核浏览器:[Trident+webkit]

内核模块组成

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

定时器是定时执行的吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<button id="btn">启动定时器</button>
<script type="text/javascript">
document.getElementById('btn').onclick = function () {
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function () {
console.log('定时器执行了', Date.now()-start) //定时器并不能保证真正定时执行,一般会延迟一丁点
}, 200)
console.log('启动定时器后...')
// 做一个长时间的工作
for (var i = 0; i < 1000000000; i++) { //会造成定时器延长很长时间
...
}
}
</script>
</body>
  • 定时器不能保证真正定时执行
  • 一般会延迟一点(在可以接受的范围),也可能延迟很久(不能接受)

定时器回调函数是在分线程执行?

在主线程执行的, JS是单线程的

定时器是如何实现的?

事件循环模型

JS是单线程执行的

单线程执行的代码

  • setTimeout()的回调函数是在主线程执行的
  • 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 如何证明JS执行是单线程的
setTimeout(function () { //4. 在将[timeout 1111]弹窗关闭后,再等一秒 执行此处
console.log('timeout 2222')
alert('22222222')
}, 2000)
setTimeout(function () { //3. 过了一秒后 打印 timeout 1111并弹窗,此处如果不将弹窗关闭,不会继续执行上方222
console.log('timeout 1111')
alert('1111111')
}, 1000)
setTimeout(function () { //2. 然后打印timeout() 00000
console.log('timeout() 00000')
}, 0)
function fn() { //1. fn()
console.log('fn()')
}
fn()
//----------------------
console.log('alert()之前')
alert('------') //暂停当前主线程的执行, 同时暂停计时, 点击确定后, 恢复程序执行和计时
console.log('alert()之后')

流程结果:

  1. 先打印了[fn()],然后马上就打印了[timeout() 00000]
  2. 过了一秒后 打印 timeout 1111并弹窗,此处如果不将弹窗关闭,不会继续执行上方222
  3. 在将[timeout 1111]弹窗关闭后,再等一秒 执行此处
  • 问:为何明明写的是2秒,却关闭上一个弹窗再过一秒就执行?
  • 解:并不是关闭后再计算的,而是一起计算的,alert只是暂停了主线程执行

JS代码分类和执行的基本流程

代码分类:

  • 初始化代码
  • 回调代码(作为参数传递给另一个函数的函数代码。)

js引擎执行代码的基本流程

  1. 先执行初始化代码: 包含一些特别的代码 回调函数(异步执行)
  • 设置定时器
  • 绑定事件监听
  • 发送ajax请求
  1. 后面在某个时刻才会执行回调代码

js单线程模式的原因

JavaScript 是一种单线程的语言,这意味着在任何给定的时间点,JavaScript 只能执行一个任务。这种设计选择主要是出于以下原因:

  1. 简单性:

    • JavaScript 最初是为了在浏览器环境中运行而设计的。为了简化实现,JavaScript 采用了单线程模型,这样开发者就不需要担心多线程编程中的复杂问题,比如同步和死锁。
  2. 一致性:

    • 单线程模型保证了脚本执行的一致性和可预测性。这使得开发者能够更容易地理解和调试他们的代码。
  3. 资源限制:

    • 当JavaScript最初设计时,浏览器环境的资源相对有限。为了避免过多占用系统资源,单线程模型可以确保浏览器性能不受影响。
  4. 异步编程:

    • 尽管JavaScript本身是单线程的,但它通过事件循环机制支持非阻塞I/O操作,这使得它能够高效地处理异步任务,如HTTP请求、DOM事件和定时器等。这使得JavaScript能够在不阻塞主线程的情况下处理后台任务。
  5. DOM操作:

    • 浏览器的DOM(文档对象模型)是共享资源,多个线程同时修改DOM可能会导致冲突。单线程模型可以避免这种情况发生。

尽管JavaScript是单线程的,但通过使用事件循环、回调函数、Promise、async/await等机制,它能够有效地处理异步操作。这些技术允许JavaScript在等待长时间运行的操作完成时继续执行其他任务,从而提高了整体的响应性和性能。

事件循环模型

概念

javascript从诞生之日起就是一门单线程的非阻塞的脚本语言

单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

非阻塞:

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务(没有挂起直接执行相当于阻塞了主线程的执行),然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的:

也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,需要进行各种各样的dom操作。 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

当然,单线程在保证了执行顺序的同时也限制了javascript的效率,因此开发出了web workers技术。这项技术号称可以让javaScript成为一门多线程语言。

然而,使用web workers技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。

可以预见,未来的javascript也会一直是一门单线程的语言。

提到javascript的另一个特点是“非阻塞”,javascript引擎到底是如何实现的这一点呢?

答案就是——event loop(事件循环)。

浏览器中js引擎事件循环机制

执行栈:

当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈(执行栈是管理这些执行上下文的一种机制。)。

事件队列(Task Queue):

JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务,当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列

被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

事件循环机制

以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)

Web Workers

此处只做简略了解,详见Web Workers

  1. H5规范提供了js分线程的实现, 取名为: Web Workers
  2. 相关API
  • Worker: 构造函数, 加载分线程执行的js文件
  • Worker.prototype.onmessage: 用于接收另一个线程的回调函数
  • Worker.prototype.postMessage: 向另一个线程发送消息
  1. 不足
  • worker内代码不能操作DOM(更新UI)
  • 不能跨域加载JS
  • 不是每个浏览器都支持这个新特性

斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
// 1 1 2 3 5 8 f(n) = f(n-1) + f(n-2)
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
// console.log(fibonacci(7))
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
var result = fibonacci(number)
alert(result)
}
</script>

当我运行此行代码,传入计算数值为50左右(有的甚至更低),整个页面就会卡住好久的时间不能操作(计算结束后才会弹窗,但是未弹窗的这段时间用户并不能进行操作)

使用Web Workers

主线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value

//创建一个Worker对象
var worker = new Worker('worker.js')
// 绑定接收消息的监听
worker.onmessage = function (event) { //此处变成回调代码,会在初始化工作完成后才会进行
console.log('主线程接收分线程返回的数据: '+event.data)
alert(event.data)
}

// 向分线程发送消息
worker.postMessage(number)
console.log('主线程向分线程发送数据: '+number)
}
// console.log(this) // window

</script>
</body>

分线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//worker.js
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}

console.log(this)
this.onmessage = function (event) {
var number = event.data
console.log('分线程接收到主线程发送的数据: '+number)
//计算
var result = fibonacci(number)
postMessage(result)
console.log('分线程向主线程返回数据: '+result)
// alert(result) alert是window的方法, 在分线程不能调用
// 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面
}

原理流程图