Conclusion first
- Redux is a state management library and an architecture
- Redux has nothing to do with React, but it is a solution to the inability to share state in React components
- Pure Redux is just a state machine. All the states are stored in the store. To change the state in the store, you can only dispatch an action.
- The sent action needs to be processed by the reducer, passing in the state and action, and returning the new state
- The subscribe method can register a callback method, and the callback will be executed when the dispatch action occurs
- Redux is actually a publish-subscribe model
- Redux supports enhancer. Enhancer is actually a decorator pattern, passing in the current createStore and returning an enhanced createStore
- Redux uses the applyMiddleware function to support middleware, and its return value is actually an enhancer
- Redux's middleware is also a decorator pattern, passing in the current dispatch and returning an enhanced dispatch
- Pure Redux has no View layer
Why did Redux appear?
We use the React stack by default, when the pages are small and simple, there is no need to use Redux at all. Redux came into existence to deal with complex components. That is, when the component is complex to three or even four layers (as shown in the figure below), component 4 wants to change the state of component 1
State hoisting, as React does, promotes state to the same parent component (grandparent component in the diagram). But once there are more levels, the root component needs to manage a lot of state, which is inconvenient to manage.
So there was a context (React 0.14 was definitely introduced), and the data sharing of "far-home components" can be realized through the context. But it also has disadvantages. Using context means that all components can modify the state in the context, just like anyone can modify the shared state, which leads to unpredictable program operation, which is not what we want.
Facebook proposed the Flux solution, which introduced the concept of one-way data flow (yes, React does not have the concept of one-way data flow, Redux is a one-way data flow concept that integrates Flux), and the architecture is shown in the following figure:
Flux is not listed here. Simple understanding, in the Flux architecture, View should notify Dispatcher (dispatcher) through Action (action), Dispatcher (dispatcher) to modify Store, Store and then modify View
What are the problems or shortcomings of Flux?
There are dependencies between stores, server-side rendering is difficult, stores mix logic and state
It was 2018 when the author was learning the React technology stack. It was the already popular solution of React + Redux. Flux has been eliminated. Understanding Flux is to lead to Redux.
The advent of Redux
Redux mainly solves the problem of state sharing
Official website: Redux is a JavaScript state container that provides predictable state management
Its author is Dan Abramov
Its structure is:
It can be seen that Redux is just a state machine without a View layer. The process can be described as follows:
- Write a reducer yourself (pure function, indicating what data will be returned by doing an action)
- Write an initState yourself (store initial value, writable or not)
Generate store through createStore, this variable contains three important properties
- store.getState: get the unique value (using the closure brother)
- store.dispatch: Action behavior (change the only specified property of the data in the store)
- store.subscribe: subscribe (publish-subscribe mode)
- Dispatch an action via store.dispatch
- The reducer handles the action and returns a new store
- If you subscribed, you will be notified when the data changes
According to the behavior process, we can write a Redux by hand, the following is in the table, let's talk about the characteristics first
Three principles
single source of truth
- The global state of the entire application is stored in an object tree, and this object tree exists in only one store
State is read-only
- The only way to change the state is to trigger an action, an action is an ordinary object describing the time that has occurred
Use pure functions to perform modifications
- To describe how actions change the state tree, you need to write pure reducers
The three principles are for better development. According to the concept of one-way data flow , the behavior becomes traceable.
Let's start writing a Redux
handwritten redux
In accordance with the behavior process and principles, we must avoid problems such as random modification of data and traceability of behavior.
Basic: 23 lines of code to let you use redux
export const createStore = (reducer, initState) => {
let state = initState
let listeners = []
const subscribe = (fn) => {
listeners.push(fn)
}
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((fn) => fn())
}
const getState = () => {
return state
}
return {
getState,
dispatch,
subscribe,
}
}
make a test case
import { createStore } from '../redux/index.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
})
store.dispatch({
type: 'INCREMENT',
})
PS: I am using ES6 modules in node and need to upgrade Node version to 13.2.0
Second Edition: Difficulty Breakthrough: Middleware
Ordinary Redux can only do the most basic return data according to the action, dispatch is just a command to fetch data, for example:
dispatch({
type: 'INCREMENT',
})
// store 中的 count + 1
But in development, we sometimes need to view logs, asynchronous calls, record daily, etc.
What to do, make a plugin
In Redux, a similar concept is called middleware
Redux's createStore has three parameters
createStore([reducer], [initial state], [enhancer]);
The third parameter is enhancer, which means enhancer. Its role is to replace the ordinary createStore and transform it into a createStore with middleware attached. A few analogies:
- Tony Stark was originally an ordinary rich man, after adding enhancers (armor), he became Iron Man
- After the central government issued a disaster relief fund, and after adding the booster (the management of the big and small officials), there was only a drop of money in the hands of the disaster victims.
- Luffy hits people with armed color, and armed color is a middleware
What the enhancer has to do is: the thing is still that thing, but it has gone through some processes to strengthen it . These steps are done by the applyMiddleware function. In industry jargon, it's a decorator pattern . It is written roughly as:
applyMiddleware(...middlewares)
// 结合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares))
So we need to transform createStore first, and judge that when there is an enhancer, we need to pass the value to the middleware
export const createStore = (reducer, initState, enhancer) => {
if (enhancer) {
const newCreateStore = enhancer(createStore)
return newCreateStore(reducer, initState)
}
let state = initState;
let listeners = [];
...
}
If there is an enhancer, pass in the createStore function first. The generated newCreateStore is the same as the original createStore, and the store will be generated according to the reducer and initState. Can be simplified to:
if (enhancer) {
return enhancer(createStore)(reducer, initState)
}
PS: Why is it written like this, because redux is written in a functional way
Why createStore can be passed by value, because functions are also objects and can also be passed as parameters (old iron closure)
In this way, our applyMiddleware is naturally clear
const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState)
...
}
}
}
The store here represents the store in the normal version, and then we need to enhance the attributes in the store
I'd call it this: Five lines of code cost a woman $180,000 for me
export const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState)
// 以下为新增
const chain = middlewares.map((middleware) => middleware(store))
// 获得老 dispatch
let dispatch = store.dispatch
chain.reverse().map((middleware) => {
// 给每个中间件传入原派发器,赋值中间件改造后的dispatch
dispatch = middleware(dispatch)
})
// 赋值给 store 上的 dispatch
store.dispatch = dispatch
return store
}
}
}
Now write a few middleware to test
// 记录日志
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState())
console.log('action', action)
next(action)
console.log('next state', store.getState())
}
// 记录异常
export const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action)
} catch (error) {
console.log('错误报告', error)
}
}
// 时间戳
export const timeMiddleware = (store) => (next) => (action) => {
console.log('time', new Date().getTime())
next(action)
}
Introduce into the project and run
import { createStore, applyMiddleware } from '../redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
})
store.dispatch({
type: 'INCREMENT',
})
Running discovery has implemented the most important function of redux - middleware
To analyze the functional programming of middleware, take loggerMiddleware as an example:
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState())
console.log('action', action)
next(action)
console.log('next state', store.getState())
}
In the applyMiddleware source code,
const chain = middlewares.map((middleware) => middleware(store))
It is equivalent to passing the value of the normal version of the store to each middleware
let dispatch = store.dispatch
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)))
It is equivalent to passing in store.dispatch to each middleware, that is, next, the original dispatch = next . At this time, the middleware is already a finished product. The code (action) => {...}
is the function const dispatch = (action) => {}
. When you execute dispatch({ type: XXX })
execute this middleware (action) => {...}
PS: Currying is difficult to understand at first, but you can understand it with a lot of habits
Third Edition: Structural Complexity and Splitting
Middleware may be a bit complicated to understand, let's look at other concepts first to change ideas
After an application grows, it is obviously unscientific to maintain the code with only one JavaScript file. In Redux, in order to avoid this kind of situation, it provides combineReducers
to use the entire multiple reducers, using methods such as:
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
Pass in an object in combinReducers
, what kind of state corresponds to what kind of reducer. That's it, then combinReducers
how to achieve it? Because it is relatively simple, do not do much analysis, go directly to the source code:
export const combinReducers = (...reducers) => {
// 拿到 counter、info
const reducerKey = Object.keys(reducers)
// combinReducers 合并的是 reducer,返回的还是一个 reducer,所以返回一样的传参
return (state = {}, action) => {
const nextState = {}
// 循环 reducerKey,什么样的 state 对应什么样的 reducer
for (let i = 0; i < reducerKey.length; i++) {
const key = reducerKey[i]
const reducer = reducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState
}
}
Create a new reducer folder in the same level directory, and create reducer.js
, info.js
, index.js
// reducer.js
export default (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT': {
return {
count: state.count - 1,
}
}
default:
return state
}
}
// info.js
export default (state, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
}
default:
return state
}
}
Merge export
import counterReducer from './counter.js'
import infoReducer from './info.js'
export { counterReducer, infoReducer }
Let's test it now
import { createStore, applyMiddleware, combinReducers } from '../redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'
const initState = {
counter: {
count: 0,
},
info: {
name: 'johan',
description: '前端之虎',
},
}
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
store.dispatch({
type: 'INCREMENT',
})
combinReducers
also done
Since the reducer is split, whether the state can also be split, and whether it needs to be passed, in our usual writing method, the state is generally not passed. Two transformations are needed here, one is that each reducer contains its state and reducer; the other is to transform createStore, so that initState can be passed or not, and initialized data
// counter.js 中写入对应的 state 和 reducer
let initState = {
counter: {
count: 0,
},
}
export default (state, action) => {
if (!state) {
state = initState
}
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT': {
return {
count: state.count - 1,
}
}
default:
return state
}
}
// info.js
let initState = {
info: {
name: 'johan',
description: '前端之虎',
},
}
export default (state, action) => {
if (!state) {
state = initState
}
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
}
default:
return state
}
}
Retrofit createStore
export const createStore = (reducer, initState, enhancer) => {
if (typeof initState === 'function') {
enhancer = initState;
initState = undefined
}
...
const getState = () => {
return state
}
// 用一个不匹配任何动作来初始化store
dispatch({ type: Symbol() })
return {
getState,
dispatch,
subscribe
}
}
in the main file
import { createStore, applyMiddleware, combinReducers } from './redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
console.dir(store.getState())
So far, we have implemented a seven-seven-eight-eight redux
Complete Redux
unsubscribe
const subscribe = (fn) => {
listeners.push(fn)
return () => {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
The store obtained by the middleware
Now the middleware can get the complete store, he can even modify our subscribe method. According to the minimum open strategy , we only need to give getState, and modify the store passed to the middleware in applyMiddleware
// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState }
const chain = middlewares.map((middleware) => middleware(simpleStore))
compose
In our applyMiddleware, convert [A, B, C] to A(B(C(next))), the effect is:
const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map((middleware) => {
dispatch = middleware(dispatch)
})
Redux provides a compose, as follows
const compose = (...funcs) => {
if (funcs.length === 0) {
return (args) => args
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
2 lines of code replaceReducer
To replace the current reudcer, use the scenario:
- code splitting
- dynamic loading
- Real-time reloading mechanism
const replaceReducer = (nextReducer) => {
reducer = nextReducer
// 刷新一次,广播 reducer 已经替换,也同样把默认值换成新的 reducer
dispatch({ type: Symbol() })
}
bindActionCreators
What does bindActionCreators do? It hides dispatch and actionCreator through closures, so that other places cannot perceive the existence of redux. Generally combined with the connect of react-redux
Paste the source code directly here:
const bindActionCreator = (actionCreator, dispatch) => {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
export const bindActionCreators = (actionCreators, dispatch) => {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
Above, we have completed all the code in Redux. In general, the more than 100 lines of code here are all of Redux. The real Redux is nothing more than adding some comments and parameter verification.
Summarize
We list the terms related to Redux and sort out what it does
createStore
- Create a store object, including getState, dispatch, subscribe, replaceReducer
reducer
- Pure function, accept old state, action, generate new state
action
- Action, which is an object, must include a type field, indicating that the view issues a notification to tell the store to change
dispatch
- Dispatch, trigger action, generate new state. is the only way for a view to issue an action
subscribe
- Subscribe, only subscribed, when dispatched, the subscription function will be executed
combineReducers
- Merge reducers into one reducer
replaceReudcer
- Functions that replace reducers
middleware
- Middleware, extending the dispatch function
The brick house once drew a flowchart about Redux
Understand in a different way
As we said, Redux is just a state management library, which is driven by data and initiates an action, which will trigger the data update of the reducer to update to the latest store
Integrate with React
Take the newly made Redux and put it in React, and try what is called a Redux + React set. Note that we don't use React-Redux here, just take the combination of these two
Create the project first
npx create-react-app demo-5-react
Introduce handwritten redux library
Introduce createStore in App.js
, and write the initial data and reducer, and monitor the data in useEffect. After listening, when an action is initiated, the data will change. See the code:
import React, { useEffect, useState } from 'react'
import { createStore } from './redux'
import './App.css'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
function App() {
const [count, setCount] = useState(store.getState().count)
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setCount(store.getState().count)
})
return () => {
if (unsubscribe) {
unsubscribe()
}
}
}, [])
const onHandle = () => {
store.dispatch({
type: 'INCREMENT',
})
console.log('store', store.getState().count)
}
return (
<div className="App">
<div>{count}</div>
<button onClick={onHandle}>add</button>
</div>
)
}
export default App
After the button is clicked, the data changes accordingly
PS: Although we can subscribe to the store and change data in this way, the code for subscription is too repetitive, and we can use high-order components to extract it. This is also what React-Redux does
Combining with native JS+HTML
We said that Redux is a separate existence from Redux, it not only acts as a data manager in Redux, but also acts as a starting position in native JS + HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="container">
<div id="count">1</div>
<button id="btn">add</button>
</div>
<script type="module">
import { createStore } from './redux/index.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
let count = document.getElementById('count')
let add = document.getElementById('btn')
add.onclick = function () {
store.dispatch({
type: 'INCREMENT',
})
}
// 渲染视图
function render() {
count.innerHTML = store.getState().count
}
render()
// 监听数据
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
render()
})
</script>
</body>
</html>
The effect is as follows:
state ecology
We talk about Redux from Flux, and then from Redux to talk about various middleware, among which React-saga is middleware for solving asynchronous behavior. It mainly adopts the concept of Generator, which is compared with React-thunk and React- Promise, it does not put asynchronous behavior on the action creator like the other two, but treats all asynchronous operations as "threads", triggers it through action, and emits action as output when the operation is completed
function* helloWorldGenerator() {
yield 'hello'
yield 'world'
yield 'ending'
}
const helloWorld = helloWorldGenerator()
hewlloWorld.next() // { value: 'hello', done: false }
hewlloWorld.next() // { value: 'world', done: false }
hewlloWorld.next() // { value: 'ending', done: true }
hewlloWorld.next() // { value: undefined, done: true }
To put it simply: when encountering the yield expression, the execution of the following operations is suspended, and the value of the expression immediately following the yield is used as the return value value, waiting for the next method to be called, and then continue to execute.
Dva
What is Dva?
Official website: Dva is first of all a data flow solution based on Redux + Redux-saga. In order to simplify the development experience, Dva has additional built-in react-router and fetch, so it can be understood as a lightweight application framework
Simply put, it integrates the most popular data flow solution, a React technology stack:
dva = React-Router + Redux + Redux-saga + React-Redux
Its data flow diagram is:
The view dispatches an action, changes the state (ie store), the state is bound to the view, and responds to the view
Others are not listed, you can go to the Dva official website to check, here is the Model, which contains 5 attributes
namespace
- The namespace of the model, which is also its attribute on the global state, can only be used as a string, and does not support the creation of multi-layer namespaces by
.
- The namespace of the model, which is also its attribute on the global state, can only be used as a string, and does not support the creation of multi-layer namespaces by
state
- initial value
reducers
- Pure function, define reducer in key/value format. Used to process synchronous erasure, the only place that can be modified
state
is triggered byaction
- The format is:
(state, action) => newState
or[(state, action) => newState, enhancer]
- Pure function, define reducer in key/value format. Used to process synchronous erasure, the only place that can be modified
effects
- Handle asynchronous operations and business logic, and define effects in key/value format
- Do not modify state directly. triggered by action
- call: perform an asynchronous operation
- put: issue an Action, similar to dispatch
subscriptions
- subscription
- Executed at
app.start()
, the data source can be the current time, the server's websocket link, keyboard input, history routing changes, geolocation changes, etc.
Mobx
Whether View is subscribed or monitored, different frameworks have different technologies. In short, when the store changes, so does the view.
Mobx uses a reactive data streaming scheme. I will write a separate article in the future. This article is too long, so I won't write it first.
Supplement: One-way data flow
Let's first introduce data transfer in React, that is, communication problems
- Send messages to child components
- Send a message to the parent component
- Send messages to other components
React only provides one way of communication: passing parameters.
That is, the parent passes the value to the child, the child cannot modify the data passed by the parent, and props are immutable. What if the child component wants to pass data to the parent component? Notify parent components by passing values through events in props
Warehouse address: https://github.com/johanazhu/jo-redux
This article participated in the SegmentFault Sifu essay "How to "anti-kill" the interviewer?" , you are welcome to join.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。