прокси-сравнение и прокси-мемоизирование
Введение
Прошло много времени с тех пор, как я начал разрабатывать reactive-react-redux и response-tracked. Эти библиотеки обеспечивают так называемое отслеживание использования состояния для оптимизации рендеринга в React. Этот подход, на мой взгляд, довольно новый, и я приложил немало усилий, чтобы улучшить его производительность.
В последнее время я подумал, что было бы лучше, если бы это можно было использовать более широко. Мне было интересно, можно ли его использовать в ванильном JS. Каким будет API в ванильном JS? Было бы хорошо, если бы это было легко понять. Моя идея закончилась мемоизацией, в основном потому, что основная цель - заменить повторный выбор.
Новая библиотека называется proxy-memoize
.
прокси-мемоизация
GitHub: https://github.com/dai-shi/proxy-memoize
Библиотека proxy-memoize
предоставляет функцию запоминания. Он примет функцию и вернет мемоизированную функцию.
import memoize from 'proxy-memoize';
const fn = (x) => ({ foo: x.foo }); const memoizedFn = memoize(fn);
В этой библиотеке есть большой выбор дизайна. Мемоизируемая функция должна быть функцией, которая принимает в качестве аргумента ровно один объект. Таким образом, функции, подобные ниже, не поддерживаются.
const unsupportedFn1 = (number) => number * 2;
const unsupportedFn2 = (obj1, obj2) => [obj1.foo, obj2.foo];
Это позволит кэшировать результаты с помощью WeakMap
. Мы можем кэшировать столько результатов, сколько захотим, и позволить JS-мусору собирать мусор, когда они перестают быть эффективными.
Прокси-серверы используются, если мы не находим результат в WeakMap
кеше. Мемоизированная функция вызывает исходную функцию с объектом аргумента, обернутым прокси. Прокси-серверы отслеживают использование свойств объекта при вызове функции. Отслеживаемая информация называется «затронутой», которая представляет собой частичную древовидную структуру исходного объекта. Для простоты в этом посте мы используем точечную нотацию.
Давайте посмотрим на следующие примеры.
const obj = { a: 1, b: { c: 2, d: 3 } };
// initially affected is empty
console.log(obj.a) // touch "a" property
// affected becomes "a"
console.log(obj.b.c) // touch "b.c" property
// affected becomes "a", "b.c"
После создания «затронутого» объекта он может проверить новый объект, если затронутые свойства изменены. Только в случае изменения какого-либо из затронутых свойств функция повторно вызовет функцию. Это позволит очень точно настроить мемоизацию.
Давайте посмотрим на пример.
const fn = (obj) => obj.arr.map((x) => x.num); const memoizedFn = memoize(fn);
const result1 = memoizedFn({ arr: [ { num: 1, text: 'hello' }, { num: 2, text: 'world' }, ], })
// affected is "arr[0].num", "arr[1].num" and "arr.length"
const result2 = memoizedFn({ arr: [ { num: 1, text: 'hello' }, { num: 2, text: 'proxy' }, ], extraProp: [1, 2, 3], })
// affected properties are not change, hence: result1 === result2 // is true
Отслеживание использования и сравнение затронутых событий выполняется внутренней библиотекой proxy-compare.
прокси-сравнение
GitHub: https://github.com/dai-shi/proxy-compare
Это библиотека, извлеченная из response-tracked, чтобы обеспечить только функцию сравнения с прокси. (На самом деле, версия 2 с отслеживанием реакции будет использовать эту библиотеку как зависимость.)
Библиотека экспортирует две основные функции: createDeepProxy
и isDeepChanged
.
Это работает следующим образом:
const state = { a: 1, b: 2 };
const affected = new WeakMap();
const proxy = createDeepProxy(state, affected);
proxy.a // touch a property
isDeepChanged(state, { a: 1, b: 22 }, affected) // is false
isDeepChanged(state, { a: 11, b: 2 }, affected) // is true
state
может быть вложенным объектом, и только при касании свойства создается новый прокси. Важно отметить, что affected
предоставляется извне, что упростит его интеграцию в хуки React.
Есть и другие моменты об улучшении производительности и работе с крайними случаями. Мы не будем вдаваться в подробности в этом посте.
Использование с React Context
Как обсуждалось в прошлом сообщении, одним из вариантов является использование useMemo. Если использовать proxy-memoize с useMemo, мы сможем получить такое же преимущество, как и response-tracked.
import memoize from 'proxy-memoize';
const MyContext = createContext();
const Component = () => { const [state, dispatch] = useContext(MyContext); const render = useMemo(() => memoize(({ firstName, lastName }) => ( <div> First Name: {firstName} <input value={firstName} onChange={(event) => { dispatch({ type: 'setFirstName', firstName: event.target.value }); }} (Last Name: {lastName}) /> </div> )), [dispatch]); return render(state); };
const App = ({ children }) => ( <MyContext.Provider value={useReducer(reducer, initialState)}> {children} </MyContext.Provider> );
Component
будет повторно отображаться при изменении контекста. Однако он возвращает мемоизированное дерево элементов реакции, если firstName
не был изменен. Итак, повторный рендеринг на этом заканчивается. Это поведение отличается от поведения с отслеживанием реакции, но оно должно быть достаточно оптимизировано.
Использование с React Redux
Повторный выбор может быть простой заменой.
import { useDispatch, useSelector } from 'react-redux'; import memoize from 'proxy-memoize';
const Component = ({ id }) => { const dispatch = useDispatch(); const selector = useMemo(() => memoize((state) => ({ firstName: state.users[id].firstName, lastName: state.users[id].lastName, })), [id]); const { firstName, lastName } = useSelector(selector); return ( <div> First Name: {firstName} <input value={firstName} onChange={(event) => { dispatch({ type: 'setFirstName', firstName: event.target.value }); }} /> (Last Name: {lastName}) </div> ); };
Это может быть слишком просто, чтобы продемонстрировать возможности proxy-memoize, один из интересных вариантов использования будет следующим.
memoize((state) => state.users.map((user) => user.firstName))
Это будет переоценено только в случае изменения длины users
или изменения одного из firstName
. Он продолжает возвращать кешированный результат даже при изменении lastName
.
Заключительные примечания
На развитие меня вдохновили отношения между MobX и Immer. Я вообще не знаком с их реализациями, но мне кажется, что Immer - это подмножество MobX для более широких вариантов использования. Я хотел создать что-то вроде Immer. Immer позволяет волшебным образом преобразовывать изменяемые операции (запись) в неизменяемые объекты. proxy-memoize позволяет волшебным образом создавать функции выбора (чтения) для неизменяемых объектов.
Первоначально опубликовано на https://blog.axlight.com 29 ноября 2020 г.