Ямы обратного отсчета

JavaScript
Ямы обратного отсчета

мы используемsetIntervalЧтобы реализовать обратный отсчет переднего плана, я недавно столкнулся с двумя небольшими ямами в своей работе, и я поделюсь ими с вами.

  1. На мобильном терминале при переходе браузера в фоновый режим таймер страницы приостанавливается, при повторном открытии браузера обратный отсчет продолжает выполняться, что приводит к увеличению времени выполнения обратного отсчета.

  2. На странице есть несколько обратных отсчетов, например, обратный отсчет для продажи списка продуктов.Когда страница открыта в течение длительного времени, будет обнаружено, что обратный отсчет некоторых продуктов будет неточным.

Давайте проанализируем один за другим

первый сценарий

Таймер приостановлен, потому что браузер остановил поток страницы. В конце концов, браузер был переведен в фоновый режим. По соображениям производительности разумно остановить поток страницы. Это приводит к тому, что наш таймер приостанавливается в в то же время Продукт не согласен с этой операцией.

Остается только прибегнуть к киллеру: это требование невозможно реализовать, а потом со слезами изменить жука.

посмотреть на каштаны

let text = document.querySelector('#text');
let beginTime = document.querySelector('#beginTime');
let end = document.querySelector('#end');
let runTime = 10 * 1000;
beginTime.innerHTML = moment().format('YYYY-MM-DD HH:mm:ss');
let interval = setInterval(() => {
    if (runTime > 0) {
        runTime -= 1000
        text.innerHTML = 'currentTime:' + moment().format('YYYY-MM-DD HH:mm:ss')
    } else {
        end.innerHTML = 'end';
        clearInterval(interval)
    }
}, 1000)

runTimeУказывает общее время выполнения обратного отсчета,currentTimeОтображение текущего времени каждого выполнения, тогда ожидаемое время последнего выполнения должно быть равно времени начала обратного отсчета плюс время выполненияcurrentTime = beginTime + runTimeКак показано ниже

normal

Проверьте эффект на мобильных устройствах

stop-time

Последнее время выполнения явно превышает запланированное время выполнения, причина указана выше, поэтому время выполнения необходимо изменить на следующее剩余运行时长 = 运行时长 - 间隔时间 - 线程暂停时间Время паузы потока — это временной интервал с момента последнего выполнения.

Измените часть таймера следующим образом:watchTimeIntervalЭто инкапсулированная самокалибрующаяся функция таймера.


watchTimeInterval(runTime,1000,()=>{
    text.innerHTML = 'currentTime: ' + moment().format('YYYY-MM-DD HH:mm:ss') //new Date().getTime();
},()=>{
    end.innerHTML = ' interval end';
})

/**
 * @description
 * 倒计时-计时器-浏览器进程切后台后,去除进程暂停时间
 * @param {number} time 倒计时时长,单位毫秒
 * @param {number} point 倒计时间隔
 * @param {function} func 倒计时执行函数
 * @param {function} timeOverFunc 倒计时结束执行函数
 * @returns {TimeOut} 倒计时唯一标识
 * @example
 * Utils.watchTimeInterval(10*1000, 1000, () => {}, () => {})
 */
function watchTimeInterval(time, point, func, timeOverFunc) {
    let _time = time;
    let startTime = new Date().valueOf();
    let interval = setInterval(() => {
        let gap = (new Date().valueOf() - startTime - point);
        if (gap < 0) {
            gap = 0;
        }
        _time = _time - gap;
        startTime = new Date().valueOf();
        if (_time > 0) {
            func && func();
            _time -= point;
        } else {
            interval && clearInterval(interval)
            timeOverFunc && timeOverFunc();
        }
    }, point)
    return interval
}

Взгляните на эффект на мобильном терминале

new-stop-time

Теперь, даже если браузер переключается в фоновый режим, конечное время выполнения все еще находится в заданном диапазоне, ха-ха, решите это, уходите с работы ~ нет ошибки, которую мы не могли бы решить (тссс~ не позволяйте продукту слышать это)

второй сценарий

Друзья, знакомые с циклом событий js, знают, что асинхронные события в js реализуются через циклическую очередь, а callback-функция таймера будет входить в очередь макросов и ждать выполнения, поэтому время выполнения таймера не равно 100%. если быть точным, если основной поток заблокирован (этот случай мы здесь пока рассматривать не будем), или если циклическая очередь имеет несколько задач, или есть трудоемкие операции, то таймер будет медленно становиться ошибочным.

Возвращаясь к сцене, на странице одновременно есть несколько таймеров, что означает, что в циклической очереди будет несколько функций обратного вызова, ожидающих выполнения одновременно. время вычислений в функции обратного вызова, Это может быть не видно в течение короткого периода времени до открытия страницы, но когда страница открыта в течение длительного времени, накопленная ошибка будет становиться все больше и больше.Если пользователь уже открыли страницу, они будут ждать окончания обратного отсчета, чтобы раскупить покупку, она уже началась, а пользователь наотмашь — это жалоба.

Основная причина этой проблемы заключается в том, что одновременно используется слишком много таймеров, и все сгенерированные события обратного вызова помещаются в очередь для выполнения. Уменьшение количества таймеров — это то, что нам нужно решить.

Подход, который я использую, таков: используйте шаблон наблюдателя для достижения.

Шаблон наблюдателя — это схема «один ко многим». Несколько подписчиков добавляют подписки к наблюдателю. Наблюдатель обнаруживает изменения и уведомляет соответствующих подписчиков, как показано на следующей диаграмме:

watch

Следующим шагом является проектирование наблюдателей и подписчиков.

наблюдатель

Наблюдатель всего один, и нам очень удобно поставить таймер в обозревателе, обозреватель наблюдает за изменением таймера и затем оповещает подписчика по заданным условиям.

/**
 * @description
 * 为解决页面中同时存在多个倒计时的情况下,生成多个计时器导致计时出现偏差的问题。
 * 采用观察者模式,由一个定时器控制多个倒计时事件
 * @class SuspendTimeNotify
 */
class SuspendTimeNotify {
    /**
     * Creates an instance of SuspendTimeNotify.
     * @param {*} [ intervalPoint=200 ] intervalPoint:计时器执行的间隔时间
     * @memberof SuspendTimeNotify
     */
    constructor(params) {
        const {intervalPoint=200} = params || {}
        this._currentTime = Date.now(); // 定时器回调函数执行的当前时间点
        this._passTime = 0; // 已经执行的时长
        this.observers = []; // 订阅者列表
        this._interval = null; // 定时器id
        this._intervalPoint = intervalPoint // 定时器间隔
    }
    /**
     * @description
     * 添加订阅者
     * @param {object} observer
     * @memberof SuspendTimeNotify
     */
    attach(observer) {
        let item = {
            key: `${this.observers.length}_key`,
            target: observer
        }
        this.observers.push(item);
    }
    /**
     * @description
     * 停止观察者的倒计时对象和情况订阅者
     * @memberof SuspendTimeNotify
     */
    stop() {
        this.observers = [];
        this._interval && clearInterval(this._interval)
    }
    /**
     * @description
     * 通知订阅者,订阅者通过 update 返回是否还继续订阅,若为 false ,则从订阅者队列中删除
     * @memberof SuspendTimeNotify
     */
    notifyObserver() {
        let deleteKeys = '';
        for (const { key, target } of this.observers) {
            let result = target.update(this._passTime);
            if (result) {
                deleteKeys += `${key},`
            }
        }
        if (deleteKeys) {
            this.observers = this.observers.filter(({ key }) => deleteKeys.indexOf(key) < 0)
        }
    }
    /**
     * @description
     * 启动倒计时
     * @memberof SuspendTimeNotify
     */
    start() {
        if (this._interval) {
            clearInterval(this._interval)
        }
        this._interval = setInterval(() => {
            let _nowTime = Date.now();
            this._passTime += _nowTime - this._currentTime;
            this._currentTime = _nowTime;
            this.notifyObserver();
        }, this._intervalPoint);
    }
}

существуетnotifyObserverПодписчик вызывается в методеtarget.update()метод, с помощью которого подписчик уведомляется, чтобы подписчики, которым больше не нужно выполнять таймер, все еще существовали в очереди, поэтому подписчику необходимо вернутьbooleanЗначение, указывающее, следует ли продолжать подписку.

подписчик

После разработки наблюдателя подписчик проще, если реализацияupdateМетода достаточно, но чтобы унифицировать структуру подписчика и как можно меньше связать с бизнес-объектом, он реализован отдельно.SuspendTimeObserveобъект подписчика.

Требуется, чтобы бизнес-объект реализовал метод запуска для выполнения обратного вызова таймера.

/**
 * @description
 * 定时器订阅者
 * @class SuspendTimeObserve
 */
class SuspendTimeObserve {
    /**
     *Creates an instance of SuspendTimeObserve.
     * @param {object} item 业务对象,业务对象可通过 run 方法获取定时器执行回调
     * @param {number} countdownTime 需要倒计时的总时长,单位毫秒
     * @memberof SuspendTimeObserve
     */
    constructor(item, countdownTime) {
        this.item = item
        this.countdownTime = countdownTime
    }
    /**
     * @desc
     * 接收观察者的通知事件
     * @param {number} passTime 已经执行的时长,单位毫秒
     * @returns {boolean} 是否继续订阅
     * @memberof SuspendTimeObserve
     */
    update(passTime) {
        var leftCountdownTime = this.countdownTime - passTime;
        this.item.run && this.item.run({ leftCountdownTime, passTime });
        return leftCountdownTime <= 0;
    }
}

Давайте посмотрим демонстрацию, на странице семь обратных отсчетов, а время выполнения непостоянно.

time-mult

Используя описанный выше метод, используйте один таймер для реализации нескольких обратных отсчетов.

// 业务对象
class Plan {
    constructor(i, time) {
        this.item = window.document.querySelector('#_' + i)
        this.time = time;
        this.item.querySelector('#runtime').innerHTML = 'runtime= '+time/1000 + '秒  '
    }
    run({ leftCountdownTime }) {
        if (leftCountdownTime > 0) {
            this.item.querySelector('#currentTime').innerHTML = 'currentTime= '+ moment().format('YYYY-MM-DD HH:mm:ss');
        }
    }
    getTime() { return this.time }
}
// 创建观察者
const suspendTimeNotify = new SuspendTimeNotify({ intervalPoint: 1000 });
for (let i = 1; i < 7; i++) {
    let runTime = i * Math.floor(Math.random() * 10)
    const plan = new Plan(i, runTime * 1000)
    // 由业务对象创建订阅者
    const ob = new SuspendTimeObserve(plan, plan.getTime())
    // 添加订阅者
    suspendTimeNotify.attach(ob)
}
// 启动定时器
suspendTimeNotify.start()
document.querySelector('#current').innerHTML = 'current= '+moment().format('YYYY-MM-DD HH:mm:ss');

резюме

Знания, затронутые в этой статье, относительно просты и решают некоторые небольшие проблемы простым способом.Вы можете попробовать скопировать и использовать функции и классы, инкапсулированные в статье.

Рекламировать мой аккаунт

Моя официальная учетная запись Xiaoli на переднем плане. Вы также можете отсканировать QR-код ниже, чтобы следовать

Статьи о Наггетс также будут публиковаться в публичном аккаунте, а в публичном аккаунте также будут публиковаться другие статьи, чаты и так далее~

Добро пожаловать, друзья, обратите внимание~

wx-code