After reading this article, you can also implement a redux.
The code corresponding to this article is as follows:https://github.com/YvetteLau/ …It is suggested that firstclone
Code, and then read this article against the code.
1.Redux
What is it?
Redux
YesJavaScript
The state container provides predictable state management.Redux
In addition to andReact
Besides being used together, other interface libraries are also supported.Redux
The body is small and small, only2KB
. Here we need to be clear:Redux
AndReact
Between, there is no strong binding relationship. This article aims to understand and implement aRedux
But it will not involvereact-redux
(It is enough to understand one knowledge point at a time.react-redux
Will appear in the next article).
2. Realize one from scratchRedux
Let’s forgetRedux
The concept of, starting with an example, usingcreate-react-app
Create a project:toredux
.
Code directory:myredux/to-redux
China.
willpublic/index.html
Inbody
Amend to read as follows:
<div id="app">
<div id="header">
前端宇宙
</div>
<div id="main">
<div id="content">大家好,我是前端宇宙作者刘小夕</div>
<button class="change-theme" id="to-blue">Blue</button>
<button class="change-theme" id="to-pink">Pink</button>
</div>
</div>
The function we want to achieve is shown in the above figure. When clicking the button, we can modify the color of the font of the entire application.
Modifysrc/index.js
The following (code:to-redux/src/index1.js
):
let state = {
color: 'blue'
}
//渲染应用
function renderApp() {
renderHeader();
renderContent();
}
//渲染 title 部分
function renderHeader() {
const header = document.getElementById('header');
header.style.color = state.color;
}
//渲染内容部分
function renderContent() {
const content = document.getElementById('content');
content.style.color = state.color;
}
renderApp();
//点击按钮,更改字体颜色
document.getElementById('to-blue').onclick = function () {
state.color = 'rgb(0, 51, 254)';
renderApp();
}
document.getElementById('to-pink').onclick = function () {
state.color = 'rgb(247, 109, 132)';
renderApp();
}
This application is very simple, but it has a problem:state
Is a shared state, but anyone can modify it, once we arbitrarily modify this state, it can lead to errors, for example, inrenderHeader
Inside, setstate = {}
, easy to cause unexpected errors.
However, most of the time, we do need to share the status, so we can consider setting some thresholds. For example, we have agreed that we cannot directly modify the global status, and we must modify it through a certain route. To this end, we define achangeState
Function, which is responsible for modifying the global state.
//在 index.js 中继续追加代码
function changeState(action) {
switch(action.type) {
case 'CHANGE_COLOR':
return {
...state,
color: action.color
}
default:
return state;
}
}
We agreed that only throughchangeState
To modify the state, it accepts a parameteraction
A that containstype
The ordinary object of the field,type
The field is used to identify your type of operation (i.e. how to modify the status).
We want to click on the button to change the font color of the entire application.
//在 index.js 中继续追加代码
document.getElementById('to-blue').onclick = function() {
let state = changeState({
type: 'CHANGE_COLOR',
color: 'rgb(0, 51, 254)'
});
//状态修改完之后,需要重新渲染页面
renderApp(state);
}
document.getElementById('to-pink').onclick = function() {
let state = changeState({
type: 'CHANGE_COLOR',
color: 'rgb(247, 109, 132)'
});
renderApp(state);
}
Pull away from store
Although we have now agreed on how to modify the status, howeverstate
Is a global variable, we can easily modify it, so we can consider turning it into a local variable and defining it inside a function (createStore
However, it needs to be used externally.state
So we need to provide a methodgetState()
In order that we maycreateStore
Getstate
.
function createStore (state) {
const getState = () => state;
return {
getState
}
}
Now, we can passstore.getState()
Method to get the state (what needs to be explained here is that,state
It is usually an object, so this object can be directly modified externally, but if it is deeply copiedstate
Return, then it must not be modified externally, given thatredux
Source code is directly returnedstate
, here we also do not make a deep copy, after all, cost performance).
It is not enough to just obtain the state. We also need to have methods to modify the state. Now the state is a private variable. We must also put the methods to modify the state into thecreateStore
, and expose it to external use.
function createStore (state) {
const getState = () => state;
const changeState = () => {
//...changeState 中的 code
}
return {
getState,
changeState
}
}
Now,index.js
The code in becomes the following (to-redux/src/index2.js
):
function createStore() {
let state = {
color: 'blue'
}
const getState = () => state;
function changeState(action) {
switch (action.type) {
case 'CHANGE_COLOR':
state = {
...state,
color: action.color
}
return state;
default:
return state;
}
}
return {
getState,
changeState
}
}
function renderApp(state) {
renderHeader(state);
renderContent(state);
}
function renderHeader(state) {
const header = document.getElementById('header');
header.style.color = state.color;
}
function renderContent(state) {
const content = document.getElementById('content');
content.style.color = state.color;
}
document.getElementById('to-blue').onclick = function () {
store.changeState({
type: 'CHANGE_COLOR',
color: 'rgb(0, 51, 254)'
});
renderApp(store.getState());
}
document.getElementById('to-pink').onclick = function () {
store.changeState({
type: 'CHANGE_COLOR',
color: 'rgb(247, 109, 132)'
});
renderApp(store.getState());
}
const store = createStore();
renderApp(store.getState());
Although, we are pulling away nowcreateStore
Method, but obviously this method is not universal at all.state
AndchangeState
Methods are defined increateStore
China. In this case, other applications cannot reuse this mode.
changeState
The logic of is supposed to be defined externally, because the logic of modifying the state must be different for each application. We stripped this part of the logic to the outside and renamed itreducer
(suppress ask why callreducer
The reason for asking is to make peace withredux
Keep consistent).reducer
What is it, to put it bluntly, is based onaction
To calculate the new state. Because it is not increateStore
Internally defined, not directly accessiblestate
So we need to pass the current state to it as a parameter. As follows:
function reducer(state, action) {
switch(action.type) {
case 'CHANGE_COLOR':
return {
...state,
color: action.color
}
default:
return state;
}
}
CreateStore evolution
function createStore(reducer) {
let state = {
color: 'blue'
}
const getState = () => state;
//将此处的 changeState 更名为 `dispatch`
const dispatch = (action) => {
//reducer 接收老状态和action,返回一个新状态
state = reducer(state, action);
}
return {
getState,
dispatch
}
}
Different applicationsstate
It must be different, we willstate
The value of is defined in thecreateStore
The interior must be unreasonable.
function createStore(reducer) {
let state;
const getState = () => state;
const dispatch = (action) => {
//reducer(state, action) 返回一个新状态
state = reducer(state, action);
}
return {
getState,
dispatch
}
}
Attention, everyonereducer
The definition of is to directly return to the old state when encountering unrecognized actions. Now, we use this to return to the initial state.
If you want tostate
There is an initial state, actually very simple, we will be the initialstate
The initialization value of is asreducer
The default value of the parameter for thecreateStore
To distribute one inreducer
If you don’t understand, you can do it. suchgetState
On the first call, you can get the default value of the state.
CreateStore evolution version 2.0
function createStore(reducer) {
let state;
const getState = () => state;
//每当 `dispatch` 一个动作的时候,我们需要调用 `reducer` 以返回一个新状态
const dispatch = (action) => {
//reducer(state, action) 返回一个新状态
state = reducer(state, action);
}
//你要是有个 action 的 type 的值是 `@@redux/__INIT__${Math.random()}`,我敬你是个狠人
dispatch({ type: `@@redux/__INIT__${Math.random()}` });
return {
getState,
dispatch
}
}
Now thiscreateStore
It can be used everywhere, but do you feel that every timedispatch
After that, all manuallyrenderApp()
It seems stupid. In the current application, it is called twice. If it needs to be modified 1000 timesstate
Do you call it 1,000 times manually?renderApp()
?
Can you simplify it? Called automatically every time the data changes.renderApp()
. Of course, we can’trenderApp()
Write increateStore()
Thedispatch
Because in other applications, the function name may not be calledrenderApp()
And may not only triggerrenderApp()
. It can be introduced herePublish subscription mode
To notify all subscribers when the state changes.
CreateStore evolution version 3.0
function createStore(reducer) {
let state;
let listeners = [];
const getState = () => state;
//subscribe 每次调用,都会返回一个取消订阅的方法
const subscribe = (ln) => {
listeners.push(ln);
//订阅之后,也要允许取消订阅。
//难道我订了某本杂志之后,就不允许我退订吗?可怕~
const unsubscribe = () => {
listeners = listeners.filter(listener => ln !== listener);
}
return unsubscribe;
};
const dispatch = (action) => {
//reducer(state, action) 返回一个新状态
state = reducer(state, action);
listeners.forEach(ln => ln());
}
//你要是有个 action 的 type 的值正好和 `@@redux/__INIT__${Math.random()}` 相等,我敬你是个狠人
dispatch({ type: `@@redux/__INIT__${Math.random()}` });
return {
getState,
dispatch,
subscribe
}
}
At this point, one of the simplestredux
It’s already been created,createStore
Yesredux
The core of. Let’s use this condensed versionredux
Rewrite our code,index.js
The contents of the file are updated as follows (to-redux/src/index.js
):
function createStore() {
//code(自行将上面createStore的代码拷贝至此处)
}
const initialState = {
color: 'blue'
}
function reducer(state = initialState, action) {
switch (action.type) {
case 'CHANGE_COLOR':
return {
...state,
color: action.color
}
default:
return state;
}
}
const store = createStore(reducer);
function renderApp(state) {
renderHeader(state);
renderContent(state);
}
function renderHeader(state) {
const header = document.getElementById('header');
header.style.color = state.color;
}
function renderContent(state) {
const content = document.getElementById('content');
content.style.color = state.color;
}
document.getElementById('to-blue').onclick = function () {
store.dispatch({
type: 'CHANGE_COLOR',
color: 'rgb(0, 51, 254)'
});
}
document.getElementById('to-pink').onclick = function () {
store.dispatch({
type: 'CHANGE_COLOR',
color: 'rgb(247, 109, 132)'
});
}
renderApp(store.getState());
//每次state发生改变时,都重新渲染
store.subscribe(() => renderApp(store.getState()));
If we want to finish clicking nowPink
After that, the font color cannot be modified, so we can also unsubscribe:
const unsub = store.subscribe(() => renderApp(store.getState()));
document.getElementById('to-pink').onclick = function () {
//code...
unsub(); //取消订阅
}
By the way:reducer
It is a pure function (if you don’t know the concept of pure function, consult the data yourself), it receives the previousstate
Andaction
And returns a newstate
. Don’t ask why.action
There must betype
Field, this is just a convention (redux
This is how it was designed)
Legacy: Whyreducer
Be sure to return a new onestate
, rather than directly modifystate
What? Welcome to leave your answer in the comment area.
In front of us we deduced step by stepredux
Now let’s review the core code ofredux
The design idea of:
Redux
design philosophy
-
Redux
The entire application state (state
) to a place (usually we call itstore
) - When we need to modify the status, we must distribute (
dispatch
) Oneaction
(action
It’s one withtype
The object of the field) - Special state processing function
reducer
Receive oldstate
Andaction
And returns a newstate
- via
subscribe
Set up subscriptions and notify all subscribers each time a distribution action is made.
We now have a basic versionredux
However, it still cannot meet our needs. Our usual business development is not as simple as the example written above, so there will be a problem:reducer
Functions can be very long becauseaction
There will be many types of. This is definitely not conducive to code writing and reading.
Imagine that there are 100 kinds of businesses in your business.action
Need to deal with, write this one hundred cases in a.reducer
Not only is the writing disgusting, but the colleagues who maintain the code later also want to kill people.
Therefore, we had better write it separately.reducer
, and then toreducer
To merge. Please welcome ourcombineReducers
(and)redux
The name of the library remains the same)
combineReducers
First of all, we need to be clear:combineReducers
Just a tool function, as we said earlier, it will be multiplereducer
Merge into onereducer
.combineReducers
What is returned isreducer
That is to say, it is a higher order function.
We will still use an example to illustrate, althoughredux
Not necessarily withreact
Cooperation, but in view of its relationship withreact
Cooperation is most suitable, here, in order toreact
Code for example:
This time, in addition to the above display, we added a counter function (usingReact
Refactoring = = >to-redux2
):
//现在我们的 state 结构如下:
let state = {
theme: {
color: 'blue'
},
counter: {
number: 0
}
}
Obviously, the modification theme and the counter can be separated by differentreducer
To deal with it is a better choice.
store/reducers/counter.js
State responsible for handling counters.
import { INCRENENT, DECREMENT } from '../action-types';
export default counter(state = {number: 0}, action) {
switch (action.type) {
case INCRENENT:
return {
...state,
number: state.number + action.number
}
case DECREMENT:
return {
...state,
number: state.number - action.number
}
default:
return state;
}
}
store/reducers/theme.js
State, which is responsible for modifying the theme color.
import { CHANGE_COLOR } from '../action-types';
export default function theme(state = {color: 'blue'}, action) {
switch (action.type) {
case CHANGE_COLOR:
return {
...state,
color: action.color
}
default:
return state;
}
}
Each ..reducer
Only responsible for managing the overall situationstate
Part of its responsibility. Each ..reducer
Thestate
The parameters are different and correspond to the part it manages.state
Data.
import counter from './counter';
import theme from './theme';
export default function appReducer(state={}, action) {
return {
theme: theme(state.theme, action),
counter: counter(state.counter, action)
}
}
appReducer
After the mergerreducer
But whenreducer
More often, this writing is also cumbersome, so we write a tool function to generate thisappReducer
, we named this tool functioncombineReducers
.
Let’s try writing this tool functioncombineReducers
:
Ideas:
-
combineReducers
Returnreducer
-
combineReducers
There are multiple references toreducer
The object of the composition - Each ..
reducer
Only deal with the globalstate
I am responsible for my own part
//reducers 是一个对象,属性值是每一个拆分的 reducer
export default function combineReducers(reducers) {
return function combination(state={}, action) {
//reducer 的返回值是新的 state
let newState = {};
for(var key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
}
}
Sonreducer
Will be responsible for returnstate
The default value of. For example, in this example,createStore
Dispatch({type:@@redux/__INIT__${Math.random()}
}), and passed to thecreateStore
The truth iscombineReducers(reducers)
Function returnedcombination
.
According tostate=reducer(state,action)
,newState.theme=theme(undefined, action)
,newState.counter=counter(undefined, action)
,counter
Andtheme
Two childrenreducer
Return separatelynewState.theme
AndnewState.counter
The initial value of the.
Use thiscombineReducers
Can be rewrittenstore/reducers/index.js
import counter from './counter';
import theme from './theme';
import { combineReducers } from '../redux';
//明显简洁了许多~
export default combineReducers({
counter,
theme
});
We wrote itcombineReducers
Although it seems to be able to meet our needs, it has one disadvantage: it returns a new one every time.state
Object, which causes meaningless re-rendering when the data does not change. Therefore, we can judge the data and return the original data when there is no change in the data.state
Just.
Combinatorial combineReducers evolutionary edition
//代码中省略了一些判断,默认传递的参数均是符合要求的,有兴趣可以查看源码中对参数合法性的判断及处理
export default function combineReducers(reducers) {
return function combination(state={}, action) {
let nextState = {};
let hasChanged = false; //状态是否改变
for(let key in reducers) {
const previousStateForKey = state[key];
const nextStateForKey = reducers[key](previousStateForKey, action);
nextState[key] = nextStateForKey;
//只有所有的 nextStateForKey 均与 previousStateForKey 相等时,hasChanged 的值才是 false
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
//state 没有改变时,返回原对象
return hasChanged ? nextState : state;
}
}
applyMiddleware
Official documentsAboutapplyMiddleware
The explanation is very clear, the following contents also refer to the contents of the official documents:
Logging
Consider a small problem, if we want to print it out in the console before every state change.state
So how do we do it?
The simplest is:
//...
<button onClick={() => {
console.log(store.getState());
store.dispatch(actions.add(2));
}}>+</button>
//...
Of course, this method is definitely not desirable. If we distribute it 100 times in our code, we cannot write it 100 times. Since it is printing when the status changesstate
, that is to say, indispatch
Previously printedstate
, then we can rewritestore.dispatch
Method, print before distributionstate
Just.
let store = createStore(reducer);
const next = store.dispatch; //next 的命令是为了和中间件的源码一致
store.dispatch = action => {
console.log(store.getState());
next(action);
}
Crash information
Suppose we don’t just need to printstate
, you also need to print out the error message when there is an exception in distribution.
const next = store.dispatch; //next 的命名是为了和中间件的源码一致
store.dispatch = action => {
try{
console.log(store.getState());
next(action);
} catct(err) {
console.error(err);
}
}
However, if we have other requirements, we need to constantly revise them.store.dispatch
Method, resulting in this part of the code is difficult to maintain.
So we need separationloggerMiddleware
AndexceptionMiddleware
.
let store = createStore(reducer);
const next = store.dispatch; //next 的命名是为了和中间件的源码一致
const loggerMiddleware = action => {
console.log(store.getState());
next(action);
}
const exceptionMiddleware = action => {
try{
loggerMiddleware(action);
}catch(err) {
console.error(err);
}
}
store.dispatch = exceptionMiddleware;
We know, a lotmiddleware
All provided by a third party, thenstore
Must be passed as a parameter tomiddleware
, further rewrite:
const loggerMiddleware = store => action => {
const next = store.dispatch;
console.log(store.getState());
next(action);
}
const exceptionMiddleware = store => action => {
try{
loggerMiddleware(store)(action);
}catch(err) {
console.error(err);
}
}
//使用
store.dispatch = exceptionMiddleware(store)(action);
There is still a small problem.exceptionMiddleware
hit the targetloggerMiddleware
It is written to death, which is certainly unreasonable. We hope that this is a parameter, so it is flexible to use. There is no reason onlyexceptionMiddleware
Need to be flexible, regardless ofloggerMiddleware
To be further rewritten as follows:
const loggerMiddleware = store => next => action => {
console.log(store.getState());
return next(action);
}
const exceptionMiddleware = store => next => action => {
try{
return next(action);
}catch(err) {
console.error(err);
}
}
//使用
const next = store.dispatch;
const logger = loggerMiddleware(store);
store.dispatch = exceptionMiddleware(store)(logger(next));
Now, we already have GMmiddleware
The writing format of the.
middleware
Received onenext()
Thedispatch
Function and returns adispatch
Function, the returned function will be taken as the nextmiddleware
Thenext()
However, there is a small problem. When there is a lot of middleware, the code for using middleware becomes very complicated. To this end,redux
One is providedapplyMiddleware
The tool function of.
As we can see from the above, what we need to change in the end is actuallydispatch
So we need to rewrite itstore
, return modifieddispatch
After the methodstore
.
Therefore, we can clarify the following points:
-
applyMiddleware
The return value isstore
-
applyMiddleware
It must be acceptedmiddleware
As a parameter -
applyMiddleware
To accept{dispatch, getState}
As a participant, howeverredux
Source code into the reference iscreateStore
AndcreateStore
I think it is not necessary to create a new one outsidestore
After all, created externallystore
In addition to being passed as a parameter into a function, it has no other effect, so it is better to put thecreateStore
AndcreateStore
Parameters to be used are passed in.
//applyMiddleWare 返回 store.
const applyMiddleware = middleware => createStore => (...args) => {
let store = createStore(...args);
let middle = loggerMiddleware(store);
let dispatch = middle(store.dispatch); //新的dispatch方法
//返回一个新的store---重写了dispatch方法
return {
...store,
dispatch
}
}
The above is onemiddleware
But we know that,middleware
It may be one or more, and we mainly need to solve more than onemiddleware
The problem of, further rewrite.
//applyMiddleware 返回 store.
const applyMiddleware = (...middlewares) => createStore => (...args) => {
let store = createStore(...args);
let dispatch;
const middlewareAPI = {
getState: store.getstate,
dispatch: (...args) => dispatch(...args)
}
//传递修改后的 dispatch
let middles = middlewares.map(middleware => middleware(middlewareAPI));
//现在我们有多个 middleware,需要多次增强 dispatch
dispatch = middles.reduceRight((prev, current) => current(prev), store.dispatch);
return {
...store,
dispatch
}
}
I don’t know if everyone understands the abovemiddles.reduceRight
, the following detailed explanation for everyone:
/*三个中间件*/
let logger1 = ({dispatch, getState}) => dispatch => action => {
console.log('111');
dispatch(action);
console.log('444');
}
let logger2 = ({ dispatch, getState }) => dispatch => action => {
console.log('222');
dispatch(action);
console.log('555')
}
let logger3 = ({ dispatch, getState }) => dispatch => action => {
console.log('333');
dispatch(action);
console.log('666');
}
let middle1 = logger1({ dispatch, getState });
let middle2 = logger2({ dispatch, getState });
let middle3 = logger3({ dispatch, getState });
//applyMiddleware(logger1,logger2,logger3)(createStore)(reducer)
//如果直接替换
store.dispatch = middle1(middle2(middle3(store.dispatch)));
Observe the abovemiddle1(middle2(middle3(store.dispatch)))
If we putmiddle1
,middle2
,middle3
As each item of the array, if you are familiar with the API of the array, you can think ofreduce
If you are not familiar with itreduce
, you can viewMDN document.
//applyMiddleware(logger1,logger3,logger3)(createStore)(reducer)
//reduceRight 从右到左执行
middles.reduceRight((prev, current) => current(prev), store.dispatch);
//第一次 prev: store.dispatch current: middle3
//第二次 prev: middle3(store.dispatch) current: middle2
//第三次 prev: middle2(middle3(store.dispatch)) current: middle1
//结果 middle1(middle2(middle3(store.dispatch)))
Read itredux
The source code of the students, may know that the source code is to provide a.compose
Function, andcompose
Not used in functionreduceRight
, but instead uses thereduce
So the code is slightly different. But the analysis process is still the same.
compose.js
export default function compose(...funcs) {
//如果没有中间件
if (funcs.length === 0) {
return arg => arg
}
//中间件长度为1
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((prev, current) => (...args) => prev(current(...args)));
}
Aboutreduce
It is suggested that the writing style be like the one above.reduceRight
Similarly, conduct an analysis
Usecompose
Tool function rewriteapplyMiddleware
.
const applyMiddleware = (...middlewares) => createStore => (...args) => {
let store = createStore(...args);
let dispatch;
const middlewareAPI = {
getState: store.getstate,
dispatch: (...args) => dispatch(...args)
}
let middles = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...middles)(store.dispatch);
return {
...store,
dispatch
}
}
bindActionCreators
redux
It also provides us withbindActionCreators
Tool function, this tool function code is very simple, we seldom use it directly in the code.react-redux
Will be used in. Here, briefly explain:
//通常我们会这样编写我们的 actionCreator
import { INCRENENT, DECREMENT } from '../action-types';
const counter = {
add(number) {
return {
type: INCRENENT,
number
}
},
minus(number) {
return {
type: DECREMENT,
number
}
}
}
export default counter;
At the time of distribution, we need to write like this:
import counter from 'xx/xx';
import store from 'xx/xx';
store.dispatch(counter.add());
Of course, we can also write our actionCreator as follows:
function add(number) {
return {
type: INCRENENT,
number
}
}
When distributing, need to write like this:
store.dispatch(add(number));
The above codes have one thing in common, that is, they are allstore.dispatch
Send out an action. So we can consider writing a function that willstore.dispatch
AndactionCreator
Bind them together.
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args));
}
function bindActionCreators(actionCreator, dispatch) {
//actionCreators 可以是一个普通函数或者是一个对象
if(typeof actionCreator === 'function') {
//如果是函数,返回一个函数,调用时,dispatch 这个函数的返回值
bindActionCreator(actionCreator, dispatch);
}else if(typeof actionCreator === 'object') {
//如果是一个对象,那么对象的每一项都要都要返回 bindActionCreator
const boundActionCreators = {}
for(let key in actionCreator) {
boundActionCreators[key] = bindActionCreator(actionCreator[key], dispatch);
}
return boundActionCreators;
}
}
In use:
let counter = bindActionCreators(counter, store.dispatch);
//派发时
counter.add();
counter.minus();
It doesn’t seem that there is too much simplification here, but it will be analyzed later.react-redux
When, will explain why this tool function is needed.
At this point, myredux
It has been basically written. Andredux
Compared with the source code of, there are still some differences, such ascreateStore
ProvidedreplaceReducer
Methods, andcreateStore
The second and third parameters of the are not mentioned, so you can understand them by looking at the code a little bit, and they will not be expanded here.
Reference link
- React.js small book
- Redux chinese document
- Fully understand redux (implementing one redux from zero)