记一次 Vue 移动端活动倒计时优化
前言
通常写倒计时效果,用的是 setInterval,但这会引发一些问题,最常见的问题就是定时器不准。
如果只是普通的动画效果,倒也无所谓,但倒计时这种需要精确到毫秒级别的,就不行了,否则活动都结束了,用户的界面上倒计时还在走,但是又参加不了活动,会被投诉的╮(╯▽╰)╭
一、 知识铺垫
1. setInterval 定时器
先说本文的主角 setInterval,MDN web doc 对其的解释是:
setInterval() 方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。
返回一个 intervalID。(可用于清除定时器)
语法: let intervalID = window.setInterval(func, delay[, param1, param2, ...]);
例:
值得注意的是,在 setInterval 里面使用 this 的话,this 指向的是 window 对象,可以通过 call、apply 等方法改变 this 指向。
setTimeout 与 setInterval 类似,只不过延迟 n 毫秒执行函数一次,且不需要手动清除。
至于 setTimeout 和 setInterval 的运行原理,就要牵扯到另一个概念: event loop (事件循环)。
2. 浏览器的 Event Loop
JavaScript 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中,若遇到异步的代码,会被挂起并加入到 task (有多种 task) 队列中。
一旦执行栈为空, event loop 就会从 task 队列中拿出需要执行的代码并放入执行栈中执行。
有了 event loop,使得 JavaScript 具备了异步编程的能力。(但本质上,还是同步行为)
先看一道经典的面试题:
console.log('Script start');setTimeout(() => {console.log('setTimeout');
}, 0);new Promise((resolve, reject) => {console.log('Promise');resolve()
}).then(() => {console.log('Promise 1');
}).then(() => {console.log('Promise 2');
});console.log('Scritp end');
复制代码
打印顺序为:
- "Script start"
- "Promise"
- "Script end"
- "Promise 1"
- "Promise 2"
- "setTimeout"
至于为什么 setTimeout 设置为 0,却在最后被打印,这就涉及到 event loop 中的微任务和宏任务了。
2.1 宏任务和微任务
不同的任务源会被分配到不同的 task 队列中,任务源可分为微任务( microtask )和宏任务( macrotask ).
在 ES6 中:
- microtask 称为 Job
- macrotask 称为 Task
macro-task(Task): 一个 event loop 有一个或者多个 task 队列。task 任务源非常宽泛,比如 ajax 的 onload,click 事件,基本上我们经常绑定的各种事件都是 task 任务源,还有数据库操作(IndexedDB ),需要注意的 是setTimeout、setInterval、setImmediate 也是 task 任务源。总结来说 task 任务源:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
micro-task(Job): microtask 队列和 task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop 里只有一个 microtask 队列。另外 microtask 执行时机和 macrotasks 也有所差异
- process.nextTick
- promises
- Object.observe
- MutationObserver
ps: 微任务并不快于宏任务
2.2 Event Loop 执行顺序
- 执行同步代码(宏任务);
- 执行栈为空,查询是否有微任务需要执行;
- 执行所有微任务;
- 必要的话渲染 UI;
- 然后开始下一轮 event loop,执行宏任务中的异步代码;
ps: 如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的界面响应,可把操作放微任务中。
setTimeout 在第一次执行时,会挂起到 task, 等待下一轮 event loop,而执行一次 event loop 最少需要 4ms,这就是为什么哪怕setTimeout(()=>{...}, 0)
都会有 4ms 的延迟。
由于 JavaScript 是单线程,所以 setInterval / setTimeout 的误差是无法被完全解决的。
可能是回调中的事件,也可能是浏览器中的各种事件导致的。
这也是为什么一个页面运行久了,定时器会不准的原因。
二、项目场景
在公司项目中遇到了倒计时的需求,但是已有前人写过组件了,因为项目时间赶,所以直接拿来用了,但使用的过程中,发现一些 Bug:
- 在某台安卓测试机上,手指滑动或者将要滑动的时候,毫秒数会停住,松开后才会继续走;
- 去到其他页面之后再回来,倒计时的分秒数不正确;
- 回到原来页面之后,重新请求数据,会导致倒计时加快;
第一个 Bug 是因为滑动阻塞了主线程,导致 macrotask 没有正常的执行。
第二个 Bug 是因为切换页面后,浏览器为了降低性能的消耗,会自动的延长之前页面定时器的间隔,导致误差越来越大。
第三个 Bug 是因为调用方法之前,没有清除定时器,导致监听时间戳的时候,又新增了定时器。
前两个 Bug 才是本文要解决的地方。
查了很多文章,大致解决方案有以下两种:
1. requestAnimationFrame()
MDN web doc 的解释如下:
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
注意: 若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
requestAnimationFrame() 的执行频率取决于浏览器屏幕的刷新率,通常的屏幕都是 60Hz 或 75Hz,也就是每秒最多只能重绘60次或75次,requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
不过要注意:requestAnimationFrame 是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame 的动画效果会大打折扣。
利用 requestAnimationFrame 可以在一定程度上替代 setInterval,不过时间间隔需要计算,按 60Hz 的屏幕刷新率( fps )来算的话,1000 / 60 = 16.6666667(ms),也就是每16.7ms执行一次,但 fps 并不是固定的,有玩过 FPS(第一人称射击游戏)的玩家会深有体会。不过相对于之前不做任何优化的 setInterval 来说,误差要比原来的小得多。
我的解决方案是,设置一个变量 then,在执行动画函数之后,记录当前时间戳,再下一次进入动画函数的时候,用 [当前时间戳] 减去 [then] ,得到时间间隔,然后让 [倒计时时间戳] 减去 [间隔],并在离开页面时记录离开时间,进一步减小误差。
<script>
export default {name: "countdown",props: {timestamp: {type: Number,default: 0}},data() {return {remainTimestamp: 0then: 0};},activated () {window.requestAnimationFrame(this.animation);},deactivated() {this.then = Date.now();},methods: {animation(tms) {if (this.remainTimestamp > 0 && this.then) {this.remainTimestamp -= (tms - this.then); // 减去当前与上一次执行的间隔this.then = tms; // 记录本次执行的时间window.requestAnimationFrame(this.animation);}}},watch: {timestamp(val) {this.remainTimestamp = val;this.then = Date.now();window.requestAnimationFrame(this.animation);}}
};
</script>
复制代码
requestAnimationFrame 在使用过程中和 setInterval 还是有区别的,最大的区别就是不能自定义间隔时间。
如果倒计时只需要精确到秒,那么 1000ms 内执行 16.7 次对性能有点过于浪费了。而如果要模拟 setInterval ,还需要额外的变量去处理间隔,也降低了代码的可读性。
因此就继续尝试第二种方案: Web Worker。
2. Web Worker
Web Worker 是 JavaScript 实现多线程的黑科技,在阮一峰博客的解释如下:
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
具体教程可以看 阮一峰的博客 和 MDN - 使用 Web Workers ,不再赘述。
但是要在 Vue 项目中使用 Web Worker 的话,还是需要一番折腾的。
首先是文件载入,官方的例子是这样的:
var myWorker = new Worker('worker.js');
由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。
因此,我们就不能直接用 import 引入,否则会找不到文件,遂 Google 之,发现有两种解决方案;
2.1 vue-worker
这是 simple-web-worker 的作者针对 Vue 项目编写的插件,它可以通过像 Promise 那样调用函数。
Github地址: vue-worker
但是在使用过程中发现一些问题,那就是 setInterval 并不会执行:
传入的 val 是倒计时剩余的时间戳,但是运行发现,return 出去的 val 并没有改变,也就是 setInterval 并没有执行。理论上 Web Worker 会保留 setInterval 的。(可能是我的姿势有问题?去提了 issues,现在还是没有人答复,有大佬指教吗?)
倒计时最核心的 setInterval 无法执行,因此弃用此插件,执行 Plan B。
2.2 worker-loader
这是和 babel-loader 类似的 JavaScript 文件转义插件,具体使用已经有大神总结了,就不再赘述:
怎么在 ES6+Webpack 下使用 Web Worker
直接贴代码:
timer.worker.js:
self.onmessage = function(e) {let time = e.data.value;const timer = setInterval(() => {time -= 71;if(time > 0) {self.postMessage({value: time});} else {clearInterval(timer);self.postMessage({value: 0});self.close();}}, 71)
};
复制代码
countdown.vue:
<script>
import Worker from './timer.worker.js'
export default {name: "countdown",props: {timestamp: {type: Number,default: 0}},data() {return {remainTimestamp: 0};},beforeDestroy () {this.worker = null;},methods: {setTimer(val) {this.worker = new Worker();this.worker.postMessage({value: val});const that = this;this.worker.onmessage = function(e) {that.remainTimestamp = e.data.value;}}},watch: {timestamp(val) {this.worker = null;this.setTimer(val);}}
};
</script>
复制代码
这里出现了一个小插曲,本地运行的时候没问题,但是打包的时候报错,排查原因是把 worker-loader 的 rules 写在了 babel-loader 的后面,结果先匹配的 .js 文件,直接把 .worker.js 用 babel-loader 处理了,导致 worker 没能引入成功,打包报错:
webpack.base.conf.js (公司项目比较老,没有使用 webpack 4.0+ 的配置方式,不过原理是一样的)
module: {rules: [{test: /\.vue$/,loader: 'vue-loader',options: {vueLoaderConfig,postcss: [require('autoprefixer')({browsers: ['last 10 Chrome versions', 'last 5 Firefox versions', 'Safari >= 6', 'ie > 8']})]}},{// 匹配的需要写在前面,否则会打包报错test: /\.worker\.js$/,loader: 'worker-loader',include: resolve('src'),options: {inline: true, // 将 worker 内联为一个 BLOBfallback: false, // 禁用 chunkname: '[name]:[hash:8].js'}},{test: /\.js$/,loader: 'babel-loader',include: [utils.resolve('src'), utils.resolve('test')]},// ...]},
复制代码
三、总结
经过一番折腾,对浏览器的 event loop 又加深了理解,不只是 setInterval 这样的定时器任务 ,其他高密集的计算也可以利用多线程去处理,不过要注意处理完毕后关闭线程,否则会严重消耗资源。 不过普通的动画还是尽量用 requestAnimationFrame 或者 CSS 动画来完成,尽可能的提高页面的流畅度。
第一次写技术博客,才疏学浅,难免有遗漏之处,如果还有更好的倒计时解决方案,欢迎各位大佬指教。
参考资料:
- 浏览器事件循环机制
- Web Worker 使用教程 - 阮一峰
- worker-loader 官方文档
- 怎么在 ES6+Webpack 下使用 Web Worker
转载于:https://juejin.im/post/5cb5858ce51d456e51614a81
记一次 Vue 移动端活动倒计时优化相关推荐
- vue\uniapp自定义活动倒计时组件
vue\uniapp自定义活动倒计时组件 效果 调用组件时传递的 timeData的属性type_id:名称,begin_time:开始时间(时间戳)end_time:结束时间(时间戳) 组件代码 & ...
- 基于Vue.js活动倒计时组件
vue2-countdown vue活动倒计时组件及遇到的坑 基于vue2.x的活动倒计时组件 主要是最近为了公司做一个倒计时活动才找到了这个组件使用的.于是去github上翻看了文档结果是一年多没更 ...
- Vue活动倒计时的功能
Vue的活动倒计时功能 话不多说,直接上代码吧,我是前端小白一枚,我搬过的砖给大家共享啦~ 欢迎大佬批评指教~ 第一步: 创建组件: <template><div class ...
- 记 vue 移动端开发 中的经验
项目背景 手上的 vue移动端 项目已经开发了大几个月了,遇到了一些很有意思的坑,也让自己学习了很多:写此文主要目的是记下一些我遇到的坑,以及自己的解决方案,分享的同时也方便以后复习. 项目的底层是上 ...
- 活动倒计时的一些想法
经常会遇到秒杀或者活动倒计时的需求,这个应该是工作过程中经常能遇到的. 不可避免的,会产生一些深入思考的行为.这篇笔记就记一下吧. 场景: 后台返回本次活动参与活动的产品的所有倒计时信息以及当前服务器 ...
- 原生JS活动倒计时实现思路
原生JS活动倒计时实现思路 由于一个活动页面里面有多个活动,所以用map去操作每一个对象,只有一个活动的话就不需要遍历了,活动分为距离活动开始和距离活动结束两个倒计时,自己可按需求增减代码.还有就是I ...
- 5 款最棒的 Vue 移动端 UI 组件库 - 特别针对国内使用场景推荐
本文完整版:<最棒的 Vue 移动端 UI 组件库 - 特别针对国内使用场景推荐> Vue 移动端 UI 组件库推荐 Vant 3 - 有赞移动 UI 组件库,支持 Vue 3 微信小程序 ...
- vue移动端项目经验
如何实现横向滚动(兼容safari,微信,浏览器) 实现横向滚动需要以下几点: 1.设置width:2000px这种大的宽度 2.父级盒子要overflow-y:hidden;overflow-x:a ...
- Vue移动端----页面旋转进入特效功能实现
[Vue移动端]页面进入特效实现–翻转 白云悠悠 苍狗悠悠 茶水一杯 温润入喉 本来今日正在浏览csdn 学习学习Koa 秃然微信 " ding" 嚯?什么情况?先瞅瞅 CSS?这 ...
最新文章
- 二叉树:二叉搜索树的创建和插入
- 第二课.PyTorch入门
- 【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十二:串口模块① — 发送
- 多项式的求逆、取模和多点求值学习小记
- WPF制作的一个小功能,智能提示(IntelliSense)
- javascript进阶教程第二章对象案例实战
- 不学好css模型的怎么入山门?师傅与徒弟的真实独白
- Qt+ArcGIS Engine 10.1 开发(一)
- 天津大学计算机预推免机试_2019预推免汇总 | 9.18New!
- 调用GPU进行神经网络的训练 GPU环境的搭建
- L2-022. 重排链表(双队列)
- [图文详解]图像处理中的高斯模糊
- 莱斯康混响插件合集 – Lexicon PCM LXP MPX Native Reverb WiN
- [打印管理器]读取样式列表失败:Invalid variant operation
- jq怎么获取值与下拉框怎么获取值
- 启用mysql系统找不到指定的文件类型_net start mysql 发生系统错误2 系统找不到指定的文件...
- (三星Samsung笔记本)误删efi分区后重装WIN10
- 【全局面包屑导航】依据路由动态生成面包屑导航
- 操作系统原理——(1)引言:计算机系统和操作系统概述
- Parental Hopes and Personal Ideals
热门文章
- 全国高等学校计算机等级考试(江西考区)一级笔试试卷a,全国高等学校计算机等级考试(江西考区)一级笔试试卷A...
- baidumap vue 判断范围_百度地图 vue-baidu-map
- 广东外语外贸大学计算机考研,广东外语外贸考研难度,2021考研广东外语外贸大学MTI会挤破头很难吗?...
- java weblogic连接池,Weblogic JNDI 方式连接连接池 (工作中遇到的问题)
- 万用表怎么测量电池容量_家电常识丨万用表的测量应用学习
- “美登杯”上海市高校大学生程序设计赛B. 小花梨的三角形(模拟,实现)
- 牛客假日团队赛6 D	迷路的牛 (思维)
- python3安装后无法使用退格键的问题
- python中的__name__=='__main__'如何简单理解(一)
- POJ 2315:Football Game(博弈论)