В этой статье мы узнаем, что такое мемоизация и как она была реализована внутри Redux Reselect.
Что такое мемоизация?
Мемоизация — это метод ускорения работы приложений за счет кэширования результатов ресурсоемких операций внутри функций и возврата кэшированных результатов с теми же входными данными, которые использовались повторно.
Как мы можем реализовать это с помощью VanillaJs и es5:
interface MemoizedFunction extends Function { (param: any): any; cache: { [key: string]: any; }; } export const memoizationExample = function (param: any) { // Checking is result already calculated for same entry if (!memoizationExample.cache[param]) { console.log('not cached'); const res = { data: param + 5 }; // Some heavy operations // Adding calculated result to cache memoizationExample.cache[param] = res; } console.log('outside'); return memoizationExample.cache[param]; } as MemoizedFunction; memoizationExample.cache = {}; let resDef = memoizationExample(11); // First call calculating data let resDef2 = memoizationExample(11); // Second call will get data from cache console.log(resDef, resDef2, memoizationExample.cache); // { data: 16 }, { data: 16 }, { "11": { "data": 16 } }
В JavaScript функции — это объекты, поэтому мы можем добавить свойство cache в memoizationExample и сохранить там кешированные данные. При первом вызове функции она проверит, есть ли в кеше уже сохраненный результат для этой записи. В случае, если кешированного результата нет, он войдет внутрь и выполнит некоторые тяжелые операции для получения данных и после того, как мы их сохраним. В случае, если уже есть кешированный результат, мы пропустим вычисления и просто вернем кешированные данные.
Использование карты и WeakMap
Объект Map
содержит пары ключ-значение и запоминает исходный порядок вставки ключей. Любое значение (как объекты, так и примитивные значения) может быть использовано либо как ключ, либо как значение.
А WeakMap
— это набор пар ключ/значение, ключи которого должны быть объектами, не создающими сильных ссылок на свои ключи. То есть наличие объекта в качестве ключа в WeakMap не препятствует сборке мусора.
let data = { value: 1 }; let map = new Map(); map.set(data, data); data = null; // removing link to the object // but object still saved inside `Map`, // we can get it with map.keys()/map.values() let weakData = { value: 1}; let weakMap = new WeakMap(); weakMap.set(weakData, weakData); john = null; // removing link to the object // And now when garbadge collector will be free will remove an object.
Таким образом, основное различие между этими двумя заключается в том, что в WeakMap вы можете использовать только объекты (также функции, поскольку они тоже являются объектами), а слабая карта не создает сильной ссылки, поэтому сборщик мусора может очистить неиспользуемые данные. Также WeakMap не имеет методов keys(), values().
Как мы можем реализовать мемоизацию с помощью Map/WeakMap
const cacheMap: Map<any, any> = new Map(); const cacheWeakMap: WeakMap<Object, any> = new WeakMap(); export const memoizationMap = (data: any) => { // Checking is result already calculated for same entry if(typeof data === 'object'){ if (!cacheWeakMap.has(data)) { console.log('not cached WeakMap'); let res = data.value + 5; // Some heavy operations // Adding calculated result to cache cacheWeakMap.set(data, res); } console.log('outside WeakMap'); return cacheWeakMap.get(data); } else { if (!cacheMap.has(data)) { console.log('not cached Map'); let res = data + 5; // Some heavy operations // Adding calculated result to cache cacheMap.set(data, res); } console.log('outside Map'); return cacheMap.get(data); } }; let data: number | null = 4; let res1 = memoizationMap(data); let res2 = memoizationMap(data); // data from cache
Итак, для объекта, который мы знаем, лучше использовать WeakMap для удаления неиспользуемых данных, а для других примитивов мы будем использовать Map.
Как мемоизация реализована внутри Reselect Redux(createSelector):
type EqualityType = (a: any, b: any) => boolean; // Ussual compearing export const defaultEqualityCheck: EqualityType = (a, b) => { return a === b; }; // Deep compearing arguments function areArgumentsShallowlyEqual(equalityCheck: EqualityType, prev: any, next: any) { // If either arguments is null, or if their lengths aren't the same, they aren't equal if (prev === null || next === null || prev.length !== next.length) { return false; } const length = prev.length; for (let i = 0; i < length; i++) { if (!equalityCheck(prev[i], next[i])) { return false; } } return true; } function createSelector(...funcs: any[]): (state: { numbers: number[]; strings: string[] }) => any { let lastArgs: any = null; let lastResult: any = null; return function () { // Checking is result already calculated for same entry if (!areArgumentsShallowlyEqual(defaultEqualityCheck, lastArgs, arguments)) { console.log('not cached selector'); const resultFunc = funcs.pop(); //This is the function that calculates the result // Getting data fom selectors and paste data as props inside resultFunc lastResult = resultFunc(...funcs.map(func => func.apply(null, arguments))); lastArgs = arguments; } console.log('outside selector'); return lastResult; }; } // Usage const stateStore: { numbers: number[]; strings: string[] } = { numbers: [1, 2, 3, 4, 5], strings: ['first', 'second', 'third', 'fourth', 'fifth'], }; const getNumbers = (state: { numbers: number[] }) => state.numbers; const getSum = createSelector(getNumbers, (numbers: any[]) => numbers.reduce((a, b) => a + b, 0)); console.log(getSum(stateStore)); console.log(getSum(stateStore)); // cache 15
Я реализовал упрощенную версию того, что есть у Reselect. Они используют defaultEqualityCheck и areArgumentsShallowlyEqual, чтобы узнать, были ли изменены аргументы. В случае отсутствия кеша они получают последний параметр из аргументов, который является resultFunc, он принимает сопоставление данных между селекторами с переданным Store в качестве аргументов.
Ссылка на повторный выбор мемоизации по умолчанию: https://github.com/reduxjs/reselect /blob/master/src/defaultMemoize.ts
Реализация с помощью WeakMap.
function createSelectorWeakMap(...funcs: any[]) { let memoizedResults: WeakMap<any, any> = new WeakMap(); return function (...args: any[]) { // Arg is array, so we need get obj as key into WeakMap which is out store const argsKey = args[0]; if (memoizedResults.has(argsKey)) { console.log('weak cache'); return memoizedResults.get(argsKey); } else { const resultFunc = funcs.pop(); const selectorsRes = funcs.map(func => { // Check for several selectors const selectors = Array.isArray(func) && func.map((item: Function) => item.apply(null, arguments)); return Array.isArray(func) ? selectors : func.apply(null, arguments); }); // Check for several selectors to remove matrix const isMatrix = Array.isArray(selectorsRes[0][0]) ? selectorsRes.flat() : selectorsRes; // Blocking changing state with Object freeze const result = resultFunc(...isMatrix.map(item => Object.freeze(item))); memoizedResults.set(argsKey, result); return result; } }; } const getNumbersWeak = (state: { numbers: number[] }) => state.numbers; const getStringsWeak = (state: { strings: string[] }) => state.strings; const getSumWeak = createSelectorWeakMap([getNumbersWeak, getStringsWeak], (numbers: number[], strings: string[]) => { console.log('number', Object.isFrozen(numbers), strings); // numbers[1] = '2' Will give an error return numbers.reduce( (a, b, currentIndex) => { return { sum: a.sum + b, str: a.str + strings[currentIndex] }; }, { sum: 0, str: '' }, ); }); console.log('weak', getSumWeak(stateStore)); // calculates and logs 15 console.log('weak', getSumWeak(stateStore)); // returns memoized 15
Как мы знаем, метод Reselect createSelector всегда принимает функции-аргументы, поэтому лучше использовать WeakMap для очистки памяти с помощью сборщика мусора. В этой реализации я также добавил возможность указывать путь в виде массива к нескольким селекторам, чтобы получать от них данные, и Object.freeze(), чтобы предотвратить мутацию. Хранить данные.
Ссылка на мемоизацию WeakMap, там же добавили проверку на примитивы и использовали Map:
https://github.com/reduxjs/reselect/blob/master/src/weakMapMemoize.ts
Ссылка на повторное выделение createSelector: https://github.com/reduxjs/reselect/blob/master/src/index.ts#L80
Заключение
Меня очень интересовало общее поведение Memoization и то, как Reselect CreateSelector был реализован в Redux. Я надеюсь, что эта статья прольет свет на мемоизацию.
Если у вас есть какие-либо предложения, не стесняйтесь добавлять комментарии.
Исходный код: https://github.com/privalovbogdan/memoization-reselect