JavaScript可否多线程?

深入探讨JavaScript引擎单线程运行机制,解释setTimeout与setInterval的异步执行过程,揭示事件驱动模式如何影响代码执行顺序,通过案例分析理解定时器行为与预期之间的差异。

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

JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如


[javascript]
setTimeout( function(){ alert(
’你好!); } , 0);
setInterval( callbackFunction , 100);
setTimeout( function(){ alert(’你好!’
); } , 0);
setInterval( callbackFunction , 100);


认为setTimeout中的问候方法会立即被执行,因为这并不是凭空而说,而是JavaScriptAPI文档明确定义第二个参数意义为隔多少毫秒后,回调方法就会被执行. 这里设成0毫秒,理所当然就立即被执行了.
同理对setInterval的callbackFunction方法每间隔100毫秒就立即被执行深信不疑!

但随着JavaScript应用开发经验不断的增加和丰富,有一天你发现了一段怪异的代码而百思不得其解:


[javascript]
div.onclick = function(){
setTimeout( function(){document.getElementById(
’inputField).focus();}, 0);
};
div.onclick = function(){
setTimeout( function(){document.getElementById(’inputField’
).focus();}, 0);
};


既然是0毫秒后执行,那么还用setTimeout干什么, 此刻, 坚定的信念已开始动摇.

直到最后某一天 , 你不小心写了一段糟糕的代码:


[javascript]
setTimeout( function(){ while(true){} } , 100);
setTimeout( function(){ alert(
’你好!); } , 200);
setInterval( callbackFunction , 200);
setTimeout( function(){ while(true){} } , 100);
setTimeout( function(){ alert(’你好!’
); } , 200);
setInterval( callbackFunction , 200);

第一行代码进入了死循环,但不久你就会发现,第二,第三行并不是预料中的事情,alert问候未见出现,callbacKFunction也杳无音讯!

这时你彻底迷惘了,这种情景是难以接受的,因为改变长久以来既定的认知去接受新思想的过程是痛苦的,但情事实摆在眼前,对JavaScript真理的探求并不会因为痛苦而停止,下面让我们来展开JavaScript线程和定时器探索之旅!

拔开云雾见月明

出现上面所有误区的最主要一个原因是:潜意识中认为,JavaScript引擎有多个线程在执行,JavaScript的定时器回调函数是异步执行的.

而事实上的,JavaScript使用了障眼法,在多数时候骗过了我们的眼睛,这里背光得澄清一个事实:

JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.

JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化.

那么单线程的JavaScript引擎是怎么配合浏览器内核处理这些定时器和响应浏览器事件的呢?
下面结合浏览器内核处理方式简单说明.

浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件,下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的.虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异.

JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如

由图可看出,浏览器中的JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可以源自JavaScript引擎当前执行的代码块,如调用setTimeout添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理.

上图t1-t2..tn表示不同的时间点,tn下面对应的小方块代表该时间点的任务,假设现在是t1时刻,引擎运行在t1对应的任务方块代码内,在这个时间点内,我们来描述一下浏览器内核其它线程的状态.

t1时刻:

GUI渲染线程:

该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.

在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.

所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.

GUI事件触发线程:

JavaScript脚本的执行不影响html元素事件的触发,在t1时间段内,首先是用户点击了一个鼠标键,点击被浏览器事件触发线程捕捉后形成一个鼠标点击事件,由图可知,对于JavaScript引擎线程来说,这事件是由其它线程异步传到任务队列尾的,由于引擎正在处理t1时的任务,这个鼠标点击事件正在等待处理.

定时触发线程:

注意这里的浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时,所以队列中的定时事件也是异步事件.

由图可知,在这t1的时间段内,继鼠标点击事件触发后,先前已设置的setTimeout定时也到达了,此刻对JavaScript引擎来说,定时触发线程产生了一个异步定时事件并放到任务队列中, 该事件被排到点击事件回调之后,等待处理.
同理, 还是在t1时间段内,接下来某个setInterval定时器也被添加了,由于是间隔定时,在t1段内连续被触发了两次,这两个事件被排到队尾等待处理.

可见,假如时间段t1非常长,远大于setInterval的定时间隔,那么定时触发线程就会源源不断的产生异步定时事件并放到任务队列尾而不管它们是否已被处理,但一旦t1和最先的定时事件前面的任务已处理完,这些排列中的定时事件就依次不间断的被执行,这是因为,对于JavaScript引擎来说,在处理队列中的各任务处理方式都是一样的,只是处理的次序不同而已.

t1过后,也就是说当前处理的任务已返回,JavaScript引擎会检查任务队列,发现当前队列非空,就取出t2下面对应的任务执行,其它时间依此类推,由此看来:

如果队列非空,引擎就从队列头取出一个任务,直到该任务处理完,即返回后引擎接着运行下一个任务,在任务没返回前队列中的其它任务是没法被执行的.

相信您现在已经很清楚JavaScript是否可多线程,也了解理解JavaScript定时器运行机制了,下面我们来对一些案例进行分析:

案例1:setTimeout与setInterval


[javascript]
setTimeout(function(){
/*
代码块... */
setTimeout(arguments.callee, 10);
}, 10);

setInterval(function(){
/*
代码块... */
}, 10);
setTimeout(function(){
/* 代码块
... */
setTimeout(arguments.callee, 10);
}, 10);

setInterval(function(){
/*
代码块... */
}, 10);

这两段代码看一起效果一样,其实非也,第一段中回调函数内的setTimeout是JavaScript引擎执行后再设置新的setTimeout定时, 假定上一个回调处理完到下一个回调开始处理为一个时间间隔,理论两个setTimeout回调执行时间间隔>=10ms.第二段自setInterval设置定时后,定时触发线程就会源源不断的每隔十秒产生异步定时事件并放到任务队列尾,理论上两个setInterval回调执行时间间隔<=10.

案例2:ajax异步请求是否真的异步?

很多同学朋友搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?
其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange所设置的函数
.



摘自 Tommy’s Css Life

 

<think>我们参考提供的引用内容,特别是引用[2]和引用[3]中关于使用Worker创建子线程的说明。 根据引用[2]: "JavaScript 使用Worker创建多线程。Worker子线程的用法简单、方便,开发者只要传入一个JavaScript脚本的URL,创建一个Worker对象,浏览器就会启动一条线程来执行这段JavaScript脚本。如果需要与Worker线程交换数据,Worker提供了postMessage(data)方法和onmessage事件监听器属性。" 引用[3]则给出了具体的代码示例。 因此,创建和使用子线程的步骤如下: 1. 创建Worker对象:在主线程中,通过`new Worker(scriptURL, options)`创建一个Worker对象,其中scriptURL是子线程执行的脚本的URL,options可以包含name等属性。 2. 与子线程通信:使用`postMessage`方法向子线程发送数据,通过设置`onmessage`事件处理函数来接收子线程发来的消息。 3. 子线程脚本:子线程脚本中,使用`self`(或`this`)来代表子线程本身,通过监听`message`事件来接收主线程发来的数据,使用`postMessage`向主线程发送数据。 同时注意引用[3]提到的限制:子线程不能操作DOM,且必须同源。 下面我们按照步骤详细说明,并给出代码示例。</think>### JavaScript 创建和使用子线程的方法 在 JavaScript 中,创建子线程主要通过 **Web Workers** 实现。Web Workers 允许在后台线程中运行脚本,避免阻塞主线程,从而提升性能。以下是具体实现步骤: #### 1. 创建 Worker 对象(主线程) ```javascript // 主线程 main.js const worker = new Worker('worker-script.js', { name: 'backgroundWorker' // 可选命名 }); // 向子线程发送数据 worker.postMessage('Hello from main thread!'); // 接收子线程返回的数据 worker.onmessage = function(event) { console.log('Received from worker:', event.data); }; // 错误处理 worker.onerror = function(error) { console.error('Worker error:', error.message); }; ``` #### 2. 编写子线程脚本(worker-script.js) ```javascript // 子线程 worker-script.js self.addEventListener('message', function(e) { const data = e.data; console.log('Received from main:', data); // 执行耗时操作(例如大数据处理) const result = heavyCalculation(data); // 将结果发回主线程 self.postMessage(result); }); function heavyCalculation(input) { // 模拟耗时计算(实际可能是图像处理/加密等) return input.split('').reverse().join(''); } ``` #### 关键特性说明: 1. **线程通信** - 主线程使用 `worker.postMessage()` 发送数据 - 子线程通过 `self.postMessage()` 返回结果 - 双向通信均通过事件监听(`onmessage`)实现[^2][^3] 2. **使用限制** - ❌ **禁止 DOM 操作**:子线程无法访问 `window` 或 `document` 对象 - ✅ **允许操作**:执行计算密集型任务、网络请求(`fetch`)、文件读写等 - 🌐 **同源策略**:Worker 脚本必须与主脚本同源[^3] 3. **资源管理** - 终止线程:主线程调用 `worker.terminate()` 立即停止 Worker - 子线程自毁:调用 `self.close()` #### 实际应用场景 1. **图像处理**:在后台线程解码/编码图片 2. **数据分析**:处理大型数据集而不阻塞 UI 3. **实时通信**:WebSocket 数据预处理 4. **加密运算**:执行 CPU 密集型的加密操作 > ⚠️ 注意事项:浏览器对 Worker 数量有限制(通常建议不超过 `navigator.hardwareConcurrency` 报告的 CPU 核心数)[^3]。过度使用可能导致内存膨胀。 --- ### 相关问题 1. Web Workers 能否共享内存?如何通过 `SharedArrayBuffer` 实现线程间数据共享? 2. 如何处理 Web Workers 中的异常?与主线程的错误处理有何不同? 3. Service Worker 和 Web Worker 的主要区别是什么?各自适用什么场景? 4. 在多线程环境下如何避免资源竞争问题?JavaScript 提供了哪些同步机制? [^1]: 开发者在使用多线程时需要充分考虑线程间的通信成本和安全性 [^2]: Worker子线程的用法简单、方便,通过 `postMessage()` 和 `onmessage` 实现通信 [^3]: 子线程不能操作DOM且必须同源,可通过 `navigator.hardwareConcurrency` 获取最佳线程数 [^4]: JavaScript 是单线程语言,依赖事件循环机制处理异步任务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值