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

  1. How to realize timer pause and resume
  2. How to keep developers from dealing with timers in life cycle functions

How to realize timer pause and resume

Here’s the idea:

  1. The timer function parameters are saved and recreated when the timer is restored
  2. 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
  3. 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:

  1. Start the timer, call the native API to create the timer and record the start time stamp.
  2. Pause the timer, clear the timer and calculate the remaining time of the cycle.
  3. 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:

  1. 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 objectthis.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).
  2. 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.
  3. 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:

  1. When a component or page is created, a Map object is created to store the timer for the component or page.
  2. When creating a Timer, save the Timer object in a Map.
  3. When the Timer ends or the Timer is cleared, remove the Timer object from the Map to avoid memory leakage.
  4. 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: