前端中的事件队列

本文详细介绍了JavaScript为何采用单线程模型及其带来的问题,并深入探讨了事件队列和事件循环机制,包括setTimeout和setInterval的行为差异以及AJAX请求的处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么js是单线程

js之所以采用单线程,原因是一开始设计的时候不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。比如,假定js同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?在Java中会使用锁来解决这种竞态条件,而js并不想这样来解决。
当然,单线程模型带来了一些问题,主要是新的任务被加在队列的尾部,只有前面的所有任务运行结束,才会轮到它执行。如果有一个任务特别耗时,后面的任务都会停在那里等待,造成浏览器失去响应,又称“假死”。为了避免“假死”,当某个操作在一定时间后仍无法结束,浏览器就会跳出提示框,询问用户是否要强行停止脚本运行。

事件队列

我们先来看下面的代码

   function fn(){
       var a = 1;
       setTimeout(function(){
           var b = 2;
           console.log('b', b);
       }, 0)
       console.log('a', a);
   }
   fn();
   var c = 3;
   console.log('c', c);

按照正常对单线程的理解,输出应该是

   b 2
   a 1
   c 3

然而,代码的执行结果竟然是

   a 1
   c 3
   b 2

为什么setTimeout里面的代码到了最后才执行,原因就是,js的执行是单线程的。而当它遇到了window的setTimeout和setInterval这样的异步任务,js都默默地先不执行这些回调,而是继续向下执行其他js脚本,等到所有js脚本都解析执行完了,再执行回调。
那么有多个回调的时候执行顺序是怎么样的呢?浏览器是多线程的,js执行线程只是它多个线程中的一个。当js的执行线程看到了setTimeout,浏览器马上会调用其他线程把这个函数中的回调扔到浏览器的事件队列中,事件队列是先入先出的队列。那么在js执行线程执行完所有脚本空闲的时候,事件队列中的事件回调,会一个一个被拿出来执行。浏览器有一个内部大消息循环Event Loop(事件循环),会轮询事件队列并处理事件。例如,浏览器当前正在忙于处理onclick事件,这时另外一个事件发生了(如:input onchange),这个异步事件就被放入事件队列等待处理,只有前面的处理完毕了才能执行下一个。

    setTimeout(function(){
        setTimeout(function(){
            console.log(1);
        }, 1000);
        setTimeout(function(){
            console.log(1);
        }, 2000);
        setTimeout(function(){
            console.log(1);
        }, 3000);
        var time = new Date().getTime();
        while(true){
            //这里模拟一个6s的任务
            if(new Date().getTime() - time > 6000){
                break;
            }
        }
    }, 0)

这里会在6秒任务后看到连续的3个1。
而setInterval有所不同,虽然也不是抢占了当前任务放到队首,但是会在当前线程结束后开始添加时间间隔队列:

    setTimeout(function(){
        setInterval(function(){
            var div = document.createElement('div');
            div.innerHTML =  'I am a interval';
            document.body.appendChild(div);
        }, 1000);
        var time = new Date().getTime();
        while(true){
            //这里模拟一个3s的任务
            if(new Date().getTime() - time > 3000){
                var div = document.createElement('div');
                div.innerHTML =  'three seconds task ends';
                document.body.appendChild(div);
                break;
            }
        }
    }, 0);

这里是先进行3秒计时,再每隔1秒在DOM中输出了 ‘I am a interval’。说明setInterval没有抢占当前的线程,在线程结束后开始计时。(这里非常感谢@伊优01的指正)。

ajax

那么ajax呢。ajax的原理完全一样,当js的执行线程发出了ajax请求后,会继续往下执行js脚本。当有响应返回时,注册在xhr对象上的监听事件中的回调处理函数被放进了任务队列中。等当脚本全部执行完了之后,放在事件队列里的回调处理函数才会执行。每次发送ajax,浏览器都会新开一个线程处理,这些线程之间会共享数据。所以当我们想并发发三个ajax请求的时候,在处理回调函数中如果想操作同一个变量,我们并不知道哪个请求会先返回,那么最好借助promise对象来决定回调函数的顺序。

js下载

js在浏览器中需要被下载、解释并执行这三步。在html body标签中的script都是阻塞的,也就是说,顺序下载、解释、执行。比如script1在script2的前面,但script2先下载完了也没有用,因为全部下载完才会开始解释和执行。
尽管Chrome可以实现多线程并行下载外部资源,例如:script file、image、frame等(css比较复杂,在IE中不阻塞下载,但Firefox阻塞下载)。但是,由于js是单线程的,所以尽管浏览器可以并发加快js的下载,但必须依次执行。
还有一点需要注意,因为浏览器在遇到 < body>才会开始呈现内容,所以js脚本最好不放在< head>标签中,因为js的下载,解释和执行都会阻塞住html的解析,页面可能在一开始出现一片空白

### JavaScript 事件触发机制和过程 #### 事件监听器注册 在前端开发中,JavaScript 的事件机制通过 `addEventListener` 方法来注册事件监听器。此方法接受三个参数:事件名称、回调函数以及可选的布尔值或选项对象。当第三个参数设置为 `true` 时,表示该事件将在捕获阶段被调用;而默认情况下 (`false`) 则是在冒泡阶段触发[^3]。 #### 事件流 浏览器接收到用户操作或其他类型的输入后会启动一个称为 **事件流** 的过程。这个过程中包含了两个主要阶段——捕获阶段与冒泡阶段: - **捕获阶段**: 浏览器从文档根节点向下遍历至目标元素,在此期间任何设置了捕捉模式(`true`)的处理器都会被执行; - **目标阶段**: 当到达具体的目标 DOM 节点时即进入目标阶段,此时无论是否指定了捕捉还是冒泡方式,只要存在相应的处理程序就会运行一次; - **冒泡阶段**: 接着再由最内部的目标向外逐层返回到顶层窗口对象,沿途激活那些处于冒泡状态下的侦听者[^1]。 #### 执行上下文栈中的任务管理 一旦某个事件发生并完成上述传播路径之后,它会被放入当前环境的任务队列里等待执行。这里涉及到 JavaScript线程特性的非阻塞 I/O 模型及其背后的异步编程概念。对于每一个宏任务(如定时器超时、网络请求响应),在其完成后将会清空所有已就绪的微任务(像 Promise 回调)。这样的设计使得页面能够保持流畅的同时还能高效地处理各种并发活动[^2]。 ```javascript // 示例代码展示如何利用 addEventListener 来绑定不同阶段的事件处理器 document.getElementById('myButton').addEventListener('click', function(event){ console.log("点击发生在:" + event.eventPhase); }, true); // 设置为 true 表示在捕获阶段触发 document.getElementById('myButton').addEventListener('click', function(event){ console.log("点击发生在:" + event.eventPhase); }); // 默认 false, 在冒泡阶段触发 ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值