Redux действительно не сложен — интерпретация исходного кода

внешний интерфейс исходный код React.js Redux

предисловие

Чтение объекта: Разработчики, которые использовали redux и не понимают принцип реализации redux.

Во время моей стажировки и вводного обучения старший брат, который обучал меня, сказал мне, что основной исходный код Redux очень прост.

Прошло больше месяца с тех пор, как я присоединился к компании, и я участвовал во многих проектах компании.Я использую redux некоторое время, но у меня нет глубокого понимания redux.Он все еще находится на стадии «знание того, как его использовать, но не знание его основных принципов».

Итак, я вытащил исходный код Redux на github, Почитав его некоторое время, я обнаружил, что там не так много вещей, и он относительно прост.

Какова функция самого редукса

В проектах мы часто не используем redux чисто, но будете сотрудничать с другими библиотеками инструментов для повышения эффективности, например,react-redux, упрощая приложениям реагирования использование избыточности и т.п.wepy-redux, библиотека инструментов, предоставляемая фреймворку апплета wepy.

Но в этой статье сфера нашего обсуждения чистосам редукс.

Какова роль самого редукса? Давайте кратко рассмотрим основную идею (рабочий процесс) redux:

  • Состояние объединено в одно состояние, и хранилище управляет этим состоянием.
  • Этот магазин создается по «форме» редуктора.
  • Роль редьюсера состоит в том, чтобы вывести новое состояние после получения действия и соответствующим образом обновить состояние в хранилище.
  • В соответствии с принципом редукции лучший способ изменить состояние извне — запустить действие, вызвав метод отправки хранилища, которое обрабатывается соответствующим редюсером для завершения обновления состояния.
  • Функция прослушивателя может быть добавлена ​​в хранилище посредством подписки. Всякий раз, когда вызывается метод отправки, выполняются все функции прослушивателя.
  • Вы можете добавить промежуточное ПО (мы поговорим о том, что делает промежуточное ПО позже) для обработки побочных эффектов.

В этом рабочем процессе функции, которые должен предоставить Redux:

  • Создание магазина, а именно:createStore()
  • Созданный магазин предоставляетsubscribe,dispatch,getStateэти методы.
  • Объединить несколько редукторов в один редуктор, т.е.:combineReducers()
  • Промежуточное ПО приложения, т.е.applyMiddleware()

Правильно, с таким количеством функций, давайте посмотрим на каталог с исходным кодом redux:

redux的源码目录

Это действительно так много, что касаетсяcompose,bindActionCreators, которые являются некоторыми инструментальными методами.

Давайте посмотрим один за другимcreateStore,combineReducers,applyMiddleware,composeреализация исходного кода.

Рекомендуется открыть ссылку:исходный адрес редукса, прочитайте исходный код со ссылкой на объяснение в этой статье.

Реализация createStore

Общая структура этой функции выглядит следующим образом:

function createStore(reducer, preloadedState, enhancer) {
    if(enhancer是有效的){  // 这个我们后面会解释,可以先忽略
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    let currentReducer = reducer // 当前store中的reducer
    let currentState = preloadedState // 当前store中存储的状态
    let currentListeners = [] // 当前store中放置的监听函数
    let nextListeners = currentListeners // 下一次dispatch时的监听函数
    // 注意:当我们新添加一个监听函数时,只会在下一次dispatch的时候生效。
    
    //...
    
    // 获取state
    function getState() {
        //...
    }
    
    // 添加一个监听函数,每当dispatch被调用的时候都会执行这个监听函数
    function subscribe() {
        //...
    }
    
    // 触发了一个action,因此我们调用reducer,得到的新的state,并且执行所有添加到store中的监听函数。
    function dispatch() {
        //...
    }
   
    //...
    
    //dispatch一个用于初始化的action,相当于调用一次reducer
    //然后将reducer中的子reducer的初始值也获取到
    //详见下面reducer的实现。
    
    
    return {
        dispatch,
        subscribe,
        getState,
        //下面两个是主要面向库开发者的方法,暂时先忽略
        //replaceReducer,
        //observable
    }
}

Видно, что метод createStore создает хранилище, но не возвращает состояние хранилища напрямую, а возвращает серию методов, которые можно использовать извне для получения состояния через эти методы (getState), либо косвенно (путем вызывающая диспетчеризация) изменить состояние.

Что касается состояния, то оно хранится в замыкании. (Студенты, которые не понимают замыкания, могут сначала понять)

Давайте подробно рассмотрим, как реализован каждый модуль (чтобы сделать логику более понятной, код обработки ошибок опущен):

getState
function getState() {
    return currentState
}

Просто, как черт. На самом деле это очень похоже на метод инкапсуляции свойств только для чтения в объектно-ориентированном программировании, который предоставляет только метод получения данных, а не непосредственно метод установки. (Хотя здесь возвращается ссылка на состояние, вы можете изменить состояние напрямую, но, как правило, Redux не рекомендует это делать.)

subscribe
function subscribe(listener) {
    // 添加到监听函数数组,
    // 注意:我们添加到了下一次dispatch时才会生效的数组
    nextListeners.push(listener)
    
    let isSubscribe = true //设置一个标志,标志该监听器已经订阅了
    // 返回取消订阅的函数,即从数组中删除该监听函数
    return function unsubscribe() {
        if(!isSubscribe) {
            return // 如果已经取消订阅过了,直接返回
        }
        
        isSubscribe = false
        // 从下一轮的监听函数数组(用于下一次dispatch)中删除这个监听器。
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}

subscribe возвращает метод для отмены подписки. Отписаться очень нужно.Когда добавленный слушатель бесполезен, его следует убрать из магазина. В противном случае этот бесполезный прослушиватель будет вызываться при каждой отправке.

dispatch
function dispatch(action) {
    //调用reducer,得到新state
    currentState = currentReducer(currentState, action);
    
    //更新监听数组
    currentListener = nextListener;
    //调用监听数组中的所有监听函数
    for(let i = 0; i < currentListener.length; i++) {
        const listener = currentListener[i];
        listener();
    }
}

Мы уже реализовали базовую функцию метода createStore, но вызов метода createStore должен предоставить редьюсер.Давайте подумаем о роли редьюсера.

combineReducers

Перед тем, как разобраться с combReducers, давайте сначала подумаем о функции редукторов: редукторы принимают старое состояние и действие, а когда действие запускается, редюсер после обработки возвращает новое состояние.

То есть редюсер отвечает за управление состоянием (или обновление). В реальном использовании состояние нашего приложения можно разделить на множество модулей, например, состояние типичного сайта социальной сети можно разделить на такие модули, как личная информация пользователя, список друзей и список сообщений. Теоретически мы можем использовать один редюсер для управления всеми состояниями, но при этом наша функция редуктора будет иметь слишком много логики, что может привести к путанице.

Таким образом, мы можем разделить логику, разделить каждый модуль на каждый подмодуль, а затем логику каждого модуля, чтобы нашу логику можно было объединить.

Для наших нужд redux предоставляет метод combReducers, который может объединять подредукторы в общий редьюсер.

Давайте посмотрим на основную логику combReducers в исходном коде redux:

function combineReducers(reducers) {
    //先获取传入reducers对象的所有key
    const reducerKeys = Object.keys(reducers)
    const finalReducers = {} // 最后真正有效的reducer存在这里
    
    //下面从reducers中筛选出有效的reducer
    for(let i = 0; i < reducerKeys.length; i++){
        const key  = reducerKeys[i]
        
        if(typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key] 
        }
    }
    const finalReducerKeys = Object.keys(finalReducers);
    
    //这里assertReducerShape函数做的事情是:
    // 检查finalReducer中的reducer接受一个初始action或一个未知的action时,是否依旧能够返回有效的值。
    let shapeAssertionError
  	try {
    	assertReducerShape(finalReducers)
  	} catch (e) {
    	shapeAssertionError = e
  	}
    
    //返回合并后的reducer
    return function combination(state= {}, action){
  		//这里的逻辑是:
    	//取得每个子reducer对应的state,与action一起作为参数给每个子reducer执行。
    	let hasChanged = false //标志state是否有变化
        let nextState = {}
        for(let i = 0; i < finalReducerKeys.length; i++) {
                    //得到本次循环的子reducer
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]
            //得到该子reducer对应的旧状态
            const previousStateForKey = state[key]
            //调用子reducer得到新状态
            const nextStateForKey = reducer(previousStateForKey, action)
            //存到nextState中(总的状态)
            nextState[key] = nextStateForKey
            //到这里时有一个问题:
            //就是如果子reducer不能处理该action,那么会返回previousStateForKey
            //也就是旧状态,当所有状态都没改变时,我们直接返回之前的state就可以了。
            hasChanged = hasChanged || previousStateForKey !== nextStateForKey
        }
        return hasChanged ? nextState : state
    }
} 

Зачем вам промежуточное ПО

В задумке дизайна redux редуктор должен быть чистой функцией

Определение чистой функции в Википедии:

существуетпрограммирование, если одинфункцияотвечает следующим требованиям, его можно считатьчистая функция:

  • Эта функция должна выдавать одинаковые выходные данные для одних и тех же входных значений. другую скрытую информацию, кроме выходных и входных значений функции илиусловиенеактуально, а также и поI/OВнешние выходы, производимые устройством, не имеют значения.
  • Функция не может иметь семантически наблюдаемыхпобочные эффекты функции, такие как «запуск события», создание вывода устройства вывода или изменение содержимого объекта, отличного от выходного значения, и т. д.

Выход чистой функции может быть независимым от всех входных значений или даже независимым от всех входных значений. Но вывод чистой функции не может быть связан ни с какой другой информацией, кроме входного значения. Чистые функции могут возвращать несколько выходных значений, но вышеуказанные принципы должны соблюдаться для всех выходных значений. Если аргументпозвонить по ссылке, если есть изменение объекта параметра, это повлияет на содержимое объекта, отличное от функции, поэтому это не чистая функция.

Подводя итог, суть чистых функций заключается в следующем:

  • Один и тот же ввод производит тот же вывод (не может использоваться внутриMath.random,Date.nowЭти методы влияют на вывод)
  • Выходные данные не могут быть связаны ни с чем, кроме входного значения (вы не можете вызывать API для получения других данных).
  • Внутри функции не может повлиять ни на что вне функции (не может напрямую изменить переданную в ссылке переменную), т.е. она не будет видоизменяться

Почему редюсер требует использования чистых функций, это также упоминается в документе и резюмируется следующим образом:

  • Состояние создается согласно редьюсеру, поэтому редюсер тесно связан с состоянием.К состоянию нам иногда нужно предъявлять некоторые требования (такие как печать состояния до и после каждого обновления, или возврат в состояние до определенного обновление) Это правильно Редюсер имеет некоторые требования.

  • Чистые функции легче отлаживать

    • Например, когда мы отлаживаем, мы надеемся, что действие и соответствующие старое и новое состояния могут быть напечатаны.Если новое состояние изменено на старое состояние, даже если используется одна и та же ссылка, то старое и новое состояния не могут быть напечатаны. распечатанный.
    • Если выходные данные функции случайны или зависят от чего-то внешнего, это может затруднить поиск проблемы при отладке.
  • Если не использовать чистые функции, то при сравнении двух объектов, соответствующих старому и новому состоянию, приходится выполнять глубокие сравнения, которые очень расточительны для производительности. Наоборот, если для всех объектов, которые могут быть изменены (например, если редюсер вызывается один раз, входящее состояние может быть изменено), мы создаем новый объект и присваиваем его, и два объекта имеют разные адреса. Тогда поверхностное сравнение подойдет.

Пока что мы уже знаем, что редюсер — это чистая функция, поэтому, если нам действительно нужно иметь дело с какими-то побочными эффектами (такими как асинхронная обработка, вызов API и т. д.) в приложении, то что нам делать? Это проблема, которую решает промежуточное ПО. Давайте поговорим о промежуточном программном обеспечении в Redux.

Механизм промежуточного программного обеспечения для борьбы с побочными эффектами

Где находится промежуточное ПО в Redux, мы можем взглянуть на эти две картинки.

Давайте сначала посмотрим на рабочий процесс redux без промежуточного программного обеспечения:

redux工作流程_同步

  1. отправить действие (чистый объектный формат)
  2. Это действие обрабатывается редуктором
  3. Редуктор обновляет хранилище (состояние в нем) на основе действия

Рабочий процесс после использования промежуточного программного обеспечения выглядит следующим образом:

redux工作流程_中间件

  1. отправить «действие» (не обязательно стандартное действие)
  2. Это «действие» сначала обрабатывается промежуточным программным обеспечением (например, здесь отправляется асинхронный запрос).
  3. После завершения обработки промежуточного программного обеспечения отправьте еще одно «действие» (это может быть исходное действие или это могут быть другие действия из-за разных функций промежуточного программного обеспечения).
  4. «Действие», выданное промежуточным программным обеспечением, может продолжать обрабатываться другим промежуточным программным обеспечением, выполняя шаги, подобные 3. То есть промежуточное ПО может быть последовательно соединено.
  5. После обработки последнего промежуточного ПО отправьте действие (чистое действие объекта), которое соответствует стандарту обработки редуктора.
  6. Это стандартное действие обрабатывается редуктором,
  7. Редуктор обновляет хранилище (состояние в нем) на основе действия

Итак, как промежуточное ПО должно быть интегрировано в Redux?

В приведенном выше процессе шаги 2-4 относятся к промежуточному программному обеспечению, но всякий раз, когда мы хотим добавить промежуточное программное обеспечение, нам нужно написать набор логики 2-4.

Если нам нужно несколько промежуточных программ, нам нужно подумать, как связать их вместе. Если вы пишете логику конкатенации для каждой конкатенации, она недостаточно гибкая.Если вам нужно добавить, удалить, изменить или настроить порядок промежуточного программного обеспечения, вам нужно изменить логику конкатенации промежуточного программного обеспечения.

Таким образом, Redux предоставляет решение, которое инкапсулирует последовательную работу промежуточного программного обеспечения.После инкапсуляции вышеуказанные шаги 2-5 могут быть интегрированы, как показано на следующем рисунке:

封装中间件后的逻辑

Нам просто нужно изменить метод отправки, который поставляется с магазином. После того, как действие происходит, оно сначала обрабатывается промежуточным программным обеспечением, и, наконец, действие отправляется редюсеру для изменения состояния.

использовать промежуточное ПО в редуксе

Помните о редуксеcreateStore()Третий параметр методаenhancer:

function createStore(reducer, preloadedState, enhancer) {
    if(enhancer是有效的){  
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    //...
}

Здесь мы видим, что энхансер (который можно назвать энхансером) — это функция, которая принимает «обычную функцию createStore» в качестве параметра и возвращает «расширенную функцию createStore».

Что мы делаем в процессе усиления, так это трансформируем диспетчеризацию и добавляем промежуточное ПО.

предоставлено редуксомapplyMiddleware()Метод возвращает энхансер.

applyMiddleware, как следует из названия, «промежуточное ПО приложений». Вход — это количество промежуточного программного обеспечения, а выход — усилитель. Давайте посмотрим на его исходный код:

function applyMiddleware(...middlewares) {
    // 返回一个函数A,函数A的参数是一个createStore函数。
    // 函数A的返回值是函数B,其实也就是一个加强后的createStore函数,大括号内的是函数B的函数体
    return createStore => (...args) => {
        //用参数传进来的createStore创建一个store
        const store  = createStore(...args)
        //注意,我们在这里需要改造的只是store的dispatch方法
        
        let dispatch = () => {  //一个临时的dispatch
            					//作用是在dispatch改造完成前调用dispatch只会打印错误信息
            throw new Error(`一些错误信息`)
        } 
        //接下来我们准备将每个中间件与我们的state关联起来(通过传入getState方法),得到改造函数。
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        //middlewares是一个中间件函数数组,中间件函数的返回值是一个改造dispatch的函数
        //调用数组中的每个中间件函数,得到所有的改造函数
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        
        //将这些改造函数compose(翻译:构成,整理成)成一个函数
        //用compose后的函数去改造store的dispatch
        dispatch = compose(...chain)(store.dispatch)
        // compose方法的作用是,例如这样调用:
        // compose(func1,func2,func3)
        // 返回一个函数: (...args) => func1( func2( func3(...args) ) )
        // 即传入的dispatch被func3改造后得到一个新的dispatch,新的dispatch继续被func2改造...
        
        // 返回store,用改造后的dispatch方法替换store中的dispatch
        return {
            ...store,
            dispatch
        }
    }
}

Подводя итог, как работает applyMiddleware:

  1. Вызвать (несколько) промежуточных функций, получить (несколько) функций преобразования
  2. Скомпонуйте все функции преобразования в одну функцию преобразования
  3. Способ отправки дооснащения

Промежуточное ПО работает следующим образом:

  • Промежуточное программное обеспечение — это функция, которую также можно назвать функцией промежуточного программного обеспечения.
  • Вход функции промежуточного программного обеспечения - это хранилищеgetStateа такжеdispatch, выходом является функция преобразования (преобразованиеdispatchФункция)
  • Вход функции модернизацииdispatch, вывод "измененныйdispatch"

В исходном коде используется очень полезный метод:compose(), который объединяет несколько функций в одну функцию. Понимание этой функции очень полезно для понимания промежуточного программного обеспечения, давайте взглянем на его исходный код:

function compose(...funcs) {
    // 当未传入函数时,返回一个函数:arg => arg
    if(funcs.length === 0) {
        return arg => arg
    }
    
    // 当只传入一个函数时,直接返回这个函数
    if(funcs.length === 1) {
        return funcs[0]
    }
    
    // 返回组合后的函数
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
    
    //reduce是js的Array对象的内置方法
    //array.reduce(callback)的作用是:给array中每一个元素应用callback函数
    //callback函数:
    /*
     *@参数{accumulator}:callback上一次调用的返回值
     *@参数{value}:当前数组元素
     *@参数{index}:可选,当前元素的索引
     *@参数{array}:可选,当前数组
     *
     *callback( accumulator, value, [index], [array])
    */
}

Нарисуйте картинку, чтобы понять, что делает compose:

compose

В методе applyMiddleware «параметр», который мы передаем, — это исходный метод отправки, а возвращаемый «результат» — модифицированный метод отправки. С композицией мы можем сделать несколько функций преобразованияабстрагироваться вФункция преобразования.

Реализация промежуточного программного обеспечения

Примечание автора: я хотел поговорить только о избыточности, но обнаружил, что понимание промежуточного программного обеспечения является предпосылкой понимания механизма промежуточного программного обеспечения избыточности.

Давайте возьмем в качестве примера redux-thunk, чтобы увидеть, как реализуется промежуточное программное обеспечение.

Особенности redux-thunk

Возможно, вы не использовали redux-thunk, поэтому, прежде чем читать исходный код, позвольте мне кратко рассказать о роли redux-thunk:

Действие параметра обычной диспетчерской функции должно быть чистым объектом. так:

store.dispatch({
    type:'REQUEST_SOME_THING',
    payload: {
        from:'bob',
    }
})

После использования thunks мы можем отправить функцию:

function logStateInOneSecond(name) {
    return (dispatch, getState, name) => {  // 这个函数会在合适的时候dispatch一个真正的action
        setTimeout({
            console.log(getState())
            dispatch({
                type:'LOG_OK',
                payload: {
                    name,
                }
            })
        }, 1000)
    }
}

store.dispatch(logStateInOneSecond('jay')) //dispatch的参数是一个函数

Зачем нужна эта функция? Или какую проблему может решить «отправить функцию»?

Из приведенного выше примера вы обнаружите, что если диспетчеризация является функцией, мы можем делать все, что захотим (асинхронная обработка, вызов интерфейсов и т. д.) внутри этой функции без каких-либо ограничений, почему?

Поскольку мы «еще не отправили настоящее действие», редьюсер не будет вызываться,Вместо того, чтобы помещать побочные эффекты в редюсер, мы обрабатываем побочные эффекты перед использованием редуктора..

Если вы все еще не понимаете функцию redux-thunk, вы можете перейти кего репозиторий на гитхабеСмотрите более подробное объяснение.

Реализация redux-thunk

Как реализовать промежуточное ПО redux-thunk?

Во-первых, промежуточное ПО должно преобразовывать метод диспетчеризации.Преобразованный диспетчер должен иметь следующие функции:

  1. Если переданный параметр является функцией, функция выполняется.
  2. В противном случае считается, что входящее - это стандартное действие, и вызывается метод "отправка перед преобразованием", действие отправки.

Теперь давайте посмотрим на исходный код redux-thunk (8 строк действительного кода):

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

Если три функции со стрелками вызывают у вас головокружение, позвольте мне помочь вам:

//createThunkMiddleware的作用是返回thunk中间件(middleware)
function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // 这是「中间件函数」
        
        return function(next) { // 这是中间件函数创建的「改造函数」
            
            return function(action) { // 这是改造函数改造后的「dispatch方法」
                if (typeof action === 'function') {
                  return action(dispatch, getState, extraArgument);
                }
                
                return next(action);
            }
        } 
    }
}

Еще одно замечание?

function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // 这是「中间件函数」
        //参数是store中的dispatch和getState方法
        
        return function(next) { // 这是中间件函数创建的「改造函数」
            //参数next是被当前中间件改造前的dispatch
            //因为在被当前中间件改造之前,可能已经被其他中间件改造过了,所以不妨叫next
            
            return function(action) { // 这是改造函数「改造后的dispatch方法」
                if (typeof action === 'function') {
                  //如果action是一个函数,就调用这个函数,并传入参数给函数使用
                  return action(dispatch, getState, extraArgument);
                }
                
                //否则调用用改造前的dispatch方法
                return next(action);
            }
        } 
    }
}

Сделанный. Видно, что redux-thunk строго следует идее мидлвара redux: обрабатывать побочные эффекты до того, как исходный метод диспетчеризации запустит обработку редьюсера.

Суммировать

На данный момент основной исходный код Redux закончен, и, наконец, я должен вздохнуть, Redux действительно красив и лаконичен.

Суммируйте основную функцию redux в одном предложении: «создать хранилище для управления состоянием».

Что касается промежуточного программного обеспечения, я постараюсь написать статью «Как самостоятельно реализовать промежуточное программное обеспечение с редукцией», чтобы глубже понять значение промежуточного программного обеспечения с редукцией.

Я напишу еще одну статью о том, как магазин работает с другими фреймворками (например, реагировать)react-reduxИнтерпретация исходного кода» исследует этот вопрос.

Следите за обновлениями.


Первое обновление (2018-09-12)

  • добавленная параcompose()объяснение исходного кода
  • Исправлено неверное описание "Единственный способ изменить состояние - вызвать отправку"; добавлена ​​информация оunSubscribeобъяснение
  • Добавлен принцип реализации промежуточного ПО вredux-thunkНапример