这是我参与更文挑战的第17天,活动详情查看: 更文挑战。
前言
Redux
自2015年横空出世后,很快席卷了 React 社区,备受推崇。那么它究竟是什么,怎么使用,又能解决 React 应用开发过程中的哪些痛点?
本文,我们将一起学习 Redux 并简单实现一个 Redux, 同时也会捎带讲一下如何使用中间件来增强redux的能力。
Redux 是什么
React 应用离不开组件,而组件的状态非常重要。React 官方提倡单向数据流,这在一定程度上将组件间的数据通信简化了。但是往往一个团队内的不同成员的开发习惯不同,应用的状态难免会随着迭代的进行变得复杂且混乱。
此时,Redux 作为一个应用数据流架构,可以和 React 完美配合,一定程度上解决上面的问题。
Redux is a predictable state container from JavaScript app.
对于 Javascript 应用而言, Redux 是一个可预测状态的“容器”。
相比于 Jquery 等,Redux 是库,又不仅仅是库。 Redux 约定了开发者在使用时的条条框框,更像是一个一种设计模式。
Redux 借鉴了函数式编程的思想,采用了单向数据流理念。它只专注于全局状态的管理,并且为数据状态的管理封装提供了很多方法。通过一系列的“魔法”和“约定模式”,使的数据状态变得可预测、可追溯。
Redux 和 React 的关系
两者并没关系,Redux 只关心数据状态的管理,它可以和任何视图库配合使用。react-redux
作为桥梁,将两者结合起来,构建出一个“有序”的应用架构。
Redux 设计哲学
Single source of truth
这里指的是数据来源的单一。
在使用 React 时,应用就是一个个状态机。无论是简单的应用,还是一个复杂的应用,我们都是使用一个 Javascript 对象来描述整个状态机的状态,并存储在 store 中。
这样设计的好处在于,我们在处理状态时只需要把注意力全部集中到这个对象上即可,而不用关心各个组件内的状态数据。
State is read-only
状态是“只读”的。这里的“只读”不是指对象不能修改,而是当页面需要新的数据状态时重新生成一个全新的状态数据树,使得 store.getState()
返回的是一个全新的对象。
那么,我们该如何生成一个新的状态数据树呢?
Redux 规定了,当页面需要展现新的数据树时,我们需要 dispatch
(派发)一个 action
(动作)。这个 action 也是一个普通对象,它描述了一个动作单元变化的所有信息。
Changes are made with pure functions called reducer
action 经过 reducer
函数处理后,会返回一个新的对象(新的状态数据树)。
(preState, action) => newState
复制代码
如上所示,reducer 接收两个参数:
- 当前页面数据状态
- 派发的 action
reducer 和 Array.prototype.reduce 有什么关系
Javascript 数组的 reduce 方法是一种运算合成,通过遍历,将数组的所有成员“累积”为一个值。
与此类似,reducer 在具备初始状态的情况下,每一次运算其实都是根据之前的状态和现有的 action 来更新 state 的,这个 state 可以理解为上面所说的累积的结果。这也是 Redux 中函数式编程的体现。
reducer 函数必须是纯函数
纯函数代表这样一类函数:
- 对于指定输出,返回指定结果
- 不存在副作用
reducer 使用纯函数,为开发和调试带来了便利性。
Store
store 是 Redux 的核心概念。之前我们说 Redux 是一个可预测状态的“容器”,这个容器指的就是 store。
store 是一个普通对象,包含了 dispatch
以及获取页面状态数据树的方法等。
store = {
dipatch,
getState,
subscribe,
replaceReducer
}
复制代码
Action
action 描述了状态变更的信息。通常我们规定 action 有个 type
属性,用来确定 action 的唯一性。另外我们还定义 payload
属性,用来携带一些数据信息。
action = {
type: 'UPDATE_MESSAGE',
payload: {
msg: "hello world"
}
}
复制代码
Reducer
action 描述状态变更的信息,真正落实变化的是 reducer 函数。
function reducer(state = initState, action){
case 'UPDATE_MESSAGE':
return {
...state,
msg: action.payload.msg
};
break;
default:
return state;
....
}
复制代码
实现一个简单的 Redux
Redux 基础实现就是一个发布订阅系统,只是在这个基础上还应用了函数式编程等其他编程范式。
Store
我们简单实现一个 Store
:
// store.js
class Store {
constructor(reducer) {
this.reducer = reducer; // 单个 reducer 函数
this.state = reducer(undefined, { type: undefined }); // 运行一次 reducer,拿到初始的数据状态
this.listeners = []; // 订阅者列表
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => {
listener(this.state);
});
}
subscribe(fn) {
if (typeof fn === "function") {
this.listeners.push(fn);
}
}
getState() {
return this.state;
}
}
module.exports = Store;
复制代码
有 Store 类,这个类已经拥有了 dispatch
派发动作、添加订阅以及获取状态数据树的能力。然后我们要创建实例。
createStore
// createStore.js
const Store = require("./store.js");
function createStore(reducer) {
let store = new Store(reducer);
return store;
}
module.exports = createStore;
复制代码
测试一下,这个可不可以正常工作。
// test.js
const createStore = require("./createStore.js");
let initState = {
msg: "hello",
};
function reducer(state = initState, action) {
switch (action.type) {
case "update_msg":
return {
...state,
msg: action.payload,
};
}
}
const store = createStore(reducer);
store.subscribe((data) => {
console.log("触发了");
console.log(data);
console.log(store.getState())
});
store.dispatch({
type: "update_msg",
payload: "hello world",
});
复制代码
输出:
触发了
{ msg: 'hello world' }
{ msg: 'hello world' }
复制代码
combineReducers
上面的 store 只支持单个reducer。当应用复杂的时候,我们要拆分reducer,Redux提供了一个 combineReducers
方法。我们也来实现一个:
function combineReducers(reducers) {
Object.defineProperty(reducers, "@@isReducers", {
configutable: true,
enumerable: false, // 避免被枚举到
value: true,
});
return reducers;
}
module.exports = combineReducers;
复制代码
修改一下 Store
类
class Store {
constructor(reducer) {
this.reducer = reducer;
this.combined = "@@isReducers" in reducer // 存在,表示使用了combineReducers
// 新增
this.state = this.combined
? Object.keys(reducer).reduce((rootState, key) => {
rootState[key] = reducer[key](undefined, { type: undefined });
return rootState;
}, {})
: reducer(undefined, { type: undefined });
this.listeners = [];
}
dispatch(action) {
// 新增
this.state = this.combined
? Object.keys(this.reducer).reduce((rootState, key) => {
rootState[key] = this.reducer[key](rootState[key], action);
return rootState;
}, this.state)
: this.reducer(this.state, action);
this.listeners.forEach((listener) => {
listener(this.state);
});
}
...
}
module.exports = Store;
复制代码
这样 combineReducers
也就实现了。
createStore 既支持 createStore(reducer)
, 也支持 createStore(combineReducers(reducers))
实现一个 redux-thunk,支持异步action
redux-thunk
是通过 Redux 的中间件特性扩展出来的。要想支持异步,action 就得是函数。
applyMiddleware
Redux 的中间件的执行时机:dispatch 之后, reducer 执行之前。
我们来模拟下添加中间件的方法 applyMiddleware
:
const compose = require("lodash/fp/compose");
function applyMiddleware(...middlewares) {
return (store) => {
const chain = middlewares.map((m) =>
m({
dispatch: (action) => store.dispatch(action),
getState: store.getState,
})
);
const dispatch = compose(chain)(store.dispatch);
store.dispatch = dispatch;
return store;
};
}
module.exports = applyMiddleware;
复制代码
redux-thunk
接入了 redux-thunk
后,如果 action 是个函数,要把 store.dispatch
传进去:
// redux-thunk.js
function thunk({ dispatch, getState }) {
return function (next) {
return function (action) {
if (typeof action === "function") {
action(dispatch, getState);
} else {
next(action);
}
};
};
}
module.exports = thunk;
复制代码
注意,Store
中的 dispatch
需要改成箭头函数。
测试一下:
const createStore = require("./createStore.js");
const combineReducers = require("./combineReducers.js");
const applyMiddleware = require("./applyMiddleware.js");
const thunk = require("./redux-thunk.js");
function reducer(
state = {
msg: "hello",
},
action
) {
switch (action.type) {
case "update_msg":
return {
...state,
msg: action.payload,
};
default:
return state;
}
}
const store = createStore(reducer);
applyMiddleware(thunk)(store);
let date = +new Date();
store.subscribe((data) => {
console.log("触发了", +new Date() - date);
console.log(data);
console.log(store.getState());
});
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch({
type: "update_msg",
payload: "hello xxx",
});
}, 1000);
});
store.dispatch({
type: "update_msg",
payload: "hello world",
});
复制代码
输出:
触发了 0
{ msg: 'hello world' }
{ msg: 'hello world' }
触发了 1004
{ msg: 'hello xxx'}
{ msg: 'hello xxx'}
复制代码
实现一个日志中间件 redux-logger
和 redux-thunk类似,也是很简单
// redux-logger
function logger({ dispatch, getState }) {
return function (next) {
return function (action) {
console.log(action);
next(action);
};
};
}
module.exports = logger;
复制代码
总结
本文我们简单了解了什么 Redux,以及 Redux 的设计哲学,并且我们还实现了一个简单的 redux
和 redux-thunk
。
最后的简易实现demo,我也贴出来,感兴趣的可以去看看—— mini-redux