Redux, как наиболее часто используемый инструмент для управления состоянием крупномасштабных приложений React, его концепции, теория и практика заслуживают изучения, анализа и затем глубокого понимания на практике, что очень полезно для развития фронтенд-разработчиков. . В этой статье планируется объединить различия между компонентами контейнера Redux и компонентами отображения и наиболее распространенной библиотекой соединений между приложениями Redux и React, анализом исходного кода react-redux, чтобы достичь более глубокого понимания приложений Redux и React.
Добро пожаловать в мой личный блог
предисловие
Библиотека React-redux обеспечиваетProvider
Компонент внедряет хранилище в приложение через контекст, а затем может использоватьconnect
Высокоуровневый метод получает и отслеживает хранилище, затем вычисляет новые реквизиты на основе состояния хранилища и собственных реквизитов компонента, внедряет их в компонент и сравнивает рассчитанные новые реквизиты, наблюдая за хранилищем, чтобы определить, нужно ли компоненту быть обновленным. обновлено.
Provider
Во-первых, библиотека react-redux предоставляетProvider
Компоненты внедряют хранилище в компонент входа всего приложения React, обычно это компонент верхнего уровня приложения.Provider
Компонент использует контекст для передачи хранилища:
// 内部组件获取redux store的键
const storeKey = 'store'
// 内部组件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
// 声明context,注入store和可选的发布订阅对象
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
// 缓存store
this[storeKey] = props.store;
}
render() {
// 渲染输出内容
return Children.only(this.props.children)
}
}
Example
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './components/App'
import reducers from './reducers'
// 创建store
const store = createStore(todoApp, reducers)
// 传递store作为props给Provider组件;
// Provider将使用context方式向下传递store
// App组件是我们的应用顶层组件
render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('app-node')
)
способ подключения
Ранее мы использовалиProvider
Компонент внедряет хранилище избыточности в приложение, и следующее, что вам нужно сделать, это соединить компонент и хранилище. И мы знаем, что Redux не позволяет напрямую манипулировать состоянием хранилища, мы можем использовать только егоgetState
получить доступ к данным или черезdispatch
Действие по изменению состояния магазина.
Это именно та возможность, которую предоставляет метод высокого порядка connect, предоставляемый react-redux.
Example
container/TodoList.js
Сначала мы создаем компонент-контейнер списка, который отвечает за получение списка задач в компоненте, а затем передает задачи компоненту презентации TodoList, а также передает функцию обратного вызова события. щелчок, вызывается соответствующий обратный вызов.Обновите состояние хранилища избыточности с помощью действий отправки и, наконец, подключите компоненты хранилища и презентации, используя react-reduxconnect
метод, который получает
import {connect} from 'react-redux'
import TodoList from 'components/TodoList.jsx'
class TodoListContainer extends React.Component {
constructor(props) {
super(props)
this.state = {todos: null, filter: null}
}
handleUpdateClick (todo) {
this.props.update(todo);
}
componentDidMount() {
const { todos, filter, actions } = this.props
if (todos.length === 0) {
this.props.fetchTodoList(filter);
}
render () {
const { todos, filter } = this.props
return (
<TodoList
todos={todos}
filter={filter}
handleUpdateClick={this.handleUpdateClick}
/* others */
/>
)
}
}
const mapStateToProps = state => {
return {
todos : state.todos,
filter: state.filter
}
}
const mapDispatchToProps = dispatch => {
return {
update : (todo) => dispatch({
type : 'UPDATE_TODO',
payload: todo
}),
fetchTodoList: (filters) => dispatch({
type : 'FETCH_TODOS',
payload: filters
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoListContainer)
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, handleUpdateClick }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} handleUpdateClick={handleUpdateClick} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.array.isRequired
).isRequired,
handleUpdateClick: PropTypes.func.isRequired
}
export default TodoList
components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'
class Todo extends React.Component {
constructor(...args) {
super(..args);
this.state = {
editable: false,
todo: this.props.todo
}
}
handleClick (e) {
this.setState({
editable: !this.state.editable
})
}
update () {
this.props.handleUpdateClick({
...this.state.todo
text: this.refs.content.innerText
})
}
render () {
return (
<li
onClick={this.handleClick}
style={{
contentEditable: editable ? 'true' : 'false'
}}
>
<p ref="content">{text}</p>
<button onClick={this.update}>Save</button>
</li>
)
}
Todo.propTypes = {
handleUpdateClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
Контейнерные компоненты и презентационные компоненты
При использовании Redux в качестве контейнера управления состоянием для приложений React обычно реализуется практика разделения компонентов на Container Components и Presentational Components.
Presentational Components | Container Components | |
---|---|---|
Цель | Представление пользовательского интерфейса (структура и стиль HTML) | Бизнес-логика (получение данных, обновление статуса) |
Осведомлен о Редукс | без | имеют |
Источники данных | props | Подпишитесь на магазин Redux |
изменить данные | Вызовите функцию обратного вызова, переданную реквизитами | Dispatch Redux actions |
многоразовый | Сильная независимость | Высокая степень связанности бизнеса |
Большая часть кода в приложении — это написание презентационных компонентов, а затем использование некоторых компонентов-контейнеров для подключения этих презентационных компонентов к хранилищу Redux.
анализ исходного кода connect()
connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories;
selectorFactory = defaultSelectorFactory;
function connect (
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual, // 严格比较是否相等
areOwnPropsEqual = shallowEqual, // 浅比较
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...extraOptions
} = {}
) {
const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
// 调用connectHOC方法
connectHOC(selectorFactory, {
// 如果mapStateToProps为false,则不监听store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 传递给selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...extraOptions // 其他配置项
});
}
strictEquall
function strictEqual(a, b) { return a === b }
shallowEquall
const hasOwn = Object.prototype.hasOwnProperty
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
return true
}
shallowEqual({x:{}},{x:{}}) // false
shallowEqual({x:1},{x:1}) // true
connectДополнительная функция высшего порядка
function connectAdvanced (
selectorFactory,
{
renderCountProp = undefined, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...connectOptions
} = {}
) {
// 获取发布订阅器的键
const subscriptionKey = storeKey + 'Subscription';
const contextTypes = {
[storeKey]: storeShape,
[subscriptionKey]: subscriptionShape,
};
const childContextTypes = {
[subscriptionKey]: subscriptionShape,
};
return function wrapWithConnect (WrappedComponent) {
const selectorFactoryOptions = {
// 如果mapStateToProps为false,则不监听store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 传递给selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...connectOptions,
...others
renderCountProp, // render调用次数
shouldHandleStateChanges, // 是否监听store state变更
storeKey,
WrappedComponent
}
// 返回拓展过props属性的Connect组件
return hoistStatics(Connect, WrappedComponent)
}
}
selectorFactory
selectorFactory
Функция возвращает функцию-селектор, которая вычисляет новые реквизиты в соответствии с состоянием хранилища, реквизитами отображаемого компонента и отправкой, и, наконец, внедряет компонент-контейнер.selectorFactory
Структура функции такая:
(dispatch, options) => (state, props) => ({
thing: state.things[props.thingId],
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})
Примечание. Состояние в избыточности обычно относится к состоянию хранилища избыточности, а не к состоянию компонента, а реквизиты здесь — это реквизиты входящего компонента wrapperComponent.
function defaultSelectorFactory (dispatch, {
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...options
}) {
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// pure为true表示selectorFactory返回的selector将缓存结果;
// 否则其总是返回一个新对象
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
// 最终执行selector工厂函数返回一个selector
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
);
}
pureFinalPropsSelectorFactory
function pureFinalPropsSelectorFactory (
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
let hasRunAtLeastOnce = false
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
// 返回合并后的props或state
// handleSubsequentCalls变更后合并;handleFirstCall初次调用
return function pureFinalPropsSelector(nextState, nextOwnProps) {
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
handleFirstCall
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps) // store state映射到组件的props
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps) // 合并后的props
hasRunAtLeastOnce = true
return mergedProps
}
defaultMergeProps
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
// 默认合并props函数
return { ...ownProps, ...stateProps, ...dispatchProps }
}
handleSubsequentCalls
function handleSubsequentCalls(nextState, nextOwnProps) {
// shallowEqual浅比较
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
// 深比较
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
// 处理props或state变更后的合并
// store state及组件props变更
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
Compute возвращает новые реквизиты
Пока собственные реквизиты компонента отображения изменяются, вам нужно вернуться к недавно объединенным реквизитам, а затем обновить компонент контейнера, независимо от того, изменилось ли состояние хранилища:
// 只有展示型组件props变更
function handleNewProps() {
// mapStateToProps计算是否依赖于展示型组件props
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps计算是否依赖于展示型组件props
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// 展示型组件props和store state均变更
function handleNewPropsAndNewState() {
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps计算是否依赖于展示型组件props
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
Compute возвращает stateProps
Обычно изменения свойств компонента контейнера управляются изменениями состояния хранилища, поэтому во многих случаях изменяется только состояние хранилища, и именно на это нужно обратить внимание при использовании Immutable:УходитеmapStateToProps
использовать в методеtoJS()
метод.
когдаmapStateToProps
Когда объект реквизита, возвращаемый дважды, не изменился, нет необходимости его пересчитывать, просто верните объект реквизита, полученный в результате предыдущего слияния напрямую, а затем дважды сравните возвращаемое значение функции селектора в объекте отслеживания селектора, чтобы увидеть, есть ли любое изменение, он вернет false, компоненты контейнера не вызывают изменений.
Поскольку поверхностное сравнение используется при многократном сравнении результатов, возвращаемых mapStateToProps, метод Immutable.toJS() не рекомендуется. Он каждый раз возвращает новый объект, и сравнение возвращает false. Если используется Immutable, а его содержимое не изменено, то вернет true, что может уменьшить ненужный повторный рендеринг.
// 只有store state变更
function handleNewState() {
const nextStateProps = mapStateToProps(state, ownProps)
// 浅比较
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 计算得到的新props变更了,才需要重新计算返回新的合并props
if (statePropsChanged) {
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
}
// 若新stateProps未发生变更,则直接返回上一次计算得出的合并props;
// 之后selector追踪对象比较两次返回值是否有变更时将返回false;
// 否则返回使用mergeProps()方法新合并得到的props对象,变更比较将返回true
return mergedProps
}
hoist-non-react-statics
Как и в случае с Object.assign, не связанные с React статические свойства или методы дочернего компонента копируются в родительский компонент, а связанные с React свойства или методы не переопределяются, а объединяются.
hoistStatics(Connect, WrappedComponent)
Connect Component
Настоящий высокоуровневый компонент Connect соединяет состояние хранилища redux и входящие компоненты, то есть сопоставляет состояние хранилища с реквизитами компонента, react-redux использует компонент Provider для внедрения хранилища через контекст, а затем компонент Connect получает store через контекст и добавляет подписку в store:
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.state = {}
this.renderCount = 0 // render调用次数初始为0
// 获取store,props或context方式
this.store = props[storeKey] || context[storeKey]
// 是否使用props方式传递store
this.propsMode = Boolean(props[storeKey])
// 初始化selector
this.initSelector()
// 初始化store订阅
this.initSubscription()
}
componentDidMount() {
// 不需要监听state变更
if (!shouldHandleStateChanges) return
// 发布订阅器执行订阅
this.subscription.trySubscribe()
// 执行selector
this.selector.run(this.props)
// 若还需要更新,则强制更新
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
// 渲染组件元素
render() {
const selector = this.selector
selector.shouldComponentUpdate = false; // 重置是否需要更新为默认的false
// 将redux store state转化映射得到的props合并入传入的组件
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
addExtraProps()
Добавьте дополнительные свойства props в props:
// 添加额外的props
addExtraProps(props) {
const withExtras = { ...props }
if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 调用次数
if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
return withExtras
}
Инициализировать объект отслеживания селектора initSelector
Селектор, селектор, в соответствии с состоянием хранилища избыточности и собственными реквизитами компонента, вычисляет новые реквизиты, которые будут введены в компонент, и кэширует новые реквизиты, а затем снова выполняет селектор, сравнивая полученные реквизиты, чтобы решить, следует ли обновить компонент.Если реквизиты меняются, компонент обновляется, иначе не обновляется.
использоватьinitSelector
Метод инициализирует объект отслеживания селектора и связанное с ним состояние и данные:
// 初始化selector
initSelector() {
// 使用selector工厂函数创建一个selector
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
// 连接组件的selector和redux store state
this.selector = makeSelectorStateful(sourceSelector, this.store)
// 执行组件的selector函数
this.selector.run(this.props)
}
makeSelectorStateful()
Создайте объект отслеживания селектора, чтобы отслеживать результаты, возвращаемые функцией селектора:
function makeSelectorStateful(sourceSelector, store) {
// 返回selector追踪对象,追踪传入的selector(sourceSelector)返回的结果
const selector = {
// 执行组件的selector函数
run: function runComponentSelector(props) {
// 根据store state和组件props执行传入的selector函数,计算得到nextProps
const nextProps = sourceSelector(store.getState(), props)
// 比较nextProps和缓存的props;
// false,则更新所缓存的props并标记selector需要更新
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true // 标记需要更新
selector.props = nextProps // 缓存props
selector.error = null
}
}
}
// 返回selector追踪对象
return selector
}
Инициализировать подписку initSubscription
Инициализировать прослушивание/подписку на состояние хранилища избыточности:
// 初始化订阅
initSubscription() {
if (!shouldHandleStateChanges) return; // 不需要监听store state
// 判断订阅内容传递方式:props或context,两者不能混杂
const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
// 订阅对象实例化,并传入事件回调函数
this.subscription = new Subscription(this.store,
parentSub,
this.onStateChange.bind(this))
// 缓存订阅器发布方法执行的作用域
this.notifyNestedSubs = this.subscription.notifyNestedSubs
.bind(this.subscription)
}
Реализация класса подписки
Реализация издателя подписки, используемая хранилищем подписок компонентов:
export default class Subscription {
constructor(store, parentSub, onStateChange) {
// redux store
this.store = store
// 订阅内容
this.parentSub = parentSub
// 订阅内容变更后的回调函数
this.onStateChange = onStateChange
this.unsubscribe = null
// 订阅记录数组
this.listeners = nullListeners
}
// 订阅
trySubscribe() {
if (!this.unsubscribe) {
// 若传递了发布订阅器则使用该订阅器订阅方法进行订阅
// 否则使用store的订阅方法
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
// 创建订阅集合对象
// { notify: function, subscribe: function }
// 内部包装了一个发布订阅器;
// 分别对应发布(执行所有回调),订阅(在订阅集合中添加回调)
this.listeners = createListenerCollection()
}
}
// 发布
notifyNestedSubs() {
this.listeners.notify()
}
}
Функция обратного вызова подписки
Функция обратного вызова, выполняемая после подписки:
onStateChange() {
// 选择器执行
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
// 不需要更新则直接发布
this.notifyNestedSubs()
} else {
// 需要更新则设置组件componentDidUpdate生命周期方法
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
// 同时调用setState触发组件更新
this.setState(dummyState) // dummyState = {}
}
}
// 在组件componentDidUpdate生命周期方法内发布变更
notifyNestedSubsOnComponentDidUpdate() {
// 清除组件componentDidUpdate生命周期方法
this.componentDidUpdate = undefined
// 发布
this.notifyNestedSubs()
}
Другие методы жизненного цикла
getChildContext () {
// 若存在props传递了store,则需要对其他从context接收store并订阅的后代组件隐藏其对于store的订阅;
// 否则将父级的订阅器映射传入,给予Connect组件控制发布变化的顺序流
const subscription = this.propsMode ? null : this.subscription
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 接收到新props
componentWillReceiveProps(nextProps) {
this.selector.run(nextProps)
}
// 是否需要更新组件
shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}
componentWillUnmount() {
// 重置selector
}