предисловие
Несколько дней назад я написал статью о реакции, еще одном инструменте управления состоянием.Unstated
изАнализ исходного кода.
Открыл мой способ увидеть исходный код. Подумайте о редуксе, который используется уже давно, но я никогда не вникал в принцип, и еще больше смущаюсь, когда сталкиваюсь с ошибкой, поэтому я прочитал его исходный код и написал эту статью,
Поделитесь моим пониманием этого.
Обзор API
Посмотрите на index.js исходный код Redux, я увидел наиболее часто используемые API:
- createStore
- combineReducers
- bindActionCreators
- applyMiddleware
- compose
Не волнуйтесь анализом, мы смотрим на базовое использование Redux:
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
const root = document.getElementById('root')
// reducer 纯函数
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 创建一个store
const store = createStore(reducer)
const render = () => ReactDOM.render(
<div>
<span>{store.getState()}</span>
<button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button>
<button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button>
</div>,
root
)
render()
// store订阅一个更新函数,待dispatch之后,执行这个更新函数,获取新的值
store.subscribe(render)
Здесь реализован эффект сложения и вычитания чисел нажатием кнопки Поведение, вызванное нажатием, и изменение числа, отображаемое на странице, выполняются с помощью избыточности. Давайте проанализируем, как работает redux на этом примере:
- Используйте редюсер для создания хранилища, чтобы мы могли общаться с редуксом через хранилище.
- через страницу
store.getState()
Получил текущий номер, начальное значение 0 (в редюсере) - store.subscribe(render) — функция для подписки на обновления страниц, вызываемая, когда редюсер возвращает новое значение. (Фактическая подписка поместит функцию в массив слушателей и впоследствии вызовет ее в цикле)
- Нажмите кнопку, чтобы сообщить редуксу, хочу ли я увеличить или уменьшить (отправка вызова, передача в действие)
- После вызова отправки, вызовы функций отправки внутри мы определяем редуктор, в сочетании с текущим состоянием и действием, возвращаем новое состояние
- После возврата в новое состояние вызовите функцию обновления подписки на подписку, чтобы обновить страницу До сих пор все наши операции выполнялись через хранилище, а хранилище создается через createStore, так что давайте посмотрим на его внутреннюю логику.
createStore
createStore получает всего три параметра:reducer
, preloadedState
, enhancer
,
- редуктор: чистая функция, которая получает предыдущее (или начальное) состояние и действие и возвращает новое состояние в соответствии с типом действия.
- preloadedState: инициализированное состояние, которое может устанавливать значения по умолчанию в хранилище,
- энхансер: энхансер, используемый для расширения функциональности магазина
Предоставляет нам несколько часто используемых API:
- диспетчеризация: Получите действие, которое является объектом{типом: 'a_action_type'} в качестве параметра, а затем оно вызовет внутренний редуктор и вернет новое состояние в соответствии с действием и текущим состоянием.
- подписаться: подписаться на функцию, которая обновляет страницу, поместить ее в массив linsteners и вызываться, когда редуктор возвращает новое состояние для обновления страницы.
- getState: получить состояние в магазине
Давайте сначала разберемся с его механизмом через полученные параметры и выставленный API:
Первый — создать хранилище, получив три упомянутых выше параметра, где хранится все состояние приложения. В то же время доступны три метода: пользовательский интерфейс может получить данные в хранилище через store.getState(). store.subscribe(), функция заключается в том, чтобы позволить магазину подписаться на функцию, которая обновляет пользовательский интерфейс, передать эту функцию в массив слушателей и дождаться выполнения. store.dispatch() — это единственный способ обновить данные в хранилище.После вызова dispatch сначала будет вызван редюсер, и будет возвращено новое состояние в соответствии с текущим состоянием и действием. Затем вызовите функцию обновления в слушателях в цикле, Функция обновления, как правило, является функцией рендеринга нашего пользовательского интерфейса.Функция будет вызывать store.getState() для получения данных, поэтому страница будет обновлена.
Взгляните на структуру функции createStore.
createStore(reducer, preloadedState, enhancer) {
// 转换参数
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
function getState() {
// 返回当前的state, 可以调用store.getState()获取到store中的数据,
...
}
function subscribe(listener) {
// 订阅一个更新函数(listener),实际上的订阅操作就是把listener放入一个listeners数组
// 然后再取消订阅,将更新函数从listeners数组内删除
// 但是注意,这两个操作都是在dispatch不执行时候进行的。因为dispatch执行时候会循环执行更新函数,要保证listeners数组在这时候不能被改变
...
}
function dispatch(action) {
// 接收action,调用reducer根据action和当前的state,返回一个新的state
// 循环调用listeners数组,执行更新函数,函数内部会通过store.getState()获取state,此时的state为最新的state,完成页面的更新
...
}
return {
dispatch,
subscribe,
getState,
}
}
Структура такая, а как последовательно соединить? Давайте посмотрим на полный код (некоторые удалены)
createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 有了这一层判断,我们就可以这样传:createStore(reducer, initialState, enhancer)
// 或者这样: createStore(reducer, enhancer),其中enhancer还会是enhancer。
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// enhancer的作用是扩展store,所以传入createStore来改造,
// 再传入reducer, preloadedState生成改造后的store,这一有一点递归调用的意思
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer // 当前的reducer,还会有新的reducer
let currentState = preloadedState // 当前的state
let currentListeners = [] // 存储更新函数的数组
let nextListeners = currentListeners // 下次dispatch将会触发的更新函数数组
let isDispatching = false //类似一把锁,如果正在dispatch action,那么就做一些限制
// 这个函数的作用是判断nextListeners 和 currentListeners是否是同一个引用,是的话就拷贝一份,避免修改各自相互影响
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function getState() {
// 正在执行reducer的时候,是不能获取state的,要等到reducer执行完,返回新的state才可以获取
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 由于dispatch函数会在reducer执行完毕后循环执行listeners数组内订阅的更新函数,所以要保证这个时候的listeners数组
// 不变,既不能添加(subscribe)更新函数也不能删除(unsubscribe)更新函数
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
// 将更新函数推入到listeners数组,实现订阅
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
// 取消订阅
nextListeners.splice(index, 1)
}
}
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 正在dispatch的话不能再次dispatch,也就是说不可以同时dispatch两个action
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
// 获取到当前的state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
// 循环执行当前的linstener
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// dispatch一个初始的action,作用是不命中你reducer中写的任何关于action的判断,直接返回初始的state
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
// observable replaceReducer和$$observable主要面向库开发者,这里先不做解析
// replaceReducer,
// [$$observable]:
}
}
combineReducers
combReducers используется для объединения нескольких редюсеров в общий редьюсер, так что, как вы можете догадаться, В конечном итоге он должен вернуть функцию, а форма — это форма общего редуктора, получающего состояние и действие, Статус возврата:
function combine(state, action) {
......
return state
}
Давайте посмотрим на основной код:
export default function combineReducers(reducers) {
// 获取到所有reducer的名字,组成数组
const reducerKeys = Object.keys(reducers)
// 这个finalReducers 是最终的有效的reducers
const finalReducers = {}
// 以reducer名为key,reducer处理函数为key,生成finalReducers对象,形式如下
/* {
* reducerName1: f,
* reducerName2: f
* }
*/
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
// assertReducerShape用来检查这每个reducer有没有默认返回的state,
// 我们在写reducer时候,都是要在switch中加一个default的,来默认返回初始状态
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 这个函数,就是上边说的返回的最后的那个终极reducer,传入createStore,
// 然后在dispatch中调用,也就是currentReducer
// 这个函数的核心是根据finalReducer中存储的所有reducer信息,循环,获取到每个reducer对应的state,
// 并依据当前dispatch的action,一起传入当前循环到的reducer,生成新的state,最终,将所有新生成的
// state作为值,各自的reducerName为键,生成最终的state,就是我们在reduxDevTool中看到的state树,形式如下:
/* {
* reducerName1: {
* key: 'value'
* },
* reducerName2: {
* key: 'value'
* },
* }
*/
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
// 存放最终的所有的state
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
// 获取每个reducer的名字
const key = finalReducerKeys[i]
// 获取每个reducer
const reducer = finalReducers[key]
// 获取每个reducer的旧状态
const previousStateForKey = state[key]
// 调用该reducer,根据这个reducer的旧状态,和当前action来生成新的state
const nextStateForKey = reducer(previousStateForKey, action)
// 以各自的reducerName为键,新生成的state作为值,生成最终的state object,
nextState[key] = nextStateForKey
// 判断所有的state变化没变化
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 变化了,返回新的state,否则,返回旧的state
return hasChanged ? nextState : state
}
}
applyMiddleware
Оригинальный метод отправки Redux может принимать только один объект в качестве действия.
Действие пользователя -> диспетчеризация (действие) -> редуктор (предыдущее состояние, действие) -> новое состояние -> интерфейс
Такая прямая и понятная операция хороша тем, что позволяет проследить каждый шаг операции и легко найти проблему, но имеет недостаток. Например, странице нужно отправить запрос на получение данных и поместить данные в Действие. Наконец, он обрабатывается редьюсером и помещается в хранилище. В это время, как это сделать?
Действие пользователя -> отправка (
action
) -> middleware(action
) -> реальное действие -> редуктор (предыдущее состояние, действие) -> новое состояние -> интерфейс
Дело в том, что рассылка(action
) -> middleware(action
) эта операция, здесьaction
Это может быть функция, а внутри функции мы можем выполнять множество операций, в том числе вызывать API,
Затем, после успешного вызова API, отправьте реальное действие. Для этого нужно расширить редукс (модифицировать метод отправки), то есть использовать энхансер: энхансер:
const store = createStore(rootReducer,
applyMiddleware(thunk),
)
applyMiddleware(thunk)
Это эквивалентно энхансеру, который отвечает за расширение избыточности, то есть за расширение метода отправки хранилища.
Поскольку вы хотите преобразовать хранилище, вы должны передать хранилище в качестве параметра в энхансер, а затем выдать преобразованное хранилище. Метод доставки выплевывания из магазина является конечной целью энхансера по преобразованию магазина.
Просмотрите этот раздел в createStore:
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 把createStore传递进enhancer
return enhancer(createStore)(reducer, preloadedState)
}
Глядя на приведенный выше код, сначала определите энхансер, то есть, когда третий параметр createStore не является неопределенным и является функцией, затем выполните энхансер.
Мы видим Enhancer(createStore), который должен передаваться в createStore для преобразования.Что бы ни возвращала эта функция, давайте посмотрим, какие параметры ей нужны после ее выполнения.(reducer, preloadedState)
, это немного знакомо? Вспомните метод вызова createStore, createStore(reducer, state).
Видно, что Enhancer(createStore) возвращает новый createStore, и этот createStore был преобразован, и его внутренний метод отправки больше не является исходным. На данный момент достигнут эффект преображения магазина.
Так как же он трансформируется? Не волнуйтесь, давайте взглянем на готовый мидлвар-редукс-преобразователь. Чтобы понять механизм промежуточного ПО Redux, вы должны понять, как работает промежуточное ПО.
Давайте сначала посмотрим на разницу между его использованием или нет:
В общем, действие отправки — это чистый объект
store.dispatch({
type:'EXPMALE_TYPE',
payload: {
name:'123',
}
})
После использования переходников действия могут быть в виде функций
function loadData() {
return (dispatch, getState) => { // 函数之内会真正dispatch action
callApi('/url').then(res => {
dispatch({
type:'LOAD_SUCCESS',
data: res.data
})
})
}
}
store.dispatch(loadData()) //派发一个函数
При нормальных обстоятельствах функция отправки сообщает об ошибке напрямую, потому что метод отправки в createStore определяет тип действия внутри себя. Что делает для нас redux-thunk, так это преобразует диспетчеризацию, чтобы она могла отправлять функцию. Взгляните на основной код redux-thunk:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
Три стрелочные функции здесь представляют собой каррирование функций.
Когда он действительно вызывается, он теоретически похож на этот thunk({dispatch, getState})(next)(action).
Среди них часть thunk({ dispatch, getState})(next), если параметр, который он получает при выполнении, является действием, то это должен быть метод отправки, который эквивалентен модифицированной отправке здесь, и эта часть будет в разделе «Вызывать» в applyMiddleware (будет упомянуто ниже)
Потом смотрим слева направо, {dispatch, getState} это текущий Dispatch Магазина и метод getState, который является самым оригинальным, которым удобно получить самый оригинальный Dispatch для раздачи реального Action после прохождения через middleware.
Далее следует DISPATCH, прежде чем он будет преобразован текущим промежуточным программным обеспечением. Обратите внимание, что он не такой, как передняя часть Dispatch, СЛЕДУЮЩИЙ - это DISPATCH до преобразования с помощью Thunk, что означает, что он может быть самым оригинальным Dispatch или может быть DISPATCH, преобразованным другим промежуточным программным обеспечением.
Для лучшего понимания лучше перевести это в обычную вложенность функций и добавить комментарии.
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) { //真正的中间件函数,内部的改造dispatch的函数是精髓
return function(next) { //改造dispatch的函数,这里的next是外部传进来的dispatch,可能是被其他中间件处理过的,也可能是最原本的
return function(action) { //这个函数就是改造过后的dispatch函数
if (typeof action === 'function') {
// 如果action是函数,那么执行它,并且将store的dispatch和getState传入,便于我们dispatch的函数内部逻辑执行完之后dispatch真正的action,
// 如上边示例的请求成功后,dispatch的部分
return action(dispatch, getState, extraArgument);
}
// 否则说明是个普通的action,直接dispatch
return next(action);
}
}
}
}
const thunk = createThunkMiddleware();
Подводя итог: Грубо говоря, роль redux-thunk состоит в том, чтобы судить, является ли действие функцией или нет, и выполнять его, либо использовать то, которое могло быть преобразовано другим промежуточным программным обеспечением, либо самое примитивное. dispatch (следующий) Отправить это действие.
Затем взгляните на исходный код applyMiddleware:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => {
// 假设我们只是用了redux-thunk,那么此时的middleware就相当于thunk,可以往上看一下thunk返回的函数,
// 就是这个: function({ dispatch, getState }),就会明白了
return middleware(middlewareAPI)
})
// 这里的compose函数的作用就是,将所有的中间件函数串联起来,中间件1结束,作为参数传入中间件2,被它处理,
// 以此类推最终返回的是被所有中间件处理完的函数,最开始接收store.dispatch为参数,层层改造后被赋值到新的dispatch变量中
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
Давайте сначала рассмотрим простейший случай: допустим, мы используем только одно промежуточное ПО (redux-thunk) и можем временно отказаться от компоновки, тогда логика здесь эквивалентна диспетчеризация = преобразователь (middlewareAPI) (store.dispatch) Звучит немного знакомо? В исходном коде redux-thunk мы проанализировали:
Когда преобразователь действительно вызывается, thunk({dispatch, getState})(next)(action) Среди них часть thunk({ dispatch, getState })(next) эквивалентна модифицированной диспетчеризации, и эта часть будет вызываться в applyMiddleware.
Следовательно, здесь преобразуется метод отправки магазина, и, наконец, отправка в исходном магазине перезаписывается преобразованной отправкой.
Обобщить,
- Отношения между промежуточным ПО и applyMiddleware Redux. Промежуточное ПО поможет нам преобразовать метод отправки оригинального магазина.
- И applyMiddleware применит модифицированный метод отправки к хранилищу (эквивалентно замене исходной отправки измененной отправкой). Понимание принципа промежуточного программного обеспечения является предпосылкой понимания механизма применения промежуточного программного обеспечения.
Также поговорим о параметре redux-thunk:extraArgument
Этот параметр не особо важен, обычно передается экземпляр, а потом его можно получить, когда нам понадобится этот параметр в реальной рассылке, например, если передается экземпляр axios, то этот экземпляр можно использовать напрямую при запросе запрашивать
import axiosInstance from '../request'
const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument(axiosInstance)))
function loadData() {
return (dispatch, getState, instance) => {
instance.get('/url').then(res => {
dispatch({
type:'LOAD_SUCCESS',
data: res.data
})
})
}
}
store.dispatch(loadData())
Суммировать
К этому моменту было объяснено несколько основных концепций редукции. Я должен сказать, что написание действительно лаконичное. Зависимости между функциями меня очень смутили. Чтобы понять это, мне все еще нужно запустить пример из исходного кода. Смотрите снова и снова.
Подводя до суммирования redux - создать магазин для управления всеми состояниями и запускает действия для изменения магазина. Сценарии использования Redux очень гибкими и могут использоваться в сочетании с различными библиотеками. Я используется для реагирования, и мне нужно использовать React-redux, когда я его использую.