Как написать код Javascript, который не блокирует пользовательский интерфейс - PullRequest
0 голосов
/ 03 декабря 2018

Я работаю над приложением javascript, которое выполняет 2 задания.

Первое задание является более важным и должно выполняться со скоростью 60 кадров в секунду.Другая работа - это «фоновая» работа, которая все еще должна выполняться, но это нормально, если она занимает больше времени.

Обычно я бы сделал так, чтобы код более важного задания был в цикле RequestAnimationFrame, и поместил фоновое задание на веб-работника.

Однако основная работа уже порождает 2 веб-работников, и я не хочу создавать третью из-за переключения контекста и потребления памяти.

В цикле RequestAnimationFrame осталось около 8 мс времени обработки, с которым мне нужно работать, чтобы выполнить фоновое задание, однако это задание, которое может занять около 100 мс.

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

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

Ответы [ 2 ]

0 голосов
/ 20 декабря 2018

Я рекомендую requestIdleCallback() как принятый ответ, но он все еще экспериментален, и мне нравится придумывать подобные вещи.Вы можете даже объединить RIC с этим ответом, чтобы получить что-то более подходящее для ваших нужд.

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

Один из способов - создать в очереди несколько функций, которые выполняют необходимую работу, например, unprocessed.forEach(x=>workQueue.push(idleFunc.bind(null,x)));}, затем назначить исполнителя, который в какой-то момент будет обрабатывать очередь в течение заданного промежутка времени.

Если у вас есть цикл, для завершения которого требуется некоторое время, вы можете использовать функцию генератора и выдавать в конце каждого цикла, а затем запускать его внутри рекурсивных вызовов на setTimeout() с вашим собственным крайним сроком.или requestIdleCallback().

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

Aв любом случае, вот что-то, что я вырвал из любопытства.

class IdleWorkExecutor {
    constructor() {
        this.workQueue=[];
        this.running=null;
    }

    addWork(func) {
        this.workQueue.push(_=>func());
        this.start();
    }

    //
    addWorkPromise(func) {
        return new Promise(r=>{
            this.workQueue.push(_=>r(func()));
            this.start();
        });
        //DRY alternative with more overhead:
        //return new Promise(r=>this.addWork(_=>r(func())));
    }

    sleep(ms) {
        return new Promise(r=>setTimeout(r,ms));
    }

    //Only run the work loop when there is work to be done
    start() {
        if (this.running) {return this.running;}
        return this.running=(async _=>{
            //Create local reference to the queue and sleep for negligible performance gain...
            const {workQueue,sleep}=this;
            //Declare deadline as 0 to pause execution as soon as the loop is entered.
            let deadline=0;
            while (workQueue.length!==0) {
                if (performance.now()>deadline) {
                    await sleep(10);
                    deadline=performance.now()+1;
                }
                /*shift* off and execute a piece of work. *push and shift are used to
                  create a FIFO buffer, but a growable ring buffer would be better. This
                  was chosen over unshift and pop because expensive operations shouldn't
                  be performed outside the idle executor.*/
                workQueue.shift()(deadline);
            }
            this.running=false;
        })();
    }
}

//Trying out the class.
let executor=new IdleWorkExecutor();

executor.addWork(_=>console.log('Hello World!'));

executor.addWorkPromise(_=>1+1).then(ans=>{
    executor.addWork(_=>console.log('Answer: '+ans));
});

//A recursive busy loop function.
executor.addWork(function a(counter=20) {
    const deadline=performance.now()+0.2;
    let i=0;
    while (performance.now()<deadline) {i++}
    console.log(deadline,i);
    if (counter>0) {
        executor.addWork(a.bind(null,counter-1));
    }
});

Если вы можете использовать requestIdleCallback() в своем коде, добавить его в IdleWorkExecutor довольно просто:

function rICPromise(opt) {
    return new Promise(r=>{
        requestIdleCallback(r,opt);
    });
}

if (!deadline||deadline.timeRemaining()>0) {
    deadline=await rICPromise({timeout:5000});
}
0 голосов
/ 03 декабря 2018

В настоящее время это экспериментальная технология, которая пока не очень хорошо поддерживается , но: есть requestIdleCallback, которая:

...ставит в очередь функцию, вызываемую в периоды простоя браузера.Это позволяет разработчикам выполнять фоновую и низкоприоритетную работу в главном цикле событий, не влияя на критически важные для задержки события, такие как анимация и отклик ввода.Функции обычно вызываются в порядке «первым пришел - первым вышел»;однако обратные вызовы, для которых задано время ожидания, могут быть вызваны не по порядку, если это необходимо для запуска их до истечения времени ожидания.

Одна из ключевых особенностей rIC - получение IdleDeadline объект , который

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

Таким образом, вы можете остановить цикл, когда deadline.timeRemaining() метод вернет достаточно небольшое количество оставшихся миллисекунд.


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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...