Author: Gu Gu Man – A Ji
background
Concave-convex man is a small program developer, he wants to achieve second kill countdown in the small program. Without thinking, he wrote the following code:
Page({
init: function () {
clearInterval(this.timer)
this.timer = setInterval((a)= > {
// Counting logic
console.log('setInterval')})}})Copy the code
However, concavity man found page hidden in the background, timer is still running. So concave-convex man optimization, running when the page is displayed, hidden when suspended.
Page({
onShow: function () {
if (this.timer) {
this.timer = setInterval((a)= > {
// Counting logic
console.log('setInterval')})}},onHide: function () {
clearInterval(this.timer)
},
init: function () {
clearInterval(this.timer)
this.timer = setInterval((a)= > {
// Counting logic
console.log('setInterval')})}})Copy the code
The problem seems to have been solved, in concaveman happily rub his hands secretly happy, suddenly found that the small program page destruction is not necessarily called onHide function, so the timer can not clean up? That could cause a memory leak. Concavity man thought, in fact, the problem is not difficult to solve, when the page onUnload also clean up a timer can be.
Page({
...
onUnload: function () {
clearInterval(this.timer)
},
})
Copy the code
This solved all the problems, but we can see that using timers in small programs needs to be very careful and can cause memory leaks if you are not careful. The more timers in the background accumulate, the more small programs get stuck and consume more power, eventually causing the program to freeze or even crash. Especially on team projects, it can be difficult to ensure that everyone cleans up timers correctly. Therefore, it is of great benefit to write a timer management library to manage the life cycle of timers.
Train of thought to sort out
First, we designed the timer API specification as close to the native API as possible so that developers could painlessly replace it.
function $setTimeout(fn, timeout, ... arg) {}function $setInterval(fn, timeout, ... arg) {}function $clearTimeout(id) {}
function $clearInterval(id) {}
Copy the code
Next, we will mainly solve the following two problems
- How to realize timer pause and resume
- How to keep developers from dealing with timers in life cycle functions
How to realize timer pause and resume
Here’s the idea:
- The timer function parameters are saved and recreated when the timer is restored
- Because a timer is re-created, the TIMER ID will be different. Therefore, you need to define a global unique ID to identify the timer
- The remaining countdown time of the timer is recorded when hiding, and the remaining time is used to recreate the timer when recovering
First we need to define a Timer class. The Timer object will store the Timer function parameters as follows
class Timer {
static count = 0
/** * constructor * @param {Boolean} isInterval whether setInterval * @param {Function} fn callback Function * @param {Number} timeout Timer execution interval * @param {... Any} Other arG timer parameters */
constructor (isInterval = false, fn = () => {}, timeout = 0. arg) {this.id = ++Timer.count // The timer increments the ID
this.fn = fn
this.timeout = timeout
this.restTime = timeout // Remaining time of timer
this.isInterval = isInterval
this.arg = arg
}
}
// Create a timer
function $setTimeout(fn, timeout, ... arg) {
const timer = new Timer(false, fn, timeout, arg)
return timer.id
}
Copy the code
Next, we will realize the pause and recovery of the timer, the realization of the idea is as follows:
- Start the timer, call the native API to create the timer and record the start time stamp.
- Pause the timer, clear the timer and calculate the remaining time of the cycle.
- Restore the timer, re-record the start time stamp, and use the remaining time to create the timer.
The code is as follows:
class Timer {
constructor (isInterval = false, fn = () => {}, timeout = 0, ... Arg) {this.id = ++ timer.count // Timer increment ID this.fn = fn this.timeout = timeout this.restTime = timeout // Remaining time of the Timer This. isInterval = isInterval this.arg = arg} /** * Start or resume the timer */start() {
this.startTime = +new Date()
if (this.isInterval) {
/* setInterval */ const cb = (... arg) => { this.fn(... Arg) /* If timerId is empty, clearInterval */ is usedif (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)} /* Pause timer */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)
}
}
Copy the code
There are a few key points to note:
- If the ID returned by setTimeout is returned directly to the developer, the developer will need clearTimeout, which will not be cleared. Therefore, you need to internally define a globally unique ID when creating the Timer object
this.id = ++Timer.count
, returns the ID to the developer. When the developer clearTimeout, we then use this ID to find the real timerId (this.timerid). - Time remaining, timeout = 0 need not calculate; When timeout > 0, you need to distinguish between setInterval and setTimeout. Because setInterval has a cycle, you need to mod the interval.
- The setInterval is implemented by calling setTimeout at the end of the callback function. When the timer is cleared, an identifier (this.timeId = “”) must be added to the timer to indicate that the timer is cleared, preventing an infinite loop.
We have implemented the pause and resume functions of timers by implementing the Timer class. Next we need to integrate the pause and resume functions of timers with the life cycle of the component or page, preferably by separating them into common reusable code so that the developer does not have to deal with timers in the life cycle functions. Scrolling through the applets’ official documentation, Behavior is a good choice.
Behavior
Behaviors are features that are used to share code between components, similar to “mixins” or “traits” in some programming languages. Each behavior can contain a set of properties, data, lifecycle functions, and methods that are incorporated into the component when the component references it, and lifecycle functions that are called at the appropriate time. Each component can reference multiple behaviors, and behaviors can reference other behaviors.
// define behavior const TimerBehavior = behavior ({pageLifetimes: {show () { console.log('show')},hide () { console.log('hide') }
},
created: function () { console.log('created')},
detached: function() { console.log('detached')}})export} // component.js uses behavior import {TimerBehavior} from'.. /behavior.js'
Component({
behaviors: [TimerBehavior],
created: function () {
console.log('[my-component] created')
},
attached: function () {
console.log('[my-component] attached')}})Copy the code
Created () => Component.created() => timerBehavior.show (). Therefore, we only need to call the corresponding method of the Timer during the TimerBehavior lifecycle and open the Timer creation and destruction API to the developer. Here’s the idea:
- When a component or page is created, a Map object is created to store the timer for the component or page.
- When creating a Timer, save the Timer object in a Map.
- When the Timer ends or the Timer is cleared, remove the Timer object from the Map to avoid memory leakage.
- Pause the timer in the Map when the page is hidden, and resume the timer when the page is displayed again.
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)
},
}
})
Copy the code
There is a lot of redundancy in the code above, so we can optimize it by defining a separate TimerStore class to manage adding, removing, resuming, and pausing components or page timers.
class TimerStore {
constructor() {
this.store = new Map(a)this.isActive = true
}
addTimer(timer) {
this.store.set(timer.id, timer)
this.isActive && timer.start(this.store)
return timer.id
}
show() {
/* No hiding, no need to restore timer */
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)
}
}
Copy the code
And I’m going to simplify the TimerBehavior again
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)
},
}
})
Copy the code
In addition, after the timer created by setTimeout finishes running, we need to remove the timer from the Map to avoid memory leaks. Modify the Timer start function slightly as follows:
class Timer {
// Omit some code
start(timerStore) {
this.startTime = +new Date(a)if (this.isInterval) {
/* setInterval */
const cb = (. arg) = > {
this.fn(... arg)/* If timerId is empty, clearInterval */ is used
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)/* Remove timer to avoid memory leak */
timerStore.delete(this.id)
}
this.timerId = setTimeout(cb, this.restTime, ... this.arg) } }Copy the code
Use with pleasure
From now on, hand over the task of clearing the timer to the TimerBehavior management, no longer worry about the small program is getting stuck.
import { TimerBehavior } from '.. /behavior.js'// Use pages ({behaviors: [TimerBehavior],onReady() {
this.$setTimeout(() => {
console.log('setTimeout')
})
this.$setInterval(() => {
console.log('setTimeout'})}}) // Use Components({behaviors: [TimerBehavior],ready() {
this.$setTimeout(() => {
console.log('setTimeout')
})
this.$setInterval(() => {
console.log('setTimeout')})}})Copy the code
NPM package support
In order to make developers better use of the applets timer management library, we have cleaned up the code and released the NPM package for developers to use. Developers can install the applets timer management library by using NPM install — Save timer-miniProgram. See the documentation and complete code at github.com/o2team/time…
Eslint configuration
To help teams better comply with timer usage specifications, we can also configure ESLint to add code hints as follows:
// .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'}}}]Copy the code
conclusion
A thousand mile dam is broken by a swarm of ants.
Mismanaged timers drain little by little of the memory and performance of small programs, and eventually let the program crash.
Pay attention to timer management and keep timer leaks away.
reference
Applets developer documentation
Welcome to the bump Lab blog: AOtu.io
Or pay attention to the bump Laboratory public account (AOTULabs), push the article from time to time: