手把手教你写个小程序定时器管理库

背景

凹凸曼是个小程序开发者,他要在小程序实现秒杀倒计时。于是他不假思索,写了以下代码:

Page({init: function () {clearInterval(this.timer)this.timer = setInterval(() => {// 倒计时计算逻辑console.log('setInterval')})},
})

可是,凹凸曼发现页面隐藏在后台时,定时器还在不断运行。于是凹凸曼优化了一下,在页面展示的时候运行,隐藏的时候就暂停。

Page({onShow: function () {if (this.timer) {this.timer = setInterval(() => {// 倒计时计算逻辑console.log('setInterval')})}},onHide: function () {clearInterval(this.timer)},init: function () {clearInterval(this.timer)this.timer = setInterval(() => {// 倒计时计算逻辑console.log('setInterval')})},
})

问题看起来已经解决了,就在凹凸曼开心地搓搓小手暗暗欢喜时,突然发现小程序页面销毁时是不一定会调用 onHide 函数的,这样定时器不就没法清理了?那可是会造成内存泄漏的。凹凸曼想了想,其实问题不难解决,在页面 onUnload 的时候也清理一遍定时器就可以了。

Page({...onUnload: function () {clearInterval(this.timer)},
})

这下问题都解决了,但我们可以发现,在小程序使用定时器需要很谨慎,一不小心就会造成内存泄漏。 后台的定时器积累得越多,小程序就越卡,耗电量也越大,最终导致程序卡死甚至崩溃。特别是团队开发的项目,很难确保每个成员都正确清理了定时器。因此,写一个定时器管理库来管理定时器的生命周期,将大有裨益。

思路整理

首先,我们先设计定时器的 API 规范,肯定是越接近原生 API 越好,这样开发者可以无痛替换。

function $setTimeout(fn, timeout, ...arg) {}
function $setInterval(fn, timeout, ...arg) {}
function $clearTimeout(id) {}
function $clearInterval(id) {}

接下来我们主要解决以下两个问题

  1. 如何实现定时器暂停和恢复

  2. 如何让开发者无须在生命周期函数处理定时器

如何实现定时器暂停和恢复

思路如下:

  1. 将定时器函数参数保存,恢复定时器时重新创建

  2. 由于重新创建定时器,定时器 ID 会不同,因此需要自定义全局唯一 ID 来标识定时器

  3. 隐藏时记录定时器剩余倒计时时间,恢复时使用剩余时间重新创建定时器

首先我们需要定义一个 Timer 类,Timer 对象会存储定时器函数参数,代码如下

class Timer {static count = 0/*** 构造函数* @param {Boolean} isInterval 是否是 setInterval* @param {Function} fn 回调函数* @param {Number} timeout 定时器执行时间间隔* @param  {...any} arg 定时器其他参数*/constructor (isInterval = false, fn = () => {}, timeout = 0, ...arg) {this.id = ++Timer.count // 定时器递增 idthis.fn = fnthis.timeout = timeoutthis.restTime = timeout // 定时器剩余计时时间this.isInterval = isIntervalthis.arg = arg}}// 创建定时器function $setTimeout(fn, timeout, ...arg) {const timer = new Timer(false, fn, timeout, arg)return timer.id}

接下来,我们来实现定时器的暂停和恢复,实现思路如下:

  1. 启动定时器,调用原生 API 创建定时器并记录下开始计时时间戳。

  2. 暂停定时器,清除定时器并计算该周期计时剩余时间。

  3. 恢复定时器,重新记录开始计时时间戳,并使用剩余时间创建定时器。

代码如下:

class Timer {constructor (isInterval = false, fn = () => {}, timeout = 0, ...arg) {this.id = ++Timer.count // 定时器递增 idthis.fn = fnthis.timeout = timeoutthis.restTime = timeout // 定时器剩余计时时间this.isInterval = isIntervalthis.arg = arg}/*** 启动或恢复定时器*/start() {this.startTime = +new Date()if (this.isInterval) {/* setInterval */const cb = (...arg) => {this.fn(...arg)/* timerId 为空表示被 clearInterval */if (this.timerId) this.timerId = setTimeout(cb, this.timeout, ...this.arg)}this.timerId = setTimeout(cb, this.restTime, ...this.arg)return}/* setTimeout  */const cb = (...arg) => {this.fn(...arg)}this.timerId = setTimeout(cb, this.restTime, ...this.arg)}/* 暂停定时器 */suspend () {if (this.timeout > 0) {const now = +new Date()const nextRestTime = this.restTime - (now - this.startTime)const intervalRestTime = nextRestTime >=0 ? nextRestTime : this.timeout - (Math.abs(nextRestTime) % this.timeout)this.restTime = this.isInterval ? intervalRestTime : nextRestTime}clearTimeout(this.timerId)}
}

其中,有几个关键点需要提示一下:

  1. 恢复定时器时,实际上我们是重新创建了一个定时器,如果直接用 setTimeout 返回的 ID 返回给开发者,开发者要 clearTimeout,这时候是清除不了的。因此需要在创建 Timer 对象时内部定义一个全局唯一 ID this.id = ++Timer.count,将该 ID 返回给 开发者。开发者 clearTimeout 时,我们再根据该 ID 去查找真实的定时器 ID (this.timerId)。

  2. 计时剩余时间,timeout = 0 时不必计算;timeout > 0 时,需要区分是 setInterval 还是 setTimeout,setInterval 因为有周期循环,因此需要对时间间隔进行取余。

  3. setInterval 通过在回调函数末尾调用 setTimeout 实现,清除定时器时,要在定时器增加一个标示位(this.timeId = "")表示被清除,防止死循环。

我们通过实现 Timer 类完成了定时器的暂停和恢复功能,接下来我们需要将定时器的暂停和恢复功能跟组件或页面的生命周期结合起来,最好是抽离成公共可复用的代码,让开发者无须在生命周期函数处理定时器。翻阅小程序官方文档,发现 Behavior 是个不错的选择。

Behavior

behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 "mixins" 或 "traits"。 每个 behavior 可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behavior,behavior 也可以引用其他 behavior 。

// behavior.js 定义behavior
const TimerBehavior = Behavior({pageLifetimes: {show () { console.log('show') },hide () { console.log('hide') }},created: function () { console.log('created')},detached: function() { console.log('detached') }
})export { TimerBehavior }// component.js 使用 behavior
import { TimerBehavior } from '../behavior.js'Component({behaviors: [TimerBehavior],created: function () {console.log('[my-component] created')},attached: function () {console.log('[my-component] attached')}
})

如上面的例子,组件使用 TimerBehavior 后,组件初始化过程中,会依次调用 TimerBehavior.created() => Component.created() => TimerBehavior.show()。 因此,我们只需要在 TimerBehavior 生命周期内调用 Timer 对应的方法,并开放定时器的创建销毁 API 给开发者即可。 思路如下:

  1. 组件或页面创建时,新建 Map 对象来存储该组件或页面的定时器。

  2. 创建定时器时,将 Timer 对象保存在 Map 中。

  3. 定时器运行结束或清除定时器时,将 Timer 对象从 Map 移除,避免内存泄漏。

  4. 页面隐藏时将 Map 中的定时器暂停,页面重新展示时恢复 Map 中的定时器。

const TimerBehavior = Behavior({created: function () {this.$store = new Map()this.$isActive = true},detached: function() {this.$store.forEach(timer => timer.suspend())this.$isActive = false},pageLifetimes: {show () {if (this.$isActive) returnthis.$isActive = truethis.$store.forEach(timer => timer.start(this.$store))},hide () {this.$store.forEach(timer => timer.suspend())this.$isActive = false}},methods: {$setTimeout (fn = () => {}, timeout = 0, ...arg) {const timer = new Timer(false, fn, timeout, ...arg)this.$store.set(timer.id, timer)this.$isActive && timer.start(this.$store)return timer.id},$setInterval (fn = () => {}, timeout = 0, ...arg) {const timer = new Timer(true, fn, timeout, ...arg)this.$store.set(timer.id, timer)this.$isActive && timer.start(this.$store)return timer.id},$clearInterval (id) {const timer = this.$store.get(id)if (!timer) returnclearTimeout(timer.timerId)timer.timerId = ''this.$store.delete(id)},$clearTimeout (id) {const timer = this.$store.get(id)if (!timer) returnclearTimeout(timer.timerId)timer.timerId = ''this.$store.delete(id)},}
})

上面的代码有许多冗余的地方,我们可以再优化一下,单独定义一个 TimerStore 类来管理组件或页面定时器的添加、删除、恢复、暂停功能。

class TimerStore {constructor() {this.store = new Map()this.isActive = true}addTimer(timer) {this.store.set(timer.id, timer)this.isActive && timer.start(this.store)return timer.id}show() {/* 没有隐藏,不需要恢复定时器 */if (this.isActive) returnthis.isActive = truethis.store.forEach(timer => timer.start(this.store))}hide() {this.store.forEach(timer => timer.suspend())this.isActive = false}clear(id) {const timer = this.store.get(id)if (!timer) returnclearTimeout(timer.timerId)timer.timerId = ''this.store.delete(id)}
}

然后再简化一遍 TimerBehavior

const TimerBehavior = Behavior({created: function () { this.$timerStore = new TimerStore() },detached: function() { this.$timerStore.hide() },pageLifetimes: {show () { this.$timerStore.show() },hide () { this.$timerStore.hide() }},methods: {$setTimeout (fn = () => {}, timeout = 0, ...arg) {const timer = new Timer(false, fn, timeout, ...arg)return this.$timerStore.addTimer(timer)},$setInterval (fn = () => {}, timeout = 0, ...arg) {const timer = new Timer(true, fn, timeout, ...arg)return this.$timerStore.addTimer(timer)},$clearInterval (id) {this.$timerStore.clear(id)},$clearTimeout (id) {this.$timerStore.clear(id)},}
})

此外,setTimeout 创建的定时器运行结束后,为了避免内存泄漏,我们需要将定时器从 Map 中移除。稍微修改下 Timer 的 start 函数,如下:

class Timer {// 省略若干代码start(timerStore) {this.startTime = +new Date()if (this.isInterval) {/* setInterval */const cb = (...arg) => {this.fn(...arg)/* timerId 为空表示被 clearInterval */if (this.timerId) this.timerId = setTimeout(cb, this.timeout, ...this.arg)}this.timerId = setTimeout(cb, this.restTime, ...this.arg)return}/* setTimeout  */const cb = (...arg) => {this.fn(...arg)/* 运行结束,移除定时器,避免内存泄漏 */timerStore.delete(this.id)}this.timerId = setTimeout(cb, this.restTime, ...this.arg)}
}

愉快地使用

从此,把清除定时器的工作交给 TimerBehavior 管理,再也不用担心小程序越来越卡。

import { TimerBehavior } from '../behavior.js'// 在页面中使用
Page({behaviors: [TimerBehavior],onReady() {this.$setTimeout(() => {console.log('setTimeout')})this.$setInterval(() => {console.log('setTimeout')})}
})// 在组件中使用
Components({behaviors: [TimerBehavior],ready() {this.$setTimeout(() => {console.log('setTimeout')})this.$setInterval(() => {console.log('setTimeout')})}
})

npm 包支持

为了让开发者更好地使用小程序定时器管理库,我们整理了代码并发布了 npm 包供开发者使用,开发者可以通过 npm install --save timer-miniprogram 安装小程序定时器管理库,文档及完整代码详看 https://github.com/o2team/timer-miniprogram

eslint 配置

为了让团队更好地遵守定时器使用规范,我们还可以配置 eslint 增加代码提示,配置如下:

// .eslintrc.js
module.exports = {'rules': {'no-restricted-globals': ['error', {'name': 'setTimeout','message': 'Please use TimerBehavior and this.$setTimeout instead. see the link: https://github.com/o2team/timer-miniprogram'}, {'name': 'setInterval','message': 'Please use TimerBehavior and this.$setInterval instead. see the link: https://github.com/o2team/timer-miniprogram'}, {'name': 'clearInterval','message': 'Please use TimerBehavior and this.$clearInterval instead. see the link: https://github.com/o2team/timer-miniprogram'}, {'name': 'clearTimout','message': 'Please use TimerBehavior and this.$clearTimout  instead. see the link: https://github.com/o2team/timer-miniprogram'}]}
}

总结

千里之堤,溃于蚁穴。

管理不当的定时器,将一点点榨干小程序的内存和性能,最终让程序崩溃。

重视定时器管理,远离定时器泄露。

参考资料

[1]

小程序开发者文档: https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html

推荐阅读

我在阿里招前端,我该怎么帮你?(文末有福利)
如何拿下阿里巴巴 P6 的前端 Offer
如何准备阿里P6/P7前端面试--项目经历准备篇
大厂面试官常问的亮点,该如何做出?
如何从初级到专家(P4-P7)打破成长瓶颈和有效突破
若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

末尾

你好,我是若川,江湖人称菜如若川,历时一年只写了一个学习源码整体架构系列~(点击蓝字了解我)

  1. 关注我的公众号若川视野,回复"pdf" 领取前端优质书籍pdf

  2. 我的博客地址:https://lxchuan12.gitee.io 欢迎收藏

  3. 觉得文章不错,可以点个在看呀^_^另外欢迎留言交流~

小提醒:若川视野公众号面试、源码等文章合集在菜单栏中间【源码精选】按钮,欢迎点击阅读

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276443.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[New Portal]Windows Azure Virtual Machine (14) 在本地制作数据文件VHD并上传至Azure(1)

《Windows Azure Platform 系列文章目录》 之前的内容里,我介绍了如何将本地的Server 2012中文版 VHD上传至Windows Azure,并创建基于该Server 2012 VHD的虚拟机。 我们知道,VHD不仅仅可以保存操作系统,而且可以保存数据文件。 如…

python 退出程序_Python:用Ctrl+C解决终止多线程程序的问题!(建议收藏)

前言:今天为大家带来的内容是Python:用CtrlC解决终止多线程程序的问题!文章中的代码具有不错的参考意义,希望在此能够帮助到各位!(多数代码用图片的方式呈现出来,方便各位观看与收藏)出发点:前段时间&#…

若川知乎高赞:有哪些必看的 JS 库?

欢迎星标我的公众号,回复加群,长期交流学习我的知乎回答目前2w阅读量,270赞,现在发到公众号声明原创。必看的js库?只有当前阶段值不值看。我从去年7月起看一些前端库的源码,历时一年才写了八篇《学习源码整…

基于EasyUI的Web应用程序及过去一年的总结

前言 一个多月之前已经提交了离职申请,好在领导都已经批准了,过几天就办理手续了,在此感谢领导的栽培与挽留,感谢各位同事在工作中的给我的帮助,离开这个团队确实有一些不舍,不为别的,只因为这个…

快速使用Vue3最新的15个常用API

之前我写了一篇博客介绍了Vue3的新特性,简单了解了一下Vue3都有哪些特色,并且在文末带大家稍微体验了一下Vue3中 Compsition API 的简单使用上一篇文章地址:紧跟尤大的脚步提前体验Vue3新特性,你不会还没了解过Vue3吧因为这个月的…

超级马里奥代码_任天堂的源码泄露,揭示超级马里奥的前世之生

被黑客盯上的任天堂任天堂遭到了史上最大规模的黑客攻击,Wii 完整源码、设计以及《宝可梦》多部作品的信息遭到泄露,而此次泄露事件的后续影响似乎也爆发了出来。《马里奥赛车》和《超级马里奥世界2》(耀西岛)的早期原型视频,以及《超级马里奥…

漫画 | 前端发展史的江湖恩怨情仇

时间总是过得很快, 似乎快得让人忘记了昨天,前端WEB领域的发展更是如此,转眼间已是近30年,时光荏苒,初心不变,在一代又一代前端人的努力下,前端已经是互联网不可或缺的一部分。然而很多前端打工…

10 个你可能还不知道 VS Code 使用技巧

经常帮一些同学 One-on-One 地解决问题,在看部分同学使用 VS Code 的时候,有些蹩脚,实际上一些有用的技巧能够提高我们的日常工作效率。NO.1一、重构代码VS Code 提供了一些快速重构代码的操作,例如:将一整段代码提取为…

构建安全的Xml Web Service系列之如何察看SoapMessage

上一篇文章地址:构建安全的Xml Web Service系列一之初探使用Soap头 (5-22 12:53) 要分析Xml Web Service的安全性,首先要解决的问题是我们能了解和清楚Soap消息的格式和内容,如果获得不了SoapMessage,分析如何能构建安全Xml w…

前端高效开发必备的 js 库梳理

之前有很多人问学好前端需要学习哪些 js 库, 主流框架应该学 vue 还是 react ? 针对这些问题, 笔者来说说自己的看法和学习总结.首先我觉得在学习任何知识之前必须要有一个明确的学习目标, 知道自己为什么要学它, 而不是看网上说的一股脑的给你灌输各种知识, 让你学习各种库, …

交叉报表crosstab隐藏列名显示_SAP软件 报表查询之 输出格式设置

SAP不仅是功能强大、逻辑严谨的ERP软件,还提供了强大的报表查询功能。SAP的ALV报表展示功能是SAP的一大特点,实现了类似于EXCEL的功能。使用好ALV报表功能可以方便用户从SAP中取到想要的数据,尤其是财务用户。大家在使用SAP报表时&#xff0c…

seo每日一贴_白杨SEO:我看ZAC的外贸SEO应该怎样做?(策略篇)

前言:这是白杨SEO公众号更新第64篇。本该写写头条SEO啥的,最近在师徒培训讲站内SEO时有旁听同学提到后面讲讲谷歌SEO怎么样,因为谷歌全世界搜索市场占有率,所以外贸SEO最主要还是做谷歌SEO。以白杨特意又去了前辈ZAC的SEO每日一贴…

[转]网页栅格系统研究(2):蛋糕的切法

[出自]http://lifesinger.org/blog/2008/10/grid-system-2/首先澄清一个应用场景问题。研究(1)中指出,对于结构复杂的网站,不少设计师们喜欢采用960固定宽度布局。但要注意的是,960并不是万能钥匙,大部分网…

Vue3响应式原理

关注若川视野,回复"pdf" 领取资料,回复"加群",可加群长期交流学习本文结构- 关于Vue3- Vue2响应式原理回顾- Vue3响应式方案- Vue3响应式原理- 手写mini版Vue3响应式本文共计:2349字2图预计阅读时间&#xff…

找准切入点,调试看源码,事半功倍

关注若川视野,回复"pdf" 领取资料,回复"加群",可加群长期交流学习最近写了很多源码分析相关的文章,React、Vue 都有,想把我阅读源码的一些心得分享给大家。React:React 架构的演变 - 从…

Android布局大全

Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦。组件按照布局的要求依次排列,就组成了用户所看见的界面。 所有的布局方式都可以归类为ViewGroup的5个类别,即ViewGroup的5个直接子类…

java实现加减乘除运算符随机生成十道题并判断对错_2020年Java面试题(3年的工作总结),最全的知识点总结...

这份Java面试题整整花了三个月的时间来整理,都是自己再工作中总结出来,记住多少就写多少,希望这份资料可以帮助你们,文末有其余部分资料的领取方式.Redis12道面试题1.什么是Redis?答:Remote Dictionary Ser…

.NET 中的泛型 101

1.1.1 摘要 图1 C# 泛型介绍 在接触泛型之前,我们编程一般都是使用具体类型(char, int, string等)或自定义类型来定义我们变量,如果我们有一个功能很强的接口,而且我们想把它提取或重构成一个通用的接口,使…

年底了,给想进阶的的前端朋友一些福利

2020 年,很多朋友都经历了一段比较艰难的求职季。年末,“就业寒冬”迎来了一丝暖阳,很多中大型互联网公司扩大了未来一年的招聘需求。前不久,字节跳动放出了年末要招 1 万人的消息,腾讯校招规模也将扩张至 5000 人&…

Vue Router 4.0 正式发布!焕然一新。

关注若川视野,回复"pdf" 领取资料,回复"加群",可加群长期交流学习12月8日,Vue Router 4 正式发布稳定版本。在经历了 14 个 Alpha,13 个 Beta 和 6 个 RC 版本之后,Vue Router v4 闪亮…