В том-то и прикол, что нельзя на Vue ничего такого написать, потому что рендеринг там заведомо синхронная операция. Эта идея обречена на провал, так же как и попытки найти способ вмешаться в цикл for из моего первого примера. Чтобы его можно было прервать, он изначально должен быть реализован таким образом, который позволил бы это сделать, но это, увы, не так. Та же ситуация и с рендерингом во Vue.
Ни nextTick, ни requestIdleCallback, ни setTimeout, ни что-либо ещё в от природы своей однопоточном джаваскрипте не способно прервать выполнение синхронного кода. Вот пример, который позволяет в этом убедиться:
setTimeout(() => console.log('a second passed'), 1000);
for (let i = 0; i < 1e10; i++);
console.log('for loop done');
Хоть мы и запросили вывод строки “a second passed” через секунду, на деле выводится она лишь после того, как завершается выполнение цикла for, а это происходит намного позже. Связано это с тем, что цикл выполняется синхронно, то есть не уступая дорогу никакому другому коду, пока цикл не будет выполнен полностью.
По той же самой причине строка “for loop done” появляется в консоли перед “a second passed”, хотя на момент её вывода прошло уже сильно больше одной секунды. Никакой код не может влезть ни между итерациями цикла, ни между циклом и выводом “a second passed”. Всё это синхронный код — последовательность выполняемых друг за другом инструкций, и возможности прервать его просто не существует. Чтобы такая возможность была, код надо сделать асинхронным, например вот так:
function tick() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
setTimeout(() => console.log('a second passed'), 1000);
let i = 0;
for (let j = 0; j < 1e3; j++) {
const end = i + 1e7;
for (; i < end; i++);
await tick();
}
console.log('for loop done');
Вычисление здесь мы разделили на 1000 равных по продолжительности частей, в конце каждой из которых с помощью await tick() контроль передаётся циклу событий (event loop), который продолжает выполнение цикла for лишь после того, как будут выполнены все остальные ждущие в очереди задачи. Примером такой задачи может быть выполнение переданной setTimeout функции по истечении указанного срока (в нашем случае это вывод строки “a second passed” по прошествии одной секунды). Другой пример — считывание взаимодействий пользователя со страницей, которые прежде были полностью заблокированы на время вычисления цикла for. Если запустите сначала первый, а потом второй пример в консоли браузера и во время вычисления попробуете нажать куда-нибудь или ввести текст на странице, к которой эта консоль привязана, разница будет очевидна.
Теперь наконец можем вернуться к useTransition и useDeferredValue. Весь смысл этих хуков в том, что они превращают изначально синхронную операцию рендеринга дерева компонентов в асинхронную так же, как это было проделано мной с циклом for в примере выше. Оба хука позволяют выполнять тяжеловесные обновления UI на фоне таким образом, чтобы это никак не мешало более срочным синхронным обновлениям и взаимодействию пользователя со страницей, а useDeferredValue ещё и позволяет отменять обновления, которые становятся неактуальны из-за изменившихся вводных данных — например когда пользователь быстро вбивает слово в строку поиска, чтобы отфильтровать по нему данные в таблице. Это как раз тот самый пример, где Vue разработчикам предлагается использовать useDebounceFn. Преимущество useDeferredValue по-моему довольно очевидно:
В случае useDebounceFn фильтрация происходит лишь после того, как ввод слова в строку поиска был завершён (например по прошествии 300 мс с момента ввода последнего символа). Из-за этого фильтрация всегда ощущается заторможенной вне зависимости от размера таблицы.
useDeferredValue же фильтрует сразу по мере ввода запроса. Если скорость рендеринга больше скорости ввода (например потому что данных в таблице пока что немного), то скорость отображения результатов этого рендеринга практически идентична тому, какой она была бы, будь этот рендеринг синхронным. Если же скорость рендеринга меньше скорости ввода, то React с каждым новым нажатием клавиши начинает новую попытку отрендерить таблицу на фоне, а предыдущую попытку при этом просто завершает, потому что она больше неактуальна. Так продолжается до тех пор, пока не выходит завершить рендеринг быстрее, чем пользователь нажимает следующую клавишу. Ресурсы расходуются максимально эффективно, при этом пользователь не чувствует никакой заторможенности.
Насчёт узконаправленности хуков под рендер реакта: я не думаю, что это соответствует действительности. Возможность прервать рендеринг имхо должна быть во всех фронтенд-фреймворках, поскольку в каждом из них возможна ситуация, когда рендеринг занимает дольше 1000/60 = 16,67 мс, и таким образом в случае его синхронности страница начинает ощущаться заторможенной, как если бы показатель FPS упал ниже 60 кадров в секунду. Но по какой-то причине в то время как разработчики React задумались над решением этой проблемы ещё 10 лет назад (о чём подробнее рассказано в этом отличном видео: https://youtu.be/edN42P_vfCI), другие фреймворки по сей день не считают её приоритетной.
В ролике не объясняется, каким образом удалось достичь сохранения DOM состояния. Предположу, что это было сделано за счёт сохранения DOM узла в памяти после его удаления из дерева документа с помощью removeChild() / remove(), чтобы потом этот же узел можно было добавить обратно. Других способов полностью сохранить DOM состояние я не знаю.
Так или иначе, узел со всеми его подузлами должен оставаться в памяти, и поэтому мне совсем непонятно, с какой это стати такой подход лучше масштабируется. По-моему вся разница между ним и подходом команды React с display: none сводится к тому, видно ли скрытые узлы в инструментах разработчика. Сказать, что эта разница сильно влияет на масштабируемость, было бы нехилым таким преувеличением.
Преимущество подхода с display: none в том, что помимо состояния, контролируемого React, сохраняется также и неконтролируемое состояние вроде значений <input>'ов и времени, на котором было остановлено проигрывание видео. Оба этих примера приведены в документации:
В основе Vue так же как и в основе React лежит концепция Virtual DOM, и, насколько я могу судить, так же как и в React компоненты обновляются целыми деревьями начиная от того компонента, где произошло изменение состояния. Поэтому проблемы, которые решают useTransition и useDeferredValue в React, ровно в той же степени актуальны и для Vue.
В моей карьере случаев, когда приходилось пользоваться этими хуками, было не так уж и много, но каждый раз я радовался тому, что работаю с React, а не другим фреймворком, потому что иначе пришлось бы пользоваться костылями вроде предложенного здесь варианта с useDebounceFn: https://dev.to/jacobandrewsky/handling-large-lists-efficiently-in-vue-3-4im1
Если надо, могу объяснить, почему это даже близко не то же самое.
Насколько мне известно, похожих на useTransition / useDeferredValue механизмов нет ни в одном другом фреймворке кроме React, и одного лишь этого факта мне уже достаточно для того, чтобы считать React более пригодным для по-настоящему сложных приложений, чем какое бы то ни было иное решение.
В том-то и прикол, что нельзя на Vue ничего такого написать, потому что рендеринг там заведомо синхронная операция. Эта идея обречена на провал, так же как и попытки найти способ вмешаться в цикл for из моего первого примера. Чтобы его можно было прервать, он изначально должен быть реализован таким образом, который позволил бы это сделать, но это, увы, не так. Та же ситуация и с рендерингом во Vue.
Ни
nextTick, ниrequestIdleCallback, ниsetTimeout, ни что-либо ещё в от природы своей однопоточном джаваскрипте не способно прервать выполнение синхронного кода. Вот пример, который позволяет в этом убедиться:Хоть мы и запросили вывод строки “a second passed” через секунду, на деле выводится она лишь после того, как завершается выполнение цикла for, а это происходит намного позже. Связано это с тем, что цикл выполняется синхронно, то есть не уступая дорогу никакому другому коду, пока цикл не будет выполнен полностью.
По той же самой причине строка “for loop done” появляется в консоли перед “a second passed”, хотя на момент её вывода прошло уже сильно больше одной секунды. Никакой код не может влезть ни между итерациями цикла, ни между циклом и выводом “a second passed”. Всё это синхронный код — последовательность выполняемых друг за другом инструкций, и возможности прервать его просто не существует. Чтобы такая возможность была, код надо сделать асинхронным, например вот так:
Вычисление здесь мы разделили на 1000 равных по продолжительности частей, в конце каждой из которых с помощью
await tick()контроль передаётся циклу событий (event loop), который продолжает выполнение цикла for лишь после того, как будут выполнены все остальные ждущие в очереди задачи. Примером такой задачи может быть выполнение переданнойsetTimeoutфункции по истечении указанного срока (в нашем случае это вывод строки “a second passed” по прошествии одной секунды). Другой пример — считывание взаимодействий пользователя со страницей, которые прежде были полностью заблокированы на время вычисления цикла for. Если запустите сначала первый, а потом второй пример в консоли браузера и во время вычисления попробуете нажать куда-нибудь или ввести текст на странице, к которой эта консоль привязана, разница будет очевидна.Теперь наконец можем вернуться к
useTransitionиuseDeferredValue. Весь смысл этих хуков в том, что они превращают изначально синхронную операцию рендеринга дерева компонентов в асинхронную так же, как это было проделано мной с циклом for в примере выше. Оба хука позволяют выполнять тяжеловесные обновления UI на фоне таким образом, чтобы это никак не мешало более срочным синхронным обновлениям и взаимодействию пользователя со страницей, аuseDeferredValueещё и позволяет отменять обновления, которые становятся неактуальны из-за изменившихся вводных данных — например когда пользователь быстро вбивает слово в строку поиска, чтобы отфильтровать по нему данные в таблице. Это как раз тот самый пример, где Vue разработчикам предлагается использоватьuseDebounceFn. ПреимуществоuseDeferredValueпо-моему довольно очевидно:Насчёт узконаправленности хуков под рендер реакта: я не думаю, что это соответствует действительности. Возможность прервать рендеринг имхо должна быть во всех фронтенд-фреймворках, поскольку в каждом из них возможна ситуация, когда рендеринг занимает дольше 1000/60 = 16,67 мс, и таким образом в случае его синхронности страница начинает ощущаться заторможенной, как если бы показатель FPS упал ниже 60 кадров в секунду. Но по какой-то причине в то время как разработчики React задумались над решением этой проблемы ещё 10 лет назад (о чём подробнее рассказано в этом отличном видео: https://youtu.be/edN42P_vfCI), другие фреймворки по сей день не считают её приоритетной.
В ролике не объясняется, каким образом удалось достичь сохранения DOM состояния. Предположу, что это было сделано за счёт сохранения DOM узла в памяти после его удаления из дерева документа с помощью removeChild() / remove(), чтобы потом этот же узел можно было добавить обратно. Других способов полностью сохранить DOM состояние я не знаю.
Так или иначе, узел со всеми его подузлами должен оставаться в памяти, и поэтому мне совсем непонятно, с какой это стати такой подход лучше масштабируется. По-моему вся разница между ним и подходом команды React с display: none сводится к тому, видно ли скрытые узлы в инструментах разработчика. Сказать, что эта разница сильно влияет на масштабируемость, было бы нехилым таким преувеличением.
Преимущество подхода с display: none в том, что помимо состояния, контролируемого React, сохраняется также и неконтролируемое состояние вроде значений <input>'ов и времени, на котором было остановлено проигрывание видео. Оба этих примера приведены в документации:
https://react.dev/reference/react/Activity#restoring-the-dom-of-hidden-components
https://react.dev/reference/react/Activity#my-hidden-components-have-unwanted-side-effects
Логики в этом по-моему больше, чем в сохранении лишь той части состояния, которая контролируется фреймворком.
В основе Vue так же как и в основе React лежит концепция Virtual DOM, и, насколько я могу судить, так же как и в React компоненты обновляются целыми деревьями начиная от того компонента, где произошло изменение состояния. Поэтому проблемы, которые решают useTransition и useDeferredValue в React, ровно в той же степени актуальны и для Vue.
В моей карьере случаев, когда приходилось пользоваться этими хуками, было не так уж и много, но каждый раз я радовался тому, что работаю с React, а не другим фреймворком, потому что иначе пришлось бы пользоваться костылями вроде предложенного здесь варианта с useDebounceFn: https://dev.to/jacobandrewsky/handling-large-lists-efficiently-in-vue-3-4im1
Если надо, могу объяснить, почему это даже близко не то же самое.
Насколько мне известно, похожих на useTransition / useDeferredValue механизмов нет ни в одном другом фреймворке кроме React, и одного лишь этого факта мне уже достаточно для того, чтобы считать React более пригодным для по-настоящему сложных приложений, чем какое бы то ни было иное решение.
В React начиная с версии 19.2 доступен компонент <Activity>, функция которого насколько я понимаю такая же, как у <KeepAlive> из Vue.
А что предлагает Vue в качестве альтернативы реактовским useTransition / useDeferredValue?