web Worker学习笔记 | 浏览器切换标签,定时器失效的解决办法

web Worker

web Worker介绍 - 多线程解决方案

Web Workers 是Html5提供的一个多线程解决方案,Web Workers可以在独立于主线程(js线程)的后台线程中,运行一个脚本操作。但是该脚本程序不能操作DOM,主要用于计算。

为什么js是单线程的,假如JS是多线程的,假设现在有2条线程,一条在dom节点上添加节点,另一条删除这个节点。
所以即使web Workers可以开启多线程,也不能操作DOM只能用于计算。

特点
1.DOM限制:没有WebAPI,该脚本程序不能操作DOM,主要用于计算
2.Worker线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成
3.同源限制:分配给 Worker 线程运行的脚本,必须与主线程的脚本文件同源,否则存在跨域问题。
在这里插入图片描述

浏览器多进程架构

浏览器工作原理与实践:https://zhqwq.github.io/broswer-working-principle/guide/01.html
在这里插入图片描述

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页
    • JS引擎线程:JavaScript引擎V8,负责处理JavaScript脚本程序。依靠任务队列来进行js代码的执行,所以js引擎会一直等待着任务队列中任务的到来,然后加以处理。
    • GUI渲染线程:负责渲染浏览器界面,解析 HTML,CSS,构建render树,布局和绘制等。
    • 计时器线程:因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响计时的准确。当使用setTimeout或者setInterval时,需要定时器线程计时。计时到了之后,将对应的回调放入事件队列中,等待JS执行。
      规定要求setTimeout中低于4ms的时间间隔算为4ms。
    • 异步http请求线程:负责异步请求管理,XMLHttpRequest在连通后通过浏览器新起一个线程请求。检测到状态变化时,如果有设置回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,等到JS执行
    • 事件触发线程控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理。
  • GPU进程: GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制。
  • 网络进程:主要负责页面的网络资源加载。
  • 插件进程: 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

渲染进程的创建时机
每次新开一个标签页,都会创建一个新的渲染进程。
但有例外,比如从A页面里面打开一个新的页面B页面,而A页面和B页面又属于同一站点的话,A和B就共用一个渲染进程。

web workers 的使用

语法:new Worker(url, [options]))

虽然 Worker API 建议 Worker 构造函数接受表示 URL 的字符串脚本,在 webpack 5 中只能使用 URL 代替。

new Worker('./worker.js');
/*
webpack5的写法
import.meta.url
*/
new Worker(new URL('./worker.js', import.meta.url));

在主线程中的使用

worker的方法描述
worker.postMessage向 worker.js 发送一个消息,消息可由任何 JavaScript 对象组成
worker.terminate立即终止当前的worker
worker.onmessage接收来自worker的消息时触发
worker.onerrorworker 出现运行中错误时,onerror 事件处理函数会被调用

在worker子线程中的使用
在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量(window可省略)。
worker子线程中,全局作用域变成了WorkerGlobalScope,这个对象少了很多API,比如DOM操作、alert()方法。WorkerGlobalScope对象的属性和方法使用时也可以省略WorkerGlobalScope

属性方法
self(只读)指向当前的WorkerGlobalScope,它的值和this是一样的;
location(只读)指向当前WorkerGlobalScope的URL,它是一个Location对象;
navigator(只读)指向当前WorkerGlobalScope的Navigator对象;
importScripts()用来加载脚本,它的参数是一个或多个脚本的URL,例如:importScripts(‘a.js’, ‘b.js’),它的返回值是undefined,如果加载失败,会抛出一个NetworkError的异常;
onerror用来注册error事件的回调函数;
onlanguagechange用来注册languagechange事件的回调函数;
onoffline用来注册offline事件的回调函数;
ononline用来注册online事件的回调函数;
onrejectionhandled用来注册rejectionhandled事件的回调函数,它是Promise的一个事件;
onunhandledrejection用来注册unhandledrejection事件的回调函数, 它是Promise的一个事件;

关闭worker

方式1:主线程主动关闭worker.terminate()
方式2:子线程内部关闭self.close()

区别
主线程主动关闭worker,主线程与 worker 线程之间的连接都会被立刻停止。
子线程内部关闭worker,不会直接断开与主线程的连接,而是等 worker 线程当前的 Event Loop 所有任务执行完,再关闭。如果在当前Event Loop 中继续调用 postMessage() 方法,主线程还是能通过监听message事件收到消息。

引用其他js文件

这里加载的js文件不受同源策略约束

同源策略:两个 URL 的协议、域名和端口都相同。

使用importScripts()引入

// utils.js
const add = (a, b) => a + b;

// worker.js(worker线程)
// 使用方法:importScripts(path1, path2, ...); 

importScripts('./utils.js'); // 加载需要的js文件

console.log(add(1, 2)); // log 3

ESModule 模式
如果引入的js文件采用 ESModule 模式,需要在创建子线程时指定子线程的类型。

// 主线程
const worker = new Worker('/worker.js', {
    type: 'module'  // 指定 worker.js 的类型
});

// utils.js
export default add = (a, b) => a + b;

// worker.js(worker线程)
import add from './utils.js';
self.addEventListener('message', e => { 
    postMessage(e.data);
});

add(1, 2); // log 3

export default self; // 只需把顶级对象self暴露出去即可

浏览器切换标签,定时器失效的解决办法

问题描述:当网页切换到了后台,大多数浏览器会进行性能优化,定时器会处于休眠或者降频状态。

解决方法
方法1:根据窗口可见性API - 控制定时器的启动与关闭
方法2:在web Workers中开启定时器 - 适合定时器需要一直存在的场景

窗口可见性 API

visibilitychange事件,当其选项卡的内容可见性变化时触发

document.addEventListener('visibilitychange', function() {
  if (document.visibilityState === 'visible') {
    // 页面变为可见状态时执行的操作
  } else if (document.visibilityState === 'hidden') {
    // 页面变为不可见状态时执行的操作
  }
});

案例
定时器来做倒计时
实现:最终时间-开始时间就是要倒计时的时间,然后定时器每秒减1就行
存在问题:最小化后回来时间不对了
方法1:倒计时启动时记录开始时间,每次计算当前时间与开始时间的差值来计时。
方法2:页面处于不可见状态时关闭定时器,页面处于可见状态时以当前时间重新开启定时器

解决定时器失效的方案

将定时器在worker.js中打开,即使切换标签,worker线程也会在后台执行。(切换标签该标签的渲染进程还是存在的)

案例
一个 web socket 链接,为了保持活跃,需要定时向服务器发送心跳信令。主线程需要每5s执行一个操作,就算该窗口隐藏到后台也需要这个操作。

主线程中

const work = new Worker(new URL('./worker.js', import.meta.url));
work.onmessage = () => {
    // 接收到消息主线发送心跳
};

worker.js线程中设置定时器

onmessage =(event)=>{
  setInterval(()=>{
    postMessage('ok')
  },5000)
}
<think>我们面对的问题:在Vue应用中,当用户长时间无操作时退出系统,但浏览器切换标签页或最小化时,定时器可能被暂停导致失效。 原因分析:现代浏览器为了优化性能和电池寿命,会在标签页非激活状态时降低定时器的执行频率(甚至暂停)。这会导致我们用于检测用户无操作的定时器不准确。 解决方案思路: 1. 使用Web Worker:在后台线程中运行定时器,不受主线程休眠影响。 2. 使用VisibilityChange事件:当页面被隐藏时记录时间,再次显示时检查时间差。 3. 使用服务器端辅助:在无操作检测上结合服务器端超时,但会增加服务器负担。 根据引用[1]中的示例,我们可以使用Web Worker来运行定时器,因为Worker线程不受页面隐藏的影响(但要注意Worker本身也会被限制,但通常比主线程的setInterval更可靠)。同时,引用[4]提到了Blob类型,但我们这里不需要。 具体方案(结合Vue): 方案一:使用Web Worker 步骤: 1. 创建一个worker文件(例如timeout.worker.js)或者使用内联worker(通过Blob创建,避免单独文件)。 2. 在Vue组件中,初始化worker,监听worker发送的消息(即超时提醒)。 3. 在用户操作时(如mousemove, keydown等)向worker发送消息重置定时器。 4. 当页面隐藏时(通过visibilitychange事件),我们可以暂停定时器?或者我们不需要暂停,因为worker在后台运行,但是为了精确,我们可以记录页面隐藏的时间,并在再次显示时检查。 但是,注意:Web Worker不能直接访问DOM,所以用户操作事件需要在主线程捕获,然后通过postMessage通知worker。 方案二:使用VisibilityChange事件结合Date对象 1. 在Vue中设置一个定时器(setInterval)用于检测无操作时间,但需要处理页面隐藏时定时器暂停的问题。 2. 监听visibilitychange事件,当页面隐藏时,记录隐藏的时间戳,并清除定时器。 3. 当页面再次显示时,获取当前时间,与隐藏时的时间戳比较,如果超过了设定的无操作时间,则直接退出系统;否则,重新启动定时器(剩余时间=设定时间 - (显示时间 - 隐藏时间))。 方案二更简单,但精度稍低(因为隐藏期间无法精确到秒),但实现简单。 综合考虑,我们采用方案二,因为实现简单且能满足需求,同时避免使用Web Worker带来的额外开销。 具体实现步骤(Vue组件内): 1. 定义变量: - lastActiveTime: 记录最后一次操作的时间戳 - timer: 定时器ID - idleTimeout: 无操作超时时间(毫秒),例如30分钟:30 * 60 * 1000 - hiddenTime: 页面隐藏时的时间戳(用于记录页面隐藏的时刻) 2. 在mounted中: - 初始化lastActiveTime为当前时间 - 启动定时器(检查无操作) - 绑定用户操作事件(如mousemove, keydown等)来更新lastActiveTime - 绑定visibilitychange事件 3. 定时器函数(checkIdle): - 计算当前时间与lastActiveTime的差值 - 如果差值大于等于idleTimeout,则触发退出系统 4. 在visibilitychange事件处理函数中: - 如果页面隐藏(document.hidden为true),则记录当前时间为hiddenTime,并清除定时器(因为页面隐藏后定时器可能暂停,但清除可以确保我们不依赖不准确的定时器) - 如果页面再次显示,则计算页面隐藏的时长(当前时间 - hiddenTime),然后更新lastActiveTime:lastActiveTime += 隐藏时长(因为隐藏期间用户没有操作,所以这段时间应该计入无操作时间) 然后立即调用checkIdle,并重新启动定时器。 注意:更新lastActiveTime的方式也可以选择不更新,而是将隐藏时间直接算作无操作时间的一部分。我们只需要在重新显示时检查一下:从最后一次操作到现在(包括隐藏时间)是否已经超时。 另一种做法:在隐藏时不清除定时器,而是让定时器继续运行(虽然会被暂停,但恢复后会继续执行)。但这样在隐藏期间定时器不会触发,所以超时检查会延迟到页面再次显示。因此,我们需要在visibilitychange事件中,当页面显示时,立即检查是否超时。 因此,调整如下: - 不需要在隐藏时清除定时器,因为即使清除了,我们也要在显示时重新计算,不如在显示时直接检查。 - 在页面显示时,我们直接调用一次checkIdle函数(因为定时器可能被延迟,所以立即检查可以避免超时未触发)。 具体步骤(简化版): 1. 在mounted中: lastActiveTime = Date.now(); // 启动定时器,每秒检查一次 this.timer = setInterval(this.checkIdle, 1000); // 绑定用户操作事件 window.addEventListener('mousemove', this.resetTimer); window.addEventListener('keydown', this.resetTimer); // 绑定visibilitychange事件 document.addEventListener('visibilitychange', this.handleVisibilityChange); 2. 在beforeDestroy中移除事件监听和清除定时器。 3. 方法: resetTimer() { this.lastActiveTime = Date.now(); } checkIdle() { const now = Date.now(); if (now - this.lastActiveTime >= this.idleTimeout) { // 退出系统 this.logout(); } } handleVisibilityChange() { if (document.hidden) { // 页面隐藏,不需要做特别处理,因为定时器会被暂停,但记录隐藏时间以便后续计算 // 注意:我们不暂停定时器,而是让它自然暂停,在页面显示时立即检查 } else { // 页面再次显示,立即检查是否超时 this.checkIdle(); } } 但是,这样在隐藏期间,定时器被暂停,所以隐藏期间不会执行checkIdle。当页面再次显示时,我们立即检查一次,这样隐藏的时间也会被计入(因为lastActiveTime没有更新,而当前时间已经变大了,所以差值变大了,隐藏的时间自然算作了无操作时间)。 因此,这个方案是可行的,而且简单。 问题:如果用户刚好在隐藏期间超过了无操作时间,那么定时器不会触发,直到页面再次显示时才会触发。这符合需求。 优化:为了避免在页面显示时可能已经超时很久(比如隐藏了很长时间),我们可以在隐藏时记录时间,然后在显示时计算隐藏的时间,如果隐藏时间已经超过了无操作时间,则直接退出。但我们的checkIdle函数已经可以处理这种情况,因为lastActiveTime没有变,而当前时间变大了,所以差值会很大。 因此,按上述方案实现即可。 代码示例(Vue组件):</think>### Vue 长时间无操作退出系统解决方案(解决浏览器切换导致定时器失效) 在 Vue 应用中实现长时间无操作退出功能时,浏览器切换标签页或最小化会导致定时器暂停,这是因为现代浏览器为优化性能会限制非激活标签页的定时器执行频率[^3]。以下是完整解决方案: #### 核心方案:使用 Page Visibility API + Web Worker ```javascript // 1. 创建 Web Worker (timer.worker.js) self.addEventListener('message', (e) => { if (e.data === 'start') { let lastActive = Date.now() // 每秒检测一次 setInterval(() => { const now = Date.now() // 10分钟无操作判定 if (now - lastActive > 600000) { self.postMessage('timeout') } }, 1000) // 接收活动事件 self.addEventListener('message', (ev) => { if (ev.data === 'activity') lastActive = Date.now() }) } }) ``` ```vue // 2. Vue组件 (IdleTimeout.vue) <template> <!-- 监听用户操作的容器 --> <div @mousemove="recordActivity" @keydown="recordActivity"> <slot></slot> </div> </template> <script> export default { data() { return { worker: null, hiddenTime: null } }, mounted() { // 初始化 Worker this.worker = new Worker('/timer.worker.js') this.worker.postMessage('start') // 监听超时事件 this.worker.onmessage = (e) => { if (e.data === 'timeout') this.logout() } // 监听页面可见性变化 document.addEventListener('visibilitychange', this.handleVisibilityChange) }, methods: { recordActivity() { // 发送活动信号给 Worker this.worker?.postMessage('activity') }, handleVisibilityChange() { if (document.hidden) { // 页面隐藏时记录时间 this.hiddenTime = Date.now() } else { // 页面恢复时检测隐藏时长 if (this.hiddenTime && Date.now() - this.hiddenTime > 600000) { this.logout() } this.hiddenTime = null } }, logout() { // 执行退出逻辑 console.log('用户长时间无操作,系统退出') // 清理 Worker this.worker?.terminate() // 跳转登录页等操作... } }, beforeDestroy() { // 组件销毁时清理资源 this.worker?.terminate() document.removeEventListener('visibilitychange', this.handleVisibilityChange) } } </script> ``` #### 关键实现说明: 1. **Web Worker 解决方案**: - 在独立线程运行定时器,不受主线程休眠影响[^1] - 通过 `postMessage` 实现主线程与 Worker 通信 - 用户操作时发送 `activity` 消息重置计时 2. **Page Visibility API**: ```javascript document.addEventListener('visibilitychange', () => { if (document.hidden) { // 页面隐藏时记录时间 } else { // 页面显示时检查隐藏时长 } }) ``` 解决浏览器切换时的定时器暂停问题[^3] 3. **双重保障机制**: - Worker 持续后台计时 - 页面恢复时检查隐藏时长 - 任一条件触发即执行退出 #### 优化建议: 1. **超时时间配置化**: ```javascript // 通过环境变量配置 const TIMEOUT = process.env.VUE_APP_IDLE_TIMEOUT || 600000 ``` 2. **本地存储备份**: ```javascript // 页面隐藏时保存时间戳 localStorage.setItem('lastActive', Date.now()) ``` 3. **心跳检测**: ```javascript // 每5分钟发送心跳请求保持会话 setInterval(() => { axios.get('/keep-alive') }, 300000) ``` #### 浏览器兼容性处理: ```javascript // 回退方案:使用 requestAnimationFrame function fallbackTimer(callback) { let lastTime = Date.now() const loop = () => { const now = Date.now() if (now - lastTime > 1000) { callback() lastTime = now } requestAnimationFrame(loop) } loop() } ``` > **提示**:对于不支持 Web Worker 的旧浏览器(如 IE10-),可使用 `requestAnimationFrame` 实现降级方案,虽然精度较低但比 `setInterval` 更可靠[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值