背景

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

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 定义behaviorconst 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 使用 behaviorimport { 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) return

this.$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) return

      clearTimeout(timer.timerId)      timer.timerId = ''this.$store.delete(id)    },    $clearTimeout (id) {const timer = this.$store.get(id)if (!timer) return

      clearTimeout(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) return

this.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) return

        clearTimeout(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.jsmodule.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. 觉得文章不错,可以点个在看呀^_^另外欢迎留言交流~

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

qt定时器暂停与重新开始_手把手教你写个小程序定时器管理库相关推荐

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

    背景 凹凸曼是个小程序开发者,他要在小程序实现秒杀倒计时.于是他不假思索,写了以下代码: Page({init: function () {clearInterval(this.timer)this. ...

  2. 另一个小程序 返回的支付结果如何得到_手把手教你测微信小程序

    WeTest 导读 在小程序持续大量爆发的形势下,现在已经成为了各平台竞争的战略布局重点.至今年2月,月活超500万的微信小程序已经达到237个,其中个人开发占比高达2成.因小程序的开发门槛低.传播快 ...

  3. 手把手教你开发微信小程序中的插件

    继上次 手把手教你实现微信小程序中的自定义组件 已经有一段时间了(不了解的小伙伴建议去看看,因为插件很多内容跟组件相似),今年3月13日,小程序新增了 小程序**「插件」 功能,以及开发者工具新增 「 ...

  4. python k线合成_手把手教你写一个Python版的K线合成函数

    手把手教你写一个Python版的K线合成函数 在编写.使用策略时,经常会使用一些不常用的K线周期数据.然而交易所.数据源又没有提供这些周期的数据.只能通过使用已有周期的数据进行合成.合成算法已经有一个 ...

  5. python网络爬虫网易云音乐_手把手教你写网络爬虫(1):网易云音乐歌单

    大家好,<手把手教你写网络爬虫>连载开始了!在笔者的职业生涯中,几乎没有发现像网络爬虫这样的编程实践,可以同时吸引程序员和门外汉的注意.本文由浅入深的把爬虫技术和盘托出,为初学者提供一种轻 ...

  6. windows脚本编制引擎_手把手教你写脚本引擎(一)

    手把手教你写脚本引擎(一)--挑选语言的特性 陈梓瀚 华南理工大学软件本科05级 脚本引擎的作用在于增强程序的可配置性.从游戏到管理系统都需要脚本,甚至连工业级产品的Office.3DS Max以及A ...

  7. 永磁同步电机驱动视频教程_矢量控制_手把手教你写代码_无感FOC_有感FOC_状态观测器_卡尔曼滤波_慧驱动

    手把手教你驱动永磁同步电机_视频教程 前言 大家在刚开始搞永磁同步电机控制的时候,大部分都是先接触的芯片厂商提供的方案,然后查资料,买芯片厂商的电机套件,买回来后,通电启动,电机顺利的转起来了,然后再 ...

  8. 手把手教你进行微信小程序开发案例1---计算器

    由于之前的文章中已经教会了大家如何注册自己的一个微信小程序,并且利用微信开发工具进行小程序的开发,所以这里不再介绍如何下载工具和注册账号,不懂的小伙伴们可以观看我之前发过的教程哦. #####下面我将 ...

  9. 手把手教你反编译小程序

    本次实验环境 操作系统: win10 10.0.19042 node: v14.17.0 微信开发者工具: Stable 1.05.2110290 前期准备 在电脑端安装模拟器工具,这里以夜神模拟器为 ...

最新文章

  1. 经典排序算法python回顾之一 交换排序
  2. java基础第十一篇之Date、Math、自动装箱和拆箱
  3. blackarch 安装美化等
  4. Ubuntu设置环境变量
  5. union的作用 c语言,C语言(union类型及应用)
  6. Unity3D开发技巧:如何避开unity编辑器的那些坑
  7. php 匿名评论,关于php:PHP匿名类的用法
  8. LNAMP 中的PHP探针
  9. AcWing基础算法课Level-2 第三讲 搜索与图论
  10. (3)通过输入参数(测量数据)构建三维体模型(02)
  11. python基于Flask构建Web服务,解决Flask数据请求中的跨域问题
  12. sbt启动机制、配置优化及与Intellij IDEA的集成
  13. 软件工程-第三章-需求分析
  14. 你为什么要去博物馆? 我的理由比较另类
  15. [Python数据分析]NBA的球星们喜欢在哪个位置出手
  16. 二阶系统响应指标图_二阶系统的脉冲响应.ppt
  17. 测试开发之Python核心笔记(7):输入与输出
  18. 小i机器人软件工程师揭秘机器人的“脑细胞”NLU
  19. pb11.5的使用体会
  20. Web:选择器的种类

热门文章

  1. php中 s=,PHP错误表中的所有值=’s’
  2. python中argsort_numpy的argsort函数
  3. python 运维包_python运维常用模块
  4. python机器学习彩票_Python机器学习及实战kaggle从零到竞赛PDF电子版分享
  5. java对象前后改变_java对象改变而不设置它们
  6. htmlcss面试笔记
  7. springmvc整合dubbo
  8. 多线程跑调度_java多线程中的调度策略
  9. python3虚拟环境不带任何模块_Python3虚拟环境-不存在的包
  10. WebLogic命令行远程部署