阅读 2033
JS的事件对象与事件机制

JS的事件对象与事件机制

本系列将从以下专题去总结:

1. JS基础知识深入总结
2. 对象高级
3. 函数高级
4. 事件对象与事件机制

暂时会对以上四个专题去总结,现在开始Part4: 事件对象与事件机制。下图是我这篇的大纲。

事件对象与事件机制

4.1 同步与异步

同步(Synchronous):你在做一件事情,不能同时去做另外一件事。

异步(Asynchronous):你在做一件事情,这件事可能会耗时很久,而此时你可以在等待的过程中,去做另外一件事。

比如煮开水这件事吧...在这过程,你担心水沸了而不去做其它事情,就等到水沸腾,那就是同步。

而你觉得这过程耗时蛮久,可以先去做其它事情,比如去扫地,直到水沸腾。这就是异步。

4.1 线程与进程

1.进程(process): 程序的一次执行, 它占有一片独有的内存空间。可以通过windows任务管理器查看进程。进程负责为程序的运行提供必备的环境,相当于工厂的车间。

2.线程(thread): 是进程内的一个独立执行单元。 是CPU的最小的调度单元。 是程序执行的一个完整流程。线程负责执行进程中的程序,相当于工厂工作的工人。

3.图解进程、线程和程序的关系:

程序线程进程的关系
一个程序A有多个进程,那么程序A 就是多进程的程序。程序B是只有一个进程,那么程序B就是单进程的程序。 如果一个进程有一个线程,那么这个程序就是单线程的。如果一个进程有多个线程,那么这个程序就是多线程的。单和多 线程是针对进程而言的。比如,我一个程序有两个进程,这两个进程分别有一个线程,那么这个程序还是单线程的程序。

4.进程与线程

  • 应用程序必须运行在某个进程的某个线程上
  • 一个进程中至少有一个运行的线程: 主线程。进程启动后自动创建
  • 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
  • 一个进程内的数据可以供其中的多个线程直接共享
  • 多个进程之间的数据是不能直接共享的(因为进程是分配独立的内存空间给它)
  • 线程池(thread pool): 保存多个线程对象的容器, 实现线程对象的反复利用。

5.何为多进程与多线程?

  • 多进程运行: 一个应用程序可以同时启动多个实例运行。
  • 多线程: 在一个进程内, 同时有多个线程运行

6.比较单线程与多线程?

单线程与多线程
7.JS是单线程还是多线程?

  • JS是单线程运行的

    在JS设计的本意只是对一些简单的操作而已,比如提交表单用户名和密码之类的。当没有JS时,那么这些数据就会提交到服务器中,那么这个数据处理将会特别大,首先假设有1000个人同时注册,那么这些请求就会到服务器上,服务器的加载量就会很大,而且,用户体验也不好,可能会延迟返回请求信息。这是如果这些操作在浏览器端来操作,那么就会简单很多。所以,JS当时设计的初衷也就单线程了,因为不需要太多的操作。单线程足矣应付,而且不占太多的内存。当然后面会说道,因为他的功能(DOM操作等)也决定了它只能单线程。

  • 但使用H5中的 Web Workers可以多线程运行(主线程只有一个,要做其他的事可以启动分线程)

8.浏览器运行是单进程还是多进程?

  • 有的是单进程
    • Firefox(据Mozilla方面表示,FireFox 54版浏览器已经可以将全部打开的网页标签分为最多四个进程来运行,以此提升浏览器对PC硬件的利用率。)
    • 老版IE
  • 有的是多进程
    • chrome
    • 新版IE

9.如何查看浏览器是否是多进程运行的呢 ?

  • 任务管理器==>进程

10.浏览器运行是单线程还是多线程?

  • 都是多线程运行的

4.2 浏览器内核(Browser core)

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

4.2.1 不同的浏览器可能有不同的内核

  • Chrome, Safari : webkit内核
  • Firefox : Gecko内核
  • IE : Trident内核
  • 360,搜狗等国内浏览器: Trident + webkit(双核,嘻嘻,给你一个眼神~)

4.2.2 浏览器内核由很多模块组成

  • 主线程

    • JS引擎模块 : 负责js程序的编译与运行(也是代码,是解释我们写的代码)
    • HTML,CSS文档解析模块 : 负责页面文本的解析(一开始是HTML和CSS文本信息)
    • DOM/CSS模块 : 负责dom/css在内存中的相关处理 (把一些标签转为DOM树对象)
    • 布局和渲染模块 : 负责页面的布局和效果的绘制(参照内存中的对象数据进行布局与渲染)
  • 分线程

    • 定时器模块 : 负责定时器的管理
    • DOM事件模块 : 负责事件的管理(onclick..)
    • 网络请求模块 : 负责Ajax请求
      浏览器内核组成

4.3 JS线程

1.如何证明js执行是单线程的?

  • setTimeout()的回调函数是在主线程执行的
  • 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行

2.为什么js要用单线程模式, 而不用多线程模式?

  • JavaScript的单线程,与它的用途有关。
  • 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
  • 如果在一个p对象(p标签),假设是在多线程的环境下,那么就会有线程的切换,当一个是操作修改p的内容,另一个是删除p标签,这个时候就会有冲突。
  • 这决定了它只能是单线程,否则会带来很复杂的同步问题。

3.代码的分类:

  • 初始化代码(同步代码)
  • 回调代码(异步代码)

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

  • 先执行初始化代码:包含一些特别的代码
    • 设置定时器
    • 绑定事件监听
    • 发送ajax请求
  • 后面在某个时刻才会执行回调代码:回调函数(异步执行) 看个案例:
<script type="text/javascript">
  setTimeout(function () {
    console.log('timeout 2222')
  }, 2000)
  setTimeout(function () {
    console.log('timeout 1111')
  }, 1000)
  
  function fn() {
    console.log('fn()')
  }
  fn()
  console.log('alert()之前')
  alert('------') //暂停当前主线程的执行, 同时暂停定时器的计时, 点击确定后, 恢复程序执行和计时。
  console.log('alert()之后')
</script>
复制代码

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

4.4 定时器引出的问题

1.定时器真是定时执行的吗?

  • 定时器并不能保证真正定时执行
  • 一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)

2.定时器回调函数是在分线程执行的吗?

  • 在主线程执行的, js是单线程的。不管是回调函数还是非回调函数都是在主线程执行的。

3.定时器是如何实现的?

  • 事件循环模型(后面讲)
<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>
复制代码

4.5 浏览器的事件循环(轮询)模型

4.5.1 一些简述

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

4.5.2 模型原理图

以下这张图就是event-driven interaction model(事件驱动模型)。

另外简单的说一下request-response model(事件响应模型),这个就相当于浏览器去服务器请求一些数据,服务器接收到这些请求,去处理这些请求,紧接着返回给浏览器的请求数据,浏览器接收到数据解析到页面上的一个过程。

现在我们主要是看一下event-driven interaction model:

事件循环模型

首先,这个图分为三个部分:JS引擎等主线程、浏览器内核的分线程、任务队列。

在第一部分中:(堆内存和栈内存)

  • 执行栈(execution stack)
    • 所有的代码都是在此空间中执行的
    • 各个任务按照文档定义的顺序一一推入"执行栈"中,当前一个任务执行完毕,才会开始执行下一个任务。
    • 实则是把上下文对象压栈和弹出,这里执行了一些初始化代码,包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码。
    • 事件(定时器/DOM事件/AJAX)回调函数交给对应模块管理。用setTimeout做比较,他会把回调函数和延迟时间1000给WebAPIS的SetTimeout模块处理。这部分并不是在主进程中执行的,而是在浏览器分线程中执行。
  • heap 用来存储声明的对象。

在第二部分中: 这一块主要是交给浏览器的分线程处理。以setTimeout定时器为比较,他会拿到回调函数和延迟时间1000,当延迟时间过了之后,就会把回调函数推入队列中。

在第三部分中:

  • 临时保存着回调函数,当执行栈为空时,就会依次将其回调函数压入执行栈中。

  • 这个部分叫做callback queue。也叫任务队列(task queue)、消息队列(message queue)、事件队列(event queue)。指的都是同一个。

刚刚以定时器介绍了这个过程。我们再以AJAX为例看看是如何执行这些过程的?

AJAX线程和主线程

上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操作,而JS引擎线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。然后,从消息队列中依次按照顺序取出消息作为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。

另外一点,我们看到事件机制模型图有事件轮询(event loop),就是从任务队列中循环取出回调函数放入执行栈中处理(一个接一个)。JS引擎线程用来执行栈中的同步任务(初始化代码),当所有同步任务(初始化代码)执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件轮询。

4.5.3 宏任务和微任务、

宏任务队列

  1. 宏任务队列可以有多个
  2. setTimeout
  3. 当宏任务队列执行完毕后,此刻微任务队列中有任务,会立即执行微任务队列中的所有任务

微任务队列

  1. 微任务队列只有一个
  2. promise对象的成功的回调和progress.nextTick()
  3. 会再次执行新的宏任务队列(如果有)
function fun() {
  console.log('程序开始执行', 11111111);
  setTimeout(function () {
    console.log('定时器开始执行',666666);
  }, 0)
  new Promise(function (resolve, reject) {
    console.log('promise对象开始执行', 2222222);
    for (var i = 0; i < 5; i++) {
      console.log(i, 33333333);
    }
    resolve();
  })
    .then(() => {
      console.log('promise对象成功的回调执行', 555555);
    })
.then(() => {
    console.log('promise对象失败的回调执行', 555555);
  });
  console.log('程序执行完毕',  444444444444);
}
fun();
//以上程序执行顺序结构就是上述的数字123456.
复制代码

宏任务和微任务

4.6 H5 Web Workers

4.6.1 介绍

Web Workers 是 HTML5 提供的一个javascript多线程解决方案,我们可以将一些大计算量的代码交由web Worker运行而不冻结用户界面,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

4.6.2 案例引入

实现一个斐波那契数列,在页面input中输入数列项的值,得到相应的数列值。

<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)  
    //递归调用(效率很低,时间复杂度很大)
  }
  var input = document.getElementById('number')
  document.getElementById('btn').onclick = function () {
    var number = input.value;
    var result = fibonacci(number); 
    //主线程会一直在处理这个递归调用。导致冻结了用户界面,也就是不能操作界面了。
    alert(result)
  }
</script>
复制代码

以上操作会在js引擎的主线程中,在计算的过程中,会冻结用户界面,达到不佳的用户体验。

4.6.3 使用Web Workers

  • H5规范提供了js分线程的实现,取名为: Web Workers。它支持JavaScript多线程的操作。

  • 相关API

    • Worker: 构造函数, 加载分线程执行的js文件
    • Worker.prototype.onmessage: 用于接收另一个线程的回调函数
    • Worker.prototype.postMessage: 向另一个线程发送消息
  • 使用步骤 步骤1:创建在分线程执行的js文件

  //workers.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, 所以在分线程中不可能更新界面
  }
复制代码

步骤2:在主线程中的js中发消息并设置回调

//主线程

  <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>
复制代码

回顾4.6.2的案例引入,我们可知,那个是完全在主线程中操作,带来的弊端就是冻结了用户界面。而使用Workers在分线程中处理耗时的运算,在主线程去接受计算好的数据,就可以解决直接使用主线程的冻结用户界面的弊端,这个时候不会冻结用户界面,但是子线程完全受主线程控制,且子线程不得操作DOM,因为其this不是window

4.6.4 图解

H5 Web Workers(多线程)

4.6.5 不足点

  1. 慢(本来在主线程肯定是更快的,现在在分线程肯定会慢点,是指这个层面上的"慢")
  2. 不能跨域加载JS
  3. worker内代码不能访问DOM(更新UI)(因为分线程的this不是window)
  4. 不是每个浏览器都支持这个新特性

4.7 习题与案例

案例1

console.log("1");

setTimeout(function(){
	console.log("2");
},1000);

console.log("3");

setTimeout(function(){
	console.log("4");
},0);
复制代码

输出结果: 1->3->4->2.

案例1分析

  1. 两个console.log()都是同步,按照文档的顺序将它们推入"执行栈"中。
  2. 执行栈中的同步任务执行完毕。
  3. 将两个异步任务(定时器)按照第二个参数 (延迟执行的时间) 顺序推入"任务队列"中。
  4. 执行异步任务。

案例2

//同步code1
var t = true;

//异步code2
window.setTimeout(function (){
    t = false;
},1000);

//同步code2
while (t){}

//同步code3
alert('end');
复制代码

案例2分析

  1. 先执行同步code1 -> 同步code2
  2. 此时到进行同步code2while(true){},进入死循环,说明这个时候栈中的同步代码永远不会执行完,也就栈永远不会清空出来,那么任务队列中的代码就不会执行。也就是任务队列中的异步的代码就无法执行。

案例3

//只有用户触发点击事件才会被推入队列中(如果点击时间小于定时器指定的时间,则先于定时器推入,否则反之)
document.querySelector("#box").onclick = function(){
  console.log("click");
};
//第一个推入队列中
setTimeout(function(){
  console.log("1");
},0);
//第三个推入队列中
setTimeout(function(){
 console.log("2");
},1000);
//第二个推入队列中
setTimeout(function(){
  console.log("3");
},0);
复制代码

执行结果:如上面代码段中的分析。

案例3分析:

以上都是异步代码,包括onclick那个。一定要分清哪些是异步的代码。异步代码中的回调函数都会定义在heap中,也就是在右边的堆分配一块内存给他们,这个时候根据他们指定的时候结束后,把他们的回调函数放到任务队列等待执行。

setTimeout的作用是在间隔一定的时间后,将回调函数插入消息队列中,等栈中的同步任务都执行完毕后,再执行。因为栈中的同步任务也会耗时,所以间隔的时间一般会大于等于指定的时间(指定的时间就是回调函数后面一个参数的毫秒值)。

setTimeout(fn, 0)的意思是,将回调函数fn立刻插入消息队列,等待执行,而不是立即执行。只有等待同步任务全部执行完,然后js引擎(js虚拟机)就去从任务队列中拿出来去执行。

案例4

setTimeout(function() {
    console.log("a")
}, 0)

for(let i=0; i<10000; i++) {}
console.log("b")
复制代码

执行结果:先输出b 再输出a

案例4分析:

这个与案例3就差不多了。先执行for循环的同步代码。定时器是异步代码,先等线程的同步代码执行结束后在从任务队列中去拿这些异步代码段执行。

案例5
执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么?

//异步代码
setTimeout(function(){
    for(var i = 0; i < 100000000; i++){}
    console.log('timer a');
}, 0)
//同步代码
for(var j = 0; j < 5; j++){
    console.log(j);
}
//异步代码
setTimeout(function(){
    console.log('timer b');
}, 0)
//函数
function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}
//异步代码
document.addEventListener('click', function(){
    console.log('click');
})
//同步代码
console.log('click begin');
//同步代码,调用函数,执行函数体
waitFiveSeconds();
复制代码

案例5分析:
首先,先执行同步任务。其中waitFiveSeconds是耗时操作,持续执行长达5s。

0
1
2
3
4
click begin
finished waiting
复制代码

然后,在JS引擎线程执行的时候,timer a对应的定时器产生的回调、 timer b对应的定时器产生的回调和两次click对应的回调被先后放入消息队列。由于JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其他异步任务。因此会产生 下面的输出顺序。

click
click
timer a
timer b
复制代码

最后,5s 后的两次 click 事件被放入消息队列,由于此时JS引擎线程空闲,便被立即执行了。

click
click
复制代码

案例6

<script>
for (var i = 0; i < 5; i++){
    var btn = document.createElement('button');
    btn.appendChild(document.createTextNode('Button ' + i));
    btn.addEventListener('click', function (){
        console.log(i);
    });
    document.body.appendChild(btn);
}

// 1、点击 Button 4,会在控制台输出什么? 5
/*An:不管点击哪个button都是输出5.*/
// 2. 给出一种预期的实现方式
/*  将for循环中的var 变成 let 或者 用对象.属性保存起来i的值 */
</script>
复制代码


此文档为吕涯原创,可任意转载,但请保留原链接,标明出处。
文章只在CSDN和掘金第一时间发布:
CSDN主页:https://blog.csdn.net/LY_code
掘金主页:https://juejin.cn/user/3667626521532855
若有错误,及时提出,一起学习,共同进步。谢谢。 😝😝😝

文章分类
前端
文章标签