Эта статья является продолжением Этого.

Батут и 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 есть проверка ошибок и многие другие возможности. Он также короткий и лаконичный, поэтому его легко читать и понимать, что он делает (в нем разумно используются замыкания). Я многому научился, читая код.