知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營銷的便利,運(yùn)營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
您當(dāng)前位置>首頁 » 新聞資訊 » 小程序相關(guān) >
手把手教你寫個(gè)小程序定時(shí)器管理庫
發(fā)表時(shí)間:2021-1-5
發(fā)布人:葵宇科技
瀏覽次數(shù):58
背景
凹凸曼是個(gè)小程序開發(fā)者,他要在小程序?qū)崿F(xiàn)秒殺倒計(jì)時(shí)。于是他不假思索,寫了以下代碼:
Page({
init: function () {
clearInterval(this.timer)
this.timer = setInterval(() => {
// 倒計(jì)時(shí)計(jì)算邏輯
console.log('setInterval')
})
},
})
可是,凹凸曼發(fā)現(xiàn)頁面隱藏在后臺(tái)時(shí),定時(shí)器還在不斷運(yùn)行。于是凹凸曼優(yōu)化了一下,在頁面展示的時(shí)候運(yùn)行,隱藏的時(shí)候就暫停。
Page({
onShow: function () {
if (this.timer) {
this.timer = setInterval(() => {
// 倒計(jì)時(shí)計(jì)算邏輯
console.log('setInterval')
})
}
},
onHide: function () {
clearInterval(this.timer)
},
init: function () {
clearInterval(this.timer)
this.timer = setInterval(() => {
// 倒計(jì)時(shí)計(jì)算邏輯
console.log('setInterval')
})
},
})
問題看起來已經(jīng)解決了,就在凹凸曼開心地搓搓小手暗暗歡喜時(shí),突然發(fā)現(xiàn)小程序頁面銷毀時(shí)是不一定會(huì)調(diào)用 onHide 函數(shù)的,這樣定時(shí)器不就沒法清理了?那可是會(huì)造成內(nèi)存泄漏的。凹凸曼想了想,其實(shí)問題不難解決,在頁面 onUnload 的時(shí)候也清理一遍定時(shí)器就可以了。
Page({
...
onUnload: function () {
clearInterval(this.timer)
},
})
這下問題都解決了,但我們可以發(fā)現(xiàn),在小程序使用定時(shí)器需要很謹(jǐn)慎,一不小心就會(huì)造成內(nèi)存泄漏。后臺(tái)的定時(shí)器積累得越多,小程序就越卡,耗電量也越大,最終導(dǎo)致程序卡死甚至崩潰。特別是團(tuán)隊(duì)開發(fā)的項(xiàng)目,很難確保每個(gè)成員都正確清理了定時(shí)器。因此,寫一個(gè)定時(shí)器管理庫來管理定時(shí)器的生命周期,將大有裨益。
思路整理
首先,我們先設(shè)計(jì)定時(shí)器的 API 規(guī)范,肯定是越接近原生 API 越好,這樣開發(fā)者可以無痛替換。
function $setTimeout(fn, timeout, ...arg) {}
function $setInterval(fn, timeout, ...arg) {}
function $clearTimeout(id) {}
function $clearInterval(id) {}
接下來我們主要解決以下兩個(gè)問題
-
如何實(shí)現(xiàn)定時(shí)器暫停和恢復(fù)
-
如何讓開發(fā)者無須在生命周期函數(shù)處理定時(shí)器
如何實(shí)現(xiàn)定時(shí)器暫停和恢復(fù)
思路如下:
-
將定時(shí)器函數(shù)參數(shù)保存,恢復(fù)定時(shí)器時(shí)重新創(chuàng)建
-
由于重新創(chuàng)建定時(shí)器,定時(shí)器 ID 會(huì)不同,因此需要自定義全局唯一 ID 來標(biāo)識(shí)定時(shí)器
-
隱藏時(shí)記錄定時(shí)器剩余倒計(jì)時(shí)時(shí)間,恢復(fù)時(shí)使用剩余時(shí)間重新創(chuàng)建定時(shí)器
首先我們需要定義一個(gè) Timer 類,Timer 對(duì)象會(huì)存儲(chǔ)定時(shí)器函數(shù)參數(shù),代碼如下
class Timer {
static count = 0
/**
* 構(gòu)造函數(shù)
* @param {Boolean} isInterval 是否是 setInterval
* @param {Function} fn 回調(diào)函數(shù)
* @param {Number} timeout 定時(shí)器執(zhí)行時(shí)間間隔
* @param {...any} arg 定時(shí)器其他參數(shù)
*/
constructor (isInterval = false, fn = () => {}, timeout = 0, ...arg) {
this.id = ++Timer.count // 定時(shí)器遞增 id
this.fn = fn
this.timeout = timeout
this.restTime = timeout // 定時(shí)器剩余計(jì)時(shí)時(shí)間
this.isInterval = isInterval
this.arg = arg
}
}
// 創(chuàng)建定時(shí)器
function $setTimeout(fn, timeout, ...arg) {
const timer = new Timer(false, fn, timeout, arg)
return timer.id
}
接下來,我們來實(shí)現(xiàn)定時(shí)器的暫停和恢復(fù),實(shí)現(xiàn)思路如下:
-
啟動(dòng)定時(shí)器,調(diào)用原生 API 創(chuàng)建定時(shí)器并記錄下開始計(jì)時(shí)時(shí)間戳。
-
暫停定時(shí)器,清除定時(shí)器并計(jì)算該周期計(jì)時(shí)剩余時(shí)間。
-
恢復(fù)定時(shí)器,重新記錄開始計(jì)時(shí)時(shí)間戳,并使用剩余時(shí)間創(chuàng)建定時(shí)器。
代碼如下:
class Timer {
constructor (isInterval = false, fn = () => {}, timeout = 0, ...arg) {
this.id = ++Timer.count // 定時(shí)器遞增 id
this.fn = fn
this.timeout = timeout
this.restTime = timeout // 定時(shí)器剩余計(jì)時(shí)時(shí)間
this.isInterval = isInterval
this.arg = arg
}
/**
* 啟動(dòng)或恢復(fù)定時(shí)器
*/
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)
}
/* 暫停定時(shí)器 */
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)
}
}
其中,有幾個(gè)關(guān)鍵點(diǎn)需要提示一下:
-
恢復(fù)定時(shí)器時(shí),實(shí)際上我們是重新創(chuàng)建了一個(gè)定時(shí)器,如果直接用 setTimeout 返回的 ID 返回給開發(fā)者,開發(fā)者要 clearTimeout,這時(shí)候是清除不了的。因此需要在創(chuàng)建 Timer 對(duì)象時(shí)內(nèi)部定義一個(gè)全局唯一 ID
this.id = ++Timer.count
,將該 ID 返回給 開發(fā)者。開發(fā)者 clearTimeout 時(shí),我們?cè)俑鶕?jù)該 ID 去查找真實(shí)的定時(shí)器 ID (this.timerId)。 -
計(jì)時(shí)剩余時(shí)間,timeout = 0 時(shí)不必計(jì)算;timeout > 0 時(shí),需要區(qū)分是 setInterval 還是 setTimeout,setInterval 因?yàn)橛兄芷谘h(huán),因此需要對(duì)時(shí)間間隔進(jìn)行取余。
-
setInterval 通過在回調(diào)函數(shù)末尾調(diào)用 setTimeout 實(shí)現(xiàn),清除定時(shí)器時(shí),要在定時(shí)器增加一個(gè)標(biāo)示位(this.timeId = "")表示被清除,防止死循環(huán)。
我們通過實(shí)現(xiàn) Timer 類完成了定時(shí)器的暫停和恢復(fù)功能,接下來我們需要將定時(shí)器的暫停和恢復(fù)功能跟組件或頁面的生命周期結(jié)合起來,最好是抽離成公共可復(fù)用的代碼,讓開發(fā)者無須在生命周期函數(shù)處理定時(shí)器。翻閱小程序官方文檔,發(fā)現(xiàn) Behavior 是個(gè)不錯(cuò)的選擇。
Behavior
behaviors 是用于組件間代碼共享的特性,類似于一些編程語言中的 "mixins" 或 "traits"。每個(gè) behavior 可以包含一組屬性、數(shù)據(jù)、生命周期函數(shù)和方法,組件引用它時(shí),它的屬性、數(shù)據(jù)和方法會(huì)被合并到組件中,生命周期函數(shù)也會(huì)在對(duì)應(yīng)時(shí)機(jī)被調(diào)用。每個(gè)組件可以引用多個(gè) 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 后,組件初始化過程中,會(huì)依次調(diào)用 TimerBehavior.created() => Component.created() => TimerBehavior.show()
。因此,我們只需要在 TimerBehavior 生命周期內(nèi)調(diào)用 Timer 對(duì)應(yīng)的方法,并開放定時(shí)器的創(chuàng)建銷毀 API 給開發(fā)者即可。思路如下:
-
組件或頁面創(chuàng)建時(shí),新建 Map 對(duì)象來存儲(chǔ)該組件或頁面的定時(shí)器。
-
創(chuàng)建定時(shí)器時(shí),將 Timer 對(duì)象保存在 Map 中。
-
定時(shí)器運(yùn)行結(jié)束或清除定時(shí)器時(shí),將 Timer 對(duì)象從 Map 移除,避免內(nèi)存泄漏。
-
頁面隱藏時(shí)將 Map 中的定時(shí)器暫停,頁面重新展示時(shí)恢復(fù) Map 中的定時(shí)器。
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 = true
this.$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)
},
}
})
上面的代碼有許多冗余的地方,我們可以再優(yōu)化一下,單獨(dú)定義一個(gè) TimerStore 類來管理組件或頁面定時(shí)器的添加、刪除、恢復(fù)、暫停功能。
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() {
/* 沒有隱藏,不需要恢復(fù)定時(shí)器 */
if (this.isActive) return
this.isActive = true
this.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)
}
}
然后再簡(jiǎn)化一遍 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 創(chuàng)建的定時(shí)器運(yùn)行結(jié)束后,為了避免內(nèi)存泄漏,我們需要將定時(shí)器從 Map 中移除。稍微修改下 Timer 的 start 函數(shù),如下:
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)
/* 運(yùn)行結(jié)束,移除定時(shí)器,避免內(nèi)存泄漏 */
timerStore.delete(this.id)
}
this.timerId = setTimeout(cb, this.restTime, ...this.arg)
}
}
愉快地使用
從此,把清除定時(shí)器的工作交給 TimerBehavior 管理,再也不用擔(dān)心小程序越來越卡。
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 包支持
為了讓開發(fā)者更好地使用小程序定時(shí)器管理庫,我們整理了代碼并發(fā)布了 npm 包供開發(fā)者使用,開發(fā)者可以通過 npm install --save timer-miniprogram
安裝小程序定時(shí)器管理庫,文檔及完整代碼詳看 https://github.com/o2team/timer-miniprogram
eslint 配置
為了讓團(tuán)隊(duì)更好地遵守定時(shí)器使用規(guī)范,我們還可以配置 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'
}]
}
}
總結(jié)
千里之堤,潰于蟻穴。
管理不當(dāng)?shù)亩〞r(shí)器,將一點(diǎn)點(diǎn)榨干小程序的內(nèi)存和性能,最終讓程序崩潰。
重視定時(shí)器管理,遠(yuǎn)離定時(shí)器泄露。
相關(guān)案例查看更多
相關(guān)閱讀
- 云南科技公司
- 云南網(wǎng)站制作哪家好
- 云南網(wǎng)站建設(shè)百度官方
- 百度推廣
- 云南軟件開發(fā)
- 報(bào)廢車回收管理系統(tǒng)
- 微信分銷系統(tǒng)
- 昆明做網(wǎng)站
- 云南etc微信小程序
- 云南小程序開發(fā)哪家好
- 網(wǎng)站建設(shè)招商
- 昆明做網(wǎng)站建設(shè)的公司排名
- 人人商城
- 小程序開發(fā)費(fèi)用
- 國內(nèi)知名網(wǎng)站建設(shè)公司排名
- 微信分銷
- 汽車報(bào)廢回收管理軟件
- web
- 昆明網(wǎng)絡(luò)公司
- 云南網(wǎng)站建設(shè)公司哪家好
- 小程序表單
- 云南網(wǎng)絡(luò)公司
- 網(wǎng)站建設(shè)費(fèi)用
- 保險(xiǎn)網(wǎng)站建設(shè)公司
- 全國前十名小程序開發(fā)公司
- 大理網(wǎng)站建設(shè)公司
- 大理小程序開發(fā)
- 小程序
- 報(bào)廢車拆解管理系統(tǒng)
- 網(wǎng)站排名優(yōu)化