手把手教你寫個(gè)小程序定時(shí)器管理庫 - 新聞資訊 - 雲南小程序開發|雲南軟件開發|雲南網站建設-昆明融晨信息技術有限公司

159-8711-8523

雲南網建設/小程序開發/軟件開發

知識

不(bù)管是(shì)網站,軟件還是(shì)小程序,都要(yào / yāo)直接或間接能爲(wéi / wèi)您産生價值,我們在(zài)追求其視覺表現的(de)同時(shí),更側重于(yú)功能的(de)便捷,營銷的(de)便利,運營的(de)高效,讓網站成爲(wéi / wèi)營銷工具,讓軟件能切實提升企業内部管理水平和(hé / huò)效率。優秀的(de)程序爲(wéi / wèi)後期升級提供便捷的(de)支持!

您當前位置>首頁 » 新聞資訊 » 小程序相關 >

手把手教你寫個(gè)小程序定時(shí)器管理庫

發表時(shí)間:2021-1-5

發布人(rén):融晨科技

浏覽次數:58

背景

凹凸曼是(shì)個(gè)小程序開發者,他(tā)要(yào / yāo)在(zài)小程序實現秒殺倒計時(shí)。于(yú)是(shì)他(tā)不(bù)假思索,寫了(le/liǎo)以(yǐ)下代碼:

Page({
  init: function () {
    clearInterval(this.timer)
    this.timer = setInterval(() => {
      // 倒計時(shí)計算邏輯
      console.log('setInterval')
    })
  },
})

可是(shì),凹凸曼發現頁面隐藏在(zài)後台時(shí),定時(shí)器還在(zài)不(bù)斷運行。于(yú)是(shì)凹凸曼優化了(le/liǎo)一下,在(zài)頁面展示的(de)時(shí)候運行,隐藏的(de)時(shí)候就(jiù)暫停。

Page({
  onShow: function () {
    if (this.timer) {
      this.timer = setInterval(() => {
        // 倒計時(shí)計算邏輯
        console.log('setInterval')
      })
    }
  },
  onHide: function () {
    clearInterval(this.timer)
  },
  init: function () {
    clearInterval(this.timer)
    this.timer = setInterval(() => {
      // 倒計時(shí)計算邏輯
      console.log('setInterval')
    })
  },
})

問題看起來(lái)已經解決了(le/liǎo),就(jiù)在(zài)凹凸曼開心地(dì / de)搓搓小手暗暗歡喜時(shí),突然發現小程序頁面銷毀時(shí)是(shì)不(bù)一定會調用 onHide 函數的(de),這(zhè)樣定時(shí)器不(bù)就(jiù)沒法清理了(le/liǎo)?那可是(shì)會造成内存洩漏的(de)。凹凸曼想了(le/liǎo)想,其實問題不(bù)難解決,在(zài)頁面 onUnload 的(de)時(shí)候也(yě)清理一遍定時(shí)器就(jiù)可以(yǐ)了(le/liǎo)。

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

這(zhè)下問題都解決了(le/liǎo),但我們可以(yǐ)發現,在(zài)小程序使用定時(shí)器需要(yào / yāo)很謹慎,一不(bù)小心就(jiù)會造成内存洩漏。後台的(de)定時(shí)器積累得越多,小程序就(jiù)越卡,耗電量也(yě)越大(dà),最終導緻程序卡死甚至崩潰。特别是(shì)團隊開發的(de)項目,很難确保每個(gè)成員都正确清理了(le/liǎo)定時(shí)器。因此,寫一個(gè)定時(shí)器管理庫來(lái)管理定時(shí)器的(de)生命周期,将大(dà)有裨益。

思路整理

首先,我們先設計定時(shí)器的(de) API 規範,肯定是(shì)越接近原生 API 越好,這(zhè)樣開發者可以(yǐ)無痛替換。

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

接下來(lái)我們主要(yào / yāo)解決以(yǐ)下兩個(gè)問題

  1. 如何實現定時(shí)器暫停和(hé / huò)恢複

  2. 如何讓開發者無須在(zài)生命周期函數處理定時(shí)器

如何實現定時(shí)器暫停和(hé / huò)恢複

思路如下:

  1. 将定時(shí)器函數參數保存,恢複定時(shí)器時(shí)重新創建

  2. 由于(yú)重新創建定時(shí)器,定時(shí)器 ID 會不(bù)同,因此需要(yào / yāo)自定義全局唯一 ID 來(lái)标識定時(shí)器

  3. 隐藏時(shí)記錄定時(shí)器剩餘倒計時(shí)時(shí)間,恢複時(shí)使用剩餘時(shí)間重新創建定時(shí)器

首先我們需要(yào / yāo)定義一個(gè) Timer 類,Timer 對象會存儲定時(shí)器函數參數,代碼如下

class Timer {
    static count = 0
    /**
     * 構造函數
     * @param {Boolean} isInterval 是(shì)否是(shì) setInterval
     * @param {Function} fn 回調函數
     * @param {Number} timeout 定時(shí)器執行時(shí)間間隔
     * @param  {...any} arg 定時(shí)器其他(tā)參數
     */
    constructor (isInterval = false, fn = () => {}, timeout = 0, ...arg) {
        this.id = ++Timer.count // 定時(shí)器遞增 id
        this.fn = fn
        this.timeout = timeout
        this.restTime = timeout // 定時(shí)器剩餘計時(shí)時(shí)間
        this.isInterval = isInterval
        this.arg = arg
    }
  }

  // 創建定時(shí)器
  function $setTimeout(fn, timeout, ...arg) {
    const timer = new Timer(false, fn, timeout, arg)
    return timer.id
  }

接下來(lái),我們來(lái)實現定時(shí)器的(de)暫停和(hé / huò)恢複,實現思路如下:

  1. 啓動定時(shí)器,調用原生 API 創建定時(shí)器并記錄下開始計時(shí)時(shí)間戳。

  2. 暫停定時(shí)器,清除定時(shí)器并計算該周期計時(shí)剩餘時(shí)間。

  3. 恢複定時(shí)器,重新記錄開始計時(shí)時(shí)間戳,并使用剩餘時(shí)間創建定時(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í)器剩餘計時(shí)時(shí)間
        this.isInterval = isInterval
        this.arg = arg
    }

    /**
     * 啓動或恢複定時(shí)器
     */
    start() {
        this.startTime = +new Date()

        if (this.isInterval) {
            /* setInterval */
            const cb = (...arg) => {
                this.fn(...arg)
                /* timerId 爲(wéi / wèi)空表示被 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è)關鍵點需要(yào / yāo)提示一下:

  1. 恢複定時(shí)器時(shí),實際上(shàng)我們是(shì)重新創建了(le/liǎo)一個(gè)定時(shí)器,如果直接用 setTimeout 返回的(de) ID 返回給開發者,開發者要(yào / yāo) clearTimeout,這(zhè)時(shí)候是(shì)清除不(bù)了(le/liǎo)的(de)。因此需要(yào / yāo)在(zài)創建 Timer 對象時(shí)内部定義一個(gè)全局唯一 ID this.id = ++Timer.count ,将該 ID 返回給 開發者。開發者 clearTimeout 時(shí),我們再根據該 ID 去查找真實的(de)定時(shí)器 ID (this.timerId)。
  2. 計時(shí)剩餘時(shí)間,timeout = 0 時(shí)不(bù)必計算;timeout > 0 時(shí),需要(yào / yāo)區分是(shì) setInterval 還是(shì) setTimeout,setInterval 因爲(wéi / wèi)有周期循環,因此需要(yào / yāo)對時(shí)間間隔進行取餘。

  3. setInterval 通過在(zài)回調函數末尾調用 setTimeout 實現,清除定時(shí)器時(shí),要(yào / yāo)在(zài)定時(shí)器增加一個(gè)标示位(this.timeId = "")表示被清除,防止死循環。

我們通過實現 Timer 類完成了(le/liǎo)定時(shí)器的(de)暫停和(hé / huò)恢複功能,接下來(lái)我們需要(yào / yāo)将定時(shí)器的(de)暫停和(hé / huò)恢複功能跟組件或頁面的(de)生命周期結合起來(lái),最好是(shì)抽離成公共可複用的(de)代碼,讓開發者無須在(zài)生命周期函數處理定時(shí)器。翻閱小程序官方文檔,發現 Behavior 是(shì)個(gè)不(bù)錯的(de)選擇。

Behavior

behaviors 是(shì)用于(yú)組件間代碼共享的(de)特性,類似于(yú)一些編程語言中的(de) "mixins" 或 "traits"。每個(gè) behavior 可以(yǐ)包含一組屬性、數據、生命周期函數和(hé / huò)方法,組件引用它時(shí),它的(de)屬性、數據和(hé / huò)方法會被合并到(dào)組件中,生命周期函數也(yě)會在(zài)對應時(shí)機被調用。每個(gè)組件可以(yǐ)引用多個(gè) behavior,behavior 也(yě)可以(yǐ)引用其他(tā) 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')
  }
})

如上(shàng)面的(de)例子(zǐ),組件使用 TimerBehavior 後,組件初始化過程中,會依次調用 TimerBehavior.created() => Component.created() => TimerBehavior.show() 。因此,我們隻需要(yào / yāo)在(zài) TimerBehavior 生命周期内調用 Timer 對應的(de)方法,并開放定時(shí)器的(de)創建銷毀 API 給開發者即可。思路如下:

  1. 組件或頁面創建時(shí),新建 Map 對象來(lái)存儲該組件或頁面的(de)定時(shí)器。

  2. 創建定時(shí)器時(shí),将 Timer 對象保存在(zài) Map 中。

  3. 定時(shí)器運行結束或清除定時(shí)器時(shí),将 Timer 對象從 Map 移除,避免内存洩漏。

  4. 頁面隐藏時(shí)将 Map 中的(de)定時(shí)器暫停,頁面重新展示時(shí)恢複 Map 中的(de)定時(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)
    },
  }
})

上(shàng)面的(de)代碼有許多冗餘的(de)地(dì / de)方,我們可以(yǐ)再優化一下,單獨定義一個(gè) TimerStore 類來(lái)管理組件或頁面定時(shí)器的(de)添加、删除、恢複、暫停功能。

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() {
        /* 沒有隐藏,不(bù)需要(yào / yāo)恢複定時(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)
    }
}

然後再簡化一遍 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 創建的(de)定時(shí)器運行結束後,爲(wéi / wèi)了(le/liǎo)避免内存洩漏,我們需要(yào / yāo)将定時(shí)器從 Map 中移除。稍微修改下 Timer 的(de) start 函數,如下:

class Timer {
    // 省略若幹代碼
    start(timerStore) {
        this.startTime = +new Date()

        if (this.isInterval) {
            /* setInterval */
            const cb = (...arg) => {
                this.fn(...arg)
                /* timerId 爲(wéi / wèi)空表示被 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)
            /* 運行結束,移除定時(shí)器,避免内存洩漏 */
            timerStore.delete(this.id)
        }
        this.timerId = setTimeout(cb, this.restTime, ...this.arg)
    }
}

愉快地(dì / de)使用

從此,把清除定時(shí)器的(de)工作交給 TimerBehavior 管理,再也(yě)不(bù)用擔心小程序越來(lái)越卡。

import { TimerBehavior } from '../behavior.js'

// 在(zài)頁面中使用
Page({
  behaviors: [TimerBehavior],
  onReady() {
    this.$setTimeout(() => {
      console.log('setTimeout')
    })
    this.$setInterval(() => {
      console.log('setTimeout')
    })
  }
})

// 在(zài)組件中使用
Components({
  behaviors: [TimerBehavior],
  ready() {
    this.$setTimeout(() => {
      console.log('setTimeout')
    })
    this.$setInterval(() => {
      console.log('setTimeout')
    })
  }
})

npm 包支持

爲(wéi / wèi)了(le/liǎo)讓開發者更好地(dì / de)使用小程序定時(shí)器管理庫,我們整理了(le/liǎo)代碼并發布了(le/liǎo) npm 包供開發者使用,開發者可以(yǐ)通過 npm install --save timer-miniprogram 安裝小程序定時(shí)器管理庫,文檔及完整代碼詳看 https://github.com/o2team/timer-miniprogram

eslint 配置

爲(wéi / wèi)了(le/liǎo)讓團隊更好地(dì / de)遵守定時(shí)器使用規範,我們還可以(yǐ)配置 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'
        }]
    }
}

總結

千裏之(zhī)堤,潰于(yú)蟻穴。

管理不(bù)當的(de)定時(shí)器,将一點點榨幹小程序的(de)内存和(hé / huò)性能,最終讓程序崩潰。

重視定時(shí)器管理,遠離定時(shí)器洩露。

相關案例查看更多