手撸一套redux和react-redux
- Redux
Redux
TIPS:
请在对redux有一定应用后,再往下阅读。
在当前的前端框架中,状态管理是核心问题之一。项目中有多个组件,都需要用到某个状态,需要进行状态的传递,也就是组件的通信。
比如A
中这样一个状态
const state = {
count: 0
}
而B
需要用到这个状态,那么就需要B
去监听这个状态,然后 A
告诉B
自己发布(更新)了状态,最后B
再去获取这个状态。这样就完成了一个状态传递。
在这个过程中,我们分别用一个属性
和三个方法
来定义这个过程。其中
- 一个属性,是指当前状态,我们用
currentState
来表示 - 三个方法,是指上面提到的对状态的
监听
、发布
和获取
- 监听,用
subscribe
方法来表示 - 发布,用
dispatch
方法来表示 - 获取,用
getState
来表示 - 此外,既然有监听,那么,就会有取消监听的需求,我们可以在监听方法中来定义取消监听的方法。
- 监听,用
好了,这三个方法,刚好就是redux
的核心方法。
现在有个新的问题,假定当前状态中count
是0
,即
const currentState = {
count: 0
}
然后经过A
的操作,count
变成了1
,如果currentState
最新的值没有被保存下来,那么B
去获取currentState
时,得到的结果仍然是0
,这怎么办呢?思考一下,在js
中,什么情况下,一个值会被保存下来,你大概想到了立即执行函数和闭包。没错,redux
在这里确实是用闭包来保存值的。现在我们就来实现这一步,我们用一个仓库来存储这些方法和状态,用代码表示出来就是
const createStore = () => {
let currentState = {};
function getState() {}
function dispatch() {}
function subscribe() {}
return { getState, dispatch, subscribe };
}
我们调用createStore
后,会得到一个对象,这个对象中包含了获取状态、发布状态和监听状态的对象,我们用store
来表示这个对象。
const store = createStore();
currentState
可以保存当前最新状态,我们只需要在获取状态时,返回这个最新状态即可
function getState() {
return currentState;
}
用户在操作时会更新状态,然后发布给其他监听的用户,说状态变了,你们快更新一下数据。用户会有不同的操作,产生不同的数据,我们用action
对象来表示用户的操作,用type
属性来表示用户的操作类型。就像这样
action = {
type: 操作类型(string)
}
在用户发布(dispatch)
时,我们给他传入用户的操作,根据不同的操作,来更新不同的状态,比如增加数值或者减少数值的操作,那么dispatch
应该是这样
function dispatch(action) {
switch(action.type) {
case "增加":
currentState = {
count: currentState.count + 1
};
case "减少":
currentState = {
count: currentState.count - 1;
};
default:
currentState = currentState
}
}
当用户执行增加
的操作时,状态的值就加1,用户执行减少
的操作时,状态就减1,其他情况,数值仍然保持初始值。但是这样写过于臃肿,且过于死板,我们优化一下。用一个reducer
函数来剥离改变状态的过程,并返回变更后的新状态
const initState = {
count: 0
}
const reducer = (state = initState, action) => {
switch(action.type) {
case 'increament':
return {
...state,
count: state.count + 1
}
case 'decreament':
return {
...state,
count: state.count - 1
}
default:
return initState;
}
}
此时dispatch
函数就可以改写为
function dispatch(action) {
// 每次的初始状态,都是之前的currentState
// 把返回的新的状态重新赋值给currentState
// currentState保存在闭包中
currentState = reducer(currentState, action);
}
reducer
需要通过参数传入createStore
函数
const createStore = (reducer) => {
let currentState = {};
function getState() {
return currentState;
}
function dispatch(action) {
currentState = reducer(currentState, action);
}
function subscribe() {}
return { getState, dispatch, subscribe };
}
现在我们来写个完整的示例来验证一下(为了方便展示,这里就不单独分开多个文件来写了,把createStore, reducer, store统一放在store.js文件里)
// src/redux/store.js
const initState = {
count: 0
};
const reducer = (state = initState, action) => {
switch (action.type) {
case 'increament':
return {
...state,
count: state.count + 1
};
case 'decreament':
return {
...state,
count: state.count - 1
};
default:
return initState;
}
}
const createStore = (reducer) => {
let currentState = {};
function getState() {
return currentState;
}
function dispatch(action) {
currentState = reducer(currentState, action);
}
function subscribe() { }
return { getState, dispatch, subscribe };
}
export const store = createStore(reducer);
// src/index.js
import { createRoot } from 'react-dom/client';
import App from './App';
import { store } from './redux/store';
// 先获取一次状态
console.log('store1: ', store.getState());
// 执行一次增加的操作
store.dispatch({ type: 'increament' });
// 再获取一次当前状态
console.log('store2: ', store.getState());
const root = createRoot(document.getElementById('root'));
root.render(
<App />
)
看下打印结果
奇怪了,一个是空值,一个是NaN
,为啥呢?我们回头看,整个过程发生了什么。首先我们通过执行createStore
生成了store
对象,然后调用了getState
获取当前状态,此时currentState
是空值。然后我们执行了一次增加的操作,此时在reducer
中,state
是{ }
,增加操作就是undefined + 1
,结果就是NaN
。问题就出在currentState
初始是个空值,然而我们并不可能在源码中给currentState
去设置一个非空值,因为我们不可能在每次状态变更后去修改源码。注意看,initState
是我们给reducer
设置的初始状态,这个是开发者自己去设置的,在reducer
函数中,如果action.type
既不是增加,也不是减少,currentState
就会被赋值为initState
,这样就可以让currentState
成为非空值了(这也是reducer
中不写default
就会报错的原因)。所以,我们需要在执行createStore
去创建store
对象前,就先执行一次dispatch
函数,让currentState
初始化。因此我们在createStore
中加入一句代码
const createStore = (reducer) => {
let currentState = {};
function getState() {
return currentState;
}
function dispatch(action) {
currentState = reducer(currentState, action);
}
function subscribe() { }
// 加入该代码
// 先执行一次状态修改,让curretState获取到初始化的状态
// 这里的type值一定不能跟实际项目中的type值重复
// 在源码中,type值还加入了随机数
dispatch({ type: '@@REDUX__INIT' });
return { getState, dispatch, subscribe };
}
此时再执行一遍上面的例子,就可以得到我们想要的结果
到这里,我们实现了状态的初始化、保存最新状态、获取状态以及状态的更新。
下一篇继续实现redux和react-redux