Инверсия зависимостей + идея итератора, реализовать следующее программирование, ориентированное на setTimeout

внешний интерфейс JavaScript
Инверсия зависимостей + идея итератора, реализовать следующее программирование, ориентированное на setTimeout

Эта статья участвовала в приказе о созыве Haowen, нажмите, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!

предисловие

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

Статья в предыдущей колонке былаЭффективная схема слияния двух массивов данных.

Возьмем пример таймера

Выполнять раз в 1 секунду, после 3 раз перестать звонить.

const nextFactory = createTimeoutGenerator();

let context = {
    counts: 0
};

nextFactory.start(function (this: any, next: Function) {
    context.counts ++;
    
    console.log("counts", context.counts);
    if(context.counts > 3){
        nextFactory.cancel();
    }
    
    next();

}, context);

image.png

таймер

Три общих интерфейсных таймераsetTimeout, setInterval, requestAnimationFrame

setIntervalЯма не является предметом этой статьи, поэтому остальные вариантыsetTimeout, requestAnimationFrame.

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

Приложение таймера

setTimeout

Мы используем нативный код для реализации 60-секундного обратного отсчета и поддержки функций паузы и продолжения. Давайте посмотрим на код: Это выглядит так:

image.png

 <div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>

    </div>

    <script>
        const secondsEl = document.getElementById("seconds");
        const INTERVAL = 1000;
        let ticket;
        let seconds = 60;

        function setSeconds(val) {
            secondsEl.innerText = val;
        }

        function onTimeout() {
            seconds--;
            setSeconds(seconds);
            ticket = setTimeout(onTimeout, INTERVAL);
        }

        ticket = setTimeout(onTimeout, INTERVAL);

        document.getElementById("btnPause").addEventListener("click", () => {
            clearTimeout(ticket);
        });

        document.getElementById("btnContinue").addEventListener("click", () => {
            ticket = setTimeout(onTimeout, INTERVAL);
        });


    </script>

Есть ли проблема? Я так думаю,

  1. INTERVAL,ticketа такжеsetTimeoutЛетать по всему небу недостаточно элегантно, мы должны уделять больше внимания ведению бизнеса;
  2. Если подобных логик много, то приходится писать повторно.setTimeout, отсутствие повторного использования;
  3. плохая семантика

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

Давайте посмотрим на другой кусок кода:
Эта же функция выглядит намного проще и имеет понятную семантику.

  • начало: начало
  • отменить: отменить
  • продолжать: продолжать
    <div class="wrapper">
        <span id="seconds">60</span>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>

    </div>

    <script src="../dist/index.js"></script>
    <script>

        const nextFactory = createTimeoutGenerator();

        const secondsEl = document.getElementById("seconds");
        let seconds = 60;
        
        function setSeconds(val) {
            secondsEl.innerText = val;
        };

        nextFactory.start(function(next){
            seconds--;
            setSeconds(seconds);
            next();            
        });

        document.getElementById("btnPause").addEventListener("click", () => {
            nextFactory.cancel();
        });

        document.getElementById("btnContinue").addEventListener("click", () => {
            nextFactory.continue();
        });

    </script>

requestAnimationFrame

Давайте взглянем на пример рисования холста.Мы рисуем текущую временную метку на холсте каждый второй цикл рисования. Это выглядит так:

image.pngТочно так же можно приостановить и возобновить.

  • время розыгрыша
  • requestAnimationFrame запускает таймер
  • События щелчка двух кнопок, обработка паузы и возобновления соответственно

Давайте сначала взглянем на базовую версию нативного JS:

    <div style="margin: 50px;">
        <canvas id="canvas" height="300" width="300"></canvas>
    </div>
    <div>
        <div>
            <button id="btnPause">暂停</button>
            <button id="btnContinue">继续</button>
        </div>
    </div>

    <script>

        let ticket;

        const canvasEl = document.getElementById("canvas");
        const ctx = canvasEl.getContext("2d");
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 300, 300);


        function drawTime() {
            ctx.clearRect(0, 0, 300, 300);
            ctx.fillStyle = "#f00";
            ctx.fillRect(0, 0, 300, 300);

            ctx.fillStyle = "#000";
            ctx.font = "bold 20px Arial";
            ctx.fillText(Date.now(), 100, 100);
        }

        function onRequestAnimationFrame() {
            drawTime();
            ticket = requestAnimationFrame(onRequestAnimationFrame);
        }

        ticket = requestAnimationFrame(onRequestAnimationFrame);
        
        document.getElementById("btnPause").addEventListener("click", () => {
            cancelAnimationFrame(ticket);
        });

        document.getElementById("btnContinue").addEventListener("click", () => {
            requestAnimationFrame(onRequestAnimationFrame);
        });

    </script>

Проблема осталась, давайте посмотрим на другую версию:

    const nextFactory = createRequestAnimationFrameGenerator();

    const canvasEl = document.getElementById("canvas");
    const ctx = canvasEl.getContext("2d");
    ctx.fillStyle = "#f00";
    ctx.fillRect(0, 0, 300, 300);

    function drawTime() {
        ctx.clearRect(0, 0, 300, 300);
        ctx.fillStyle = "#f00";
        ctx.fillRect(0, 0, 300, 300);

        ctx.fillStyle = "#000";
        ctx.font = "bold 20px Arial";
        ctx.fillText(Date.now(), 100, 100);
    }

    nextFactory.start((next)=>{
        drawTime();
        next();
    });

    document.getElementById("btnPause").addEventListener("click", () => {
        nextFactory.cancel();
    });

    document.getElementById("btnContinue").addEventListener("click", () => {
        nextFactory.continue();
    });

Здесь все заметили,createTimeoutGeneratorа такжеcreateRequestAnimationFrameGeneratorЭто ключ, это волшебный ключ, давайте приоткроем завесу.

позади createTimeoutGenerator

Поскольку название слишком длинное, оно должно бытьcreateTimeoutGeneratorа такжеcreateRequestAnimationFrameGeneratorпозади.

createTimeoutGeneratorкод:

Он внутренне строитexecuteа такжеcancelобъект свойств, затем создает экземплярNextGenerator, То есть,NextGeneratorявляется ядром.

export function createTimeoutGenerator(interval: number = 1000) {
    const timeoutGenerator = function (cb: Function) {

        let ticket: number;
        function execute() {
            ticket = setTimeout(cb, interval);
        }

        return {
            execute,
            cancel: function () {
                clearTimeout(ticket);
            }
        }
    }

    const factory = new NextGenerator(timeoutGenerator);
    return factory;
}

не могу дождаться открытияcreateRequestAnimationFrameGenerator:

Внезапно проснулась, чудесная, секунды.

export function createRequestAnimationFrameGenerator() {

    const requestAnimationFrameGenerator = function (cb: FrameRequestCallback) {

        let ticket: any;
        function execute() {
            ticket = window.requestAnimationFrame(cb);
        }

        return {
            execute,
            cancel: function () {
                cancelAnimationFrame(ticket);
            }
        }
    }

    const factory = new NextGenerator(requestAnimationFrameGenerator);
    return factory
}

Как вам нравится следующая

закончить просмотрcreateTimeoutGeneratorа такжеcreateRequestAnimationFrameGenerator. Можете ли вы смело думать, что пока я конструирую объект сexecuteа такжеcancelметод, вы можете получитьNextGenerator, а потом нагло звонить

  • start
  • cancel
  • continue

Ответ положительный.

С таким же успехом мы могли бы сделать таймер, который удваивает время, первый раз 100 мс, второй раз 200 мс, второй раз 400 мс, и рисовать совок в соответствии с тыквой:

export function createStepUpGenerator(interval: number = 1000) {

    const stepUpGenerator = function (cb: Function) {
        let ticket: any;
        function execute() {
            interval = interval * 2;
            ticket = setTimeout(cb, interval);
        }

        return {
            execute,
            cancel: function () {
                clearTimeout(ticket);
            }
        }
    }

    const factory = new NextGenerator(stepUpGenerator);
    return factory;
}

intervalПараметр является начальным значением по умолчанию в первый раз, а затем удваивается. Выполняйте его по одному, чтобы увидеть результат.
Тестовый код:

const nextFactory = createStepUpGenerator(100);

let lastTime = Date.now();
nextFactory.start(function (this: any, next, ...args: any[]) {
  
    const now = Date.now();

    console.log("time:", Date.now());
    console.log("costt time", now - lastTime);
    lastTime = now;
    console.log(" ");

    next();  
})

image.png

как хочешь, теперь можешь делать что хочешь, хочешь что хочешь, будь тоsetTimeout, requestAnimationFrame,Promise, async/awaitИ так далее, вы можете использовать его для создания таймера со своими битами.

макро идея

Анализируя это, давайте поговорим об идее здесь

  1. программирование для следующего
  2. инверсия зависимости
  3. Композиция имеет приоритет над наследованием

Программирование, ориентированное на следующее (итераторы)

Это имя - мой личный фаворит. Он относится к режиму итератора.

После того, как мы вызвали его один раз, нам нужно позвонить в следующий раз через определенное время, верно?nextШерстяная ткань?

Нативный интерфейс поставляется с:

  1. Iterator
  2. Generator

Может кто не помнит, напишуIteratorКод для него:

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

внешний фреймворкreduxпромежуточное ПО, есть ли такоеnext.

Что касается фоновых службexpressа такжеkoa, с ним все знакомы, поэтому упоминать не буду.

инверсия зависимости

Цитата Ван ЧжэнКрасота шаблонов дизайнаслова внутри

Модуль высокого уровня (высокоуровневые модули) не полагаются на низкоуровневые модули (низкоуровневый). Модуль мощного и низкоуровневого модуля должен зависеть друг от друга через резюме (абстракции). Кроме того, абстракция (абстракции) не полагаются на конкретные детали реализации (детали), конкретные детали реализации (детали) полагаются полагаются (абстракции).

Так называемое отделение модулей высокого уровня и низкоуровневые модули - это просто в цепочке вызовов, вызывающий абонент принадлежит высоким уровне, а Callee принадлежит низкому уровню.

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

NextGenerator и иметьexecuteа такжеcancelОбъекты свойств не имеют прямых зависимостей, оба зависят от одного и того же «аннотации».

Мы используем TS для описания этой абстракции:
NextFnInfoпросто эта абстракция

interface Unsubscribe {
    (): void
}

interface CallbackFunction<T = any> {
    (context: T, ...args: any[]): void
}

interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) => any
}

Осторожно, должно быть, нашел, на самом делеnextфункция такжеcontextи другие параметры, да.

Для упрощения кода предыдущие были удалены.Контекстом является функция обратного вызова, переданная start.thisконтекст.

const nextFactory = createTimeoutGenerator();

let context = {
    val: 0
};

nextFactory.start(function (this: any, next, ...args: any[]) {

  
    console.log("this", this);  // this { val: 0 }
    console.log("args", ...args); // args param1 param2

    nextFactory.cancel();

}, context, "param1", "param2")

Посмотрите внимательно на комментарии к коду:

  1. это равно контексту
  2. param1 и param2 передаются как есть

На самом деле, есть еще один слой информации,nextФункции могут повторно передавать контекст и другие параметры.

Еще одно шоу:
После того, как мы закончим выполнение, следующий проход{ a: 10 }В качестве контекста следующий вызов проверяет, равно ли a 10, и если да, то останавливает вызов.

const nextFactory = createTimeoutGenerator();

let context = {
    val: 0
};

nextFactory.start(function (this: any, next, ...args: any[]) {

    console.log("this", this);  // this { val: 0 }
    console.log("args", ...args); // args param1 param2


    next({ a: 10 }, "param-1", "param-2");

    if (this.a === 10) {
        nextFactory.cancel();
    }

}, context, "param1", "param2")

Выходной результат:

this { val: 0 }
args param1 param2
this { a: 10 }
args param-1 param-2

Композиция имеет приоритет над наследованием

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

Ядро NextGenerator

условие

Реализуем некоторые правила

  1. cancelПозже,nextне сработает в следующий раз, только вызовcontinueвосстанавливаться;
  2. В функции выполнения несколько вызововnextвступит в силу только один раз

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

  1. В ожидании, график запрошен
  2. в исполнении
  3. Отмена

параметр кеша

Благодаря приведенному выше коду мы знаем, что можем передавать контекст и параметры, а также можем передаватьnextПараметры переопределены, поэтому мы хотим кэшировать эти параметры.

контекст

Существует несколько способов изменения контекста функции:

  1. привязать к объекту
  2. call
  3. apply
  4. стрелочная функция
  5. bind
  6. разное

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

Полный текст кода

Руководство по исходному коду:

  1. Основной кодnextметод

это звонитNextFnGeneratorэкземпляр создает новый объектNextFnInfoЭкземпляр , который предоставляет методы для получения следующего плана выполнения и отмены следующего плана выполнения.

  1. Лучшее из этогоexecuteметод

это былоnextМетод привязан к контексту, и все параметры передаются.
Это определяет, что он можетNextGeneratorЭкземпляр взаимодействует и может получить все параметры и выполнить функцию обратного вызова.

Некоторые декларации ТС:

interface Unsubscribe {
    (): void
}

interface CallbackFunction<T = any> {
    (context: T, ...args: any[]): void
}

interface NextFnInfo<T = any> {
    cancel: Unsubscribe
    execute: (next: CallbackFunction<T>) => any
}

interface NextFnGenerator {
    (...args: any[]): NextFnInfo;
}

enum EnumStatus {
    uninitialized = 0,
    initialized,
    waiting,
    working,
    canceled,
    unkown
}

Основной класс NextGenerator:

export default class NextGenerator<T = any> {

    private status: EnumStatus = EnumStatus.uninitialized;
    private nextInfo!: NextFnInfo;

    // 传入的回调函数
    private cb!: CallbackFunction;

    // 下次回调函数的参数
    private args: any[] = [];

    constructor(private generator: NextFnGenerator) {
        this.status = EnumStatus.initialized;
    }

    private next(...args: any[]) {

        if (this.status === EnumStatus.canceled) {
            return console.warn("current status is canceled, please call continute method to continue");
        }

        if (this.status === EnumStatus.waiting) {
            return console.warn("current status is waiting, please don't multiple call next method");
        }

        if (args.length > 0) {
            this.args = args;
        }

        // this.args[0] context
        const boundFn = this.execute.bind(this, this.cb, ...this.args);

        this.nextInfo = this.generator(boundFn);

        this.status = EnumStatus.waiting;
        this.nextInfo.execute(undefined as any);
    }

    private execute(this: NextGenerator<T>, cb: Function, context: T, ...args: any[]) {
        this.status = EnumStatus.working;
        cb.apply(context, [this.next.bind(this), ...args]);
    }

    cancel() {
        this.status = EnumStatus.canceled;
        if (this.nextInfo && typeof this.nextInfo.cancel === "function") {
            this.nextInfo.cancel();
        }
    }

    start(cb: CallbackFunction, ...args: any[]) {
        if (typeof cb === "function") {
            this.cb = cb;
        }

        if (typeof this.cb !== "function") {
            throw new SyntaxError("param cb must be a function");
        }

        if (args.length > 0) {
            this.args = args;
        }

        this.next();
    }

    continue() {
        this.status = EnumStatus.initialized;
        this.next();
    }
}

Суммировать

Мы всегда пишем код, когда мы пишем один и тот же код дважды и более, то надо остановиться и подумать, есть ли проблема и есть ли место для оптимизации.

Я написал библиотеку, которая упрощает вызовы setTimeout.timeout, Видения и абстракции в то время было недостаточно. Количество решаемых задач также ограничено.

Сначала хотел написатьДля следующего программирования и реального боя, включает в себя слишком много вещей, таких как промежуточное ПО redux, промежуточное ПО koa, экспресс-принцип и реализация промежуточного ПО и так далее.

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

  1. можно реализовать самостоятельноNextFnGenerator, обеспечивая относительно высокие возможности настройки
  2. встроенныйcreateRequestAnimationFrameGenerator, createTimeoutGenerator, createStepUpGenerator, из коробки
  3. И инициализация, и следующий могут настроить контекст и параметры для повышения гибкости вызова.
  4. просто разоблачитьstart, cancel, continue, в соответствии с принципом наименьшего знания

Существующие проблемы:

  1. Что делать, если время истекло?
  2. Что ненормально?
  3. СинхронныйGeneratorкак рассчитать

напиши в конце

Добро пожаловать в колонкуОт вопроса к вопросу, общаться и учиться вместе.

Писать не просто, ваши лайки и комментарии - движущая сила для меня двигаться вперед.