Эта статья является продолжением Этого.
Батут и Thunk
В разделе рекурсии части 1 была проблема с размером стека. Чтобы решить эту проблему, я хотел бы представить метод под названием батут.
Прежде чем перейти к рассмотрению того, что такое батут, необходимо пояснить концепцию преобразователя.
Преобразователь - это вызов функции, заключенный в анонимную функцию. Его основное использование - отсрочить выполнение.
const f = (x, y) => x * y;
const f_thunk = () => f(2, 3); // f function is wrapped // in an anonymous function
const val = f_thunk(); // f function is executed // by calling f_thunk function
console.log(val); // 6
thunk
функцию можно определить как:
// thunk function takes a function and the arguments // for the function, and returns a function that // will call the passed function with the arguments.
const thunk = (f, ...args) => () => f(...args);
В части 1 функция factorial
была определена как:
const factorial = n => {
const _factorial = (n, accum) =>
n < 2 ? accum : _factorial(n - 1, n * accum);
return _factorial(n, 1);
};
С thunk
вышесказанное можно переписать:
const factorial = n => { const _factorial = (n, accum) => n < 2 ? accum : thunk(_factorial, n - 1, n * accum);
return _factorial(n, 1); };
let val = factorial(3); // () => _factorial(2, 3) val = val(); // () => _factorial(1, 6) val = val(); // 6
Каждый раз, когда вызывается val
, он возвращает преобразователь, который выполнит следующий рекурсивный шаг. На этом шаге удается отделить рекурсивные функции от стека вызовов.
Но было бы лучше, если бы у нас был способ автоматически вызывать функции преобразователя вместо того, чтобы вызывать их вручную по очереди.
Здесь нам на помощь приходит функция trampoline
. Механика этого очень проста. Он в основном вызывает каждый преобразователь до тех пор, пока не будет возвращено что-то отличное от функции.
const trampoline = f => { let result = f();
while(result && result instanceof Function) { result = result(); }
return result; };
С trampoline
, factorial
теперь можно переписать как:
const factorial = n => { const _factorial = (n, accum) => n < 2 ? accum : thunk(_factorial, n - 1, n * accum);
return trampoline(_factorial(n, 1)); };
console.log(factorial(10)); // 3628800 console.log(factorial(3628800)); // Infinity
Также для вывода трассировки стека в функцию factorial
можно добавить console.trace()
:
// first attempt const factorial = n => { console.trace();
return n < 2 ? 1 : n * factorial(n - 1); };
console.log(factorial(3628800)); // Uncaught RangeError: Maximum call stack size exceeded
// tail optimized const factorial = n => { console.trace(); const _factorial = (n, accum) => n < 2 ? accum : _factorial(n - 1, n * accum);
return _factorial(n, 1); };
console.log(factorial(3628800)); // Uncaught RangeError: Maximum call stack size exceeded
// with trampoline and thunk const factorial = n => { console.trace(); const _factorial = (n, accum) => n < 2 ? accum : thunk(_factorial, n - 1, n * accum); return trampoline(_factorial(n, 1)); };
console.log(factorial(3628800)); // Infinity
Он добавляет больше шагов для создания рекурсивной функции, но когда у нас есть thunk
и trampoline
функции, их можно использовать повторно, и их добавление требует лишь небольшого дополнительного набора текста.
Глобальные состояния
Еще одна тема, которую я хочу затронуть в этой статье, - это способ управления глобальными состояниями.
В части 1 в качестве побочного эффекта была представлена мутация. Однако иногда необходимы глобальные состояния, и их нужно изменять для обновлений. Один из способов решить эту проблему - создать объект, который управляет всеми состояниями подобно тому, что делает Redux.
В упрощенном объяснении Redux создает объект, который действует как глобальное хранилище, с тремя функциями: getState
, subscribe
и dispatch
.
dispatch
используется для вызова функций reducer, которые обновляют состояние в магазине.
Когда состояние обновляется, вызывается функция subscribe
, чтобы уведомить об обновлении зарегистрированные функции прослушивателя.
Наконец, getState
используется для получения состояния в магазине.
Ожидается, что функции reducer будут чистыми функциями, и состояние, полученное функцией getState
, не должно изменяться.
const createStore = (reducer, initialState = {}) => { let listeners = []; let state = initialState; let isDispatching = false;
function getState() { return state; }
function subscribe(listener) { let isSubscribed = true; listeners.push(listener);
return function unsubscribe() { if(!isSubscribed) { return; }
isSubscribed = false; const index = listeners.indexOf(listener); listeners.splice(index, 1); }; }
function dispatch(action) { if(isDispatching) { throw new Error('action may not be dispatched'); }
try { isDispatching = true; state = reducer(state, action); } finally { isDispatching = false; }
for(let i = 0, len = listeners.length; i < len; i++) { const listener = listeners[i]; listener(); }
return action; } return { dispatch, subscribe, getState }; };
Примечание: приведенный выше код упрощен от оригинала.
Приведенная выше функция createStore
принимает reducer
и initialState
и возвращает объект хранилища. initialState
, как следует из названия, используется для установки начального состояния в магазине. reducer
- это функция, которая вызывается функцией dispatch
. reducer
функция принимает объект состояния и объект действия и возвращает новый объект состояния. reducer
можно создать с помощью combineReducers
функции, которая принимает reducers
, который представляет собой объект с методами, которые будут обновлять состояние в магазине. Название методов также должно соответствовать ключам состояния.
const combineReducers = reducers => { const reducerKeys = Object.keys(reducers); let finalReducers = {};
reducerKeys.forEach(key => { if(typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; } });
const finalReducerKeys = Object.keys(finalReducers);
return (state = {}, action) => { let hasChanged = false; const nextState = {};
for(let i = 0, len = finalReducerKeys.length; i < len; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state; }; };
Примечание: приведенный выше код упрощен от оригинала.
Например, установив состояние как:
const initialState = { name: '', age: null };
Редукторы будут:
const reducers = { name: function(state = '', action) { switch(action.type) { case 'UPDATE_NAME': return action.name;
default: return state; } }, age: function(state = 0, action) { switch(action.type) { case 'UPDATE_AGE': return action.age;
default: return state; } } };
Методы в редукторах принимают state
в качестве аргумента, который соответствует значению в состоянии, ключ которого совпадает с именем метода. Итак, в приведенном выше случае состояние name
передается name
методу.
Другой аргумент, который принимают методы, - это action
объект. Он передается из dispatch
функции и должен иметь type
, чтобы предлагать тип действия, которое должен выполнить метод. Другие ключи в объекте action
- это значения для обновления состояния.
dispatch
функция используется для вызова reducer
функции извне.
В приведенном выше случае:
// update state.name with the value 'John' dispatch({ type: 'UPDATE_NAME', name: 'John' });
// update state.age with the value 22 dispatch({ type: 'UPDATE_AGE', age: 22 });
А в целом его можно использовать как:
const initialState = { name: '', age: null };
const reducers = { name: function(state = '', action) { switch(action.type) { case 'UPDATE_NAME': return action.name;
default: return state; } }, age: function(state = null, action) { switch(action.type) { case 'UPDATE_AGE': return action.age;
default: return state; } } };
const store = createStore(combineReducers(reducers), initialState);
// to get notification when the state is updated store.subscribe(() => { console.log(store.getState()); });
// update state.name store.dispatch({ type: 'UPDATE_NAME', name: 'John' }); // now the state should be { name: 'John', age: null }
// update state.age store.dispatch({ type: 'UPDATE_AGE', age: 22 }); // now the state should be { name: 'John', age: 22 }
Вот и все!
В оригинальном Redux есть проверка ошибок и многие другие возможности. Он также короткий и лаконичный, поэтому его легко читать и понимать, что он делает (в нем разумно используются замыкания). Я многому научился, читая код.