很早之前实现过Redux源码,但是没有形成文档。最近得空整理这篇文章,我们将一起学习:
- 函数式编程:柯里化、compose聚合函数(优点灵活、缺点思路绕)、洋葱模型
- redux的设计原理和源码实现
- applyMiddleware函数原理和实现
- 中间件函数
- React-Redux实现原理
函数式编程
柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
const add = (v1, v2, v3) => v1 + v2 + v3;
const result = add(1, 2, 3);
// 上述过程转换为下面,就是一个柯里化过程
const sum = (v1) => {
return (v2) => {
return (v3) => {
return v1 + v2 + v3;
}
}
}
const result1 = sum(1)(2)(3)
柯里化是函数返回函数,这样就可以依次执行
compose函数
数组求和
如何实现数组的求和
const arr = [1, 2, 3, 4];
// 1.for循环
const fn1 = () => {
let res = 0;
for (let i = 0; i < arr.length; i++) {
res += arr[i]
}
return res;
}
// 2.利用Array.prototype.reduce函数
const fn2 = () => {
const reducer = (res, curValue) => res + curValue;
arr.reduce(reducer)
}
可以通过两种方式实现数组的求和:
- 循环
- Array.prototype.reduce
注:实际上reduce类似于pipe函数(从左往右执行),但是这里不展开
函数依次执行
如题,我希望将我定义的三个函数依次执行
function fn1(arg) {
console.info("fn1", arg)
return arg;
}
function fn2(arg) {
console.info("fn2", arg)
return arg;
}
function fn3(arg) {
console.info("fn3", arg)
return arg;
}
- 1.直接执行
fn1("max");
fn2("max");
fn3("max");
// 执行顺序 fn1 -> fn2 -> fn3
- 2.链式,容易进入嵌套地狱,洋葱模型
fn3(fn2(fn1("max")))
// 执行顺序 fn1 -> fn2 -> fn3
- 3.compose聚合函数
function compose(...fns) {
return fns.reduce((a, b) => (...args) => a(b(...args)))
}
const newFn = compose(fn1, fn2, fn3)
newFn("max"); // 执行顺序 fn3 -> fn2 -> fn1
函数和普通值的区别是函数需要接收参数,所以reduce返回的是一个函数A,A接收args作为,作为最里层函数b的参数,b(...args)执行的返回结果又作为函数a的参数
考虑到compose函数没有参数的情况,获取只接受一个函数的情况,可以对上面的compose函数做下扩展
function compose(...fns) {
if (fns.length === 0) {
return arg => arg
}
if (fns.length === 1) {
return fns[0];
}
return fns.reduce((a, b) => (...args) => a(b(...args)))
}
上面的newFn等同于const newFn = (...args) => fn1(fn2(fn3(...args)))。执行newFn("max")等同于执行fn1(fn2(fn3("max")))
上面的例子就是一个简单的compose函数(从右向左执行)。到目前好像和redux还没有什么关系,咱们继续往下看
洋葱模型
第一次听说洋葱模型还是在学习Koa的时候,因为流程酷似一个洋葱而得名。
层层包裹的函数,底层函数把执行结果抛给上一层函数,是不是和上面提到的compose函数很像?
- 上图中的store.dispatch是增强后的dispatch
- mid3里面的dispatch是createStore返回的原始dispatch
- 从上面两张图可以看出,洋葱模型中每个函数都会接收一个
next,这个next参数是函数的时候(一般表示更里一层的中间件函数),函数自己控制这个next的执行时机 - next的类型需要做判断,因为next有两种情况:
- 1.函数:可能是下一层中间件函数、也可能是最里层的中间件函数,接收到的是函数作为参数(Redux的中间件就是这种情况,因为最后接收到的是原生dispatch)
- 2.其他变量:只有一种情况就是最内层接收到参数
通过简化Koa的核心代码koa-compose来加深理解
// 下面是3个中间件函数
function fn1(next) {
console.info("当前执行fn1的前置");
next();
console.info("当前执行fn1的后置")
}
function fn2(next) {
console.info("当前执行fn2的前置");
next();
console.info("当前执行fn2的后置")
}
function fn3(next) {
console.info("当前执行fn3的前置");
next();
console.info("当前执行fn3的后置")
}
// 这个也是koa-compose的简化版
function compose (middleware) {
return function (next) {
function dispatch (i) {
console.info(next)
let fn = middleware[i]
if (i === middleware.length) {
fn = next
}
if (!fn) return;
return fn(function next () {
return dispatch(i + 1)
})
}
return dispatch(0)
}
}
var composeMiddles = compose([fn1,fn2,fn3])
composeMiddles()
/**
当前执行fn1的前置
当前执行fn2的前置
当前执行fn3的前置
当前执行fn3的后置
当前执行fn2的后置
当前执行fn1的后置
*/
上面的composeMiddles函数等同于
(...args) => fn1(null, function next() {
fn2(null, function next() {
fn3(null, function next() {
return;
})
})
})
所以:fn1函数最先执行、最后结束
那个啥,应该懂了吧?
Reducer
Reducer就是纯函数,接收旧的 state 和 action,返回新的 state
(prevState, action) => nextState
因为和Array.prototype.reduce(reducer, initialValue)里回调函数类似而得名。保持reducer纯净很重要,固定的入参数决定了固定的输出结果,应该只是一个纯粹的执行工具。所以不要在Reducer函数中:
- 修改传入参数
- 执有副作的操作,如 API 请求和路由跳转等
- 调用非纯函数,如Date.now()、Math.random()等
Redux的使用规则
知道怎么用才能知道需要实现什么功能,如果你很熟悉Redux的使用规则,可以直接跳过该章节
三大原则
- 数据唯一性原则:全局只有一个store
- 保持只读状态:state是只读的,要修改state的方法就通过触发action
- 数据改变只能通过纯函数进行:也就是reducers
Redux Flow
- React Components提交Action派发更新
- store通过调用Reducer修改store中的值
- store中的值在修改后,影响到React components(用
subscribe+forceUpdate实现) - Redux只是纯粹的状态管理器,默认只支持
同步
数据的改变流程:
使用规则
// store.js
import { createStore, applyMiddleware } from "redux";
// 定义Reducer
function reducer(state = 0, action) {
switch(action.type) {
case "add":
return state + 1;
case "minus":
return state - action.payload || 1;
default:
return state;
}
}
// createStore接收Reducer、applyMiddleware作为参数
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
// App.jsx
import store from "./store";
class App extends React.Component {
componentDidMount() {
// 这里通过订阅数据变化,实现数据更新
store.subscribe(() => {
this.forceUpdate();
})
}
add = () => {
// 同步更新
store.dispatch({ type: "add" })
}
// 这里的dispatch接收的Action是一个函数,需要用到中间件
asyncAdd = () => {
store.dispatch((dispatch, getState) => {
setTimeout(() => {
dispatch({ type: "add" })
}, 100)
})
}
render() {
console.info(store.getState()); // 打印当前store中的值
return (
<button onClick={this.add}>add</button>
<button onClick={this.asyncAdd}>asyncAdd</button>
)
}
}
Redux源码
从上面大致可以梳理出(不考虑middleware)
- 1.createStore函数创建、返回store对象包含
- 2.getState 获取当前状态值state
- 3.dispatch 提交更新
- 4.subscribe 订阅state变化
除了上面可以直接看出来的,在实现的过程中创建以后需要一个初始化的过程
- 5.reducer初始化、修改状态函数
/**
1.createStore函数创建、返回store
@param reducer Reducer函数
@return store
*/
function createStore(reducer) {
let currentState; // 当前保存的状态值
let currentListeners = []; // 所有subscribe的监听函数
// 2.获取当前状态值
function getState() {
return currentState;
}
// 3.dispatch
function dispatch(action) {
currentState = reducer(currentState, action);
// state改变,执行订阅的函数
currentListeners.forEach(listener => listener());
}
// 4.消息监听
function subscribe(listener) {
currentListener.push(listener)
return () => { // 取消监听
const index = currentListener.indexOf(listener);
currentListener.splice(index, 1)
}
}
}
// 5.reducer初始化、修改状态函数。也就是主动执行一次dispatch。否则 currentState 没有初始值
dispatch({ type: Symbol() });
return {
getState,
dispatch,
subscribe
}
以上就是Redux的源码,是不是很简单?
applyMiddleware
applyMiddleware函数是Redux最精髓部分。中间件拦截的是dispatch提交到执行reducer的过程,从而增强dispatch的功能
applyMiddleware源码
- 改造一个中间件的核心思想是
返回新的dispatch而不是替换原dispatch - 强烈推荐看下官网如何实现一个中间件内容
1.createStore函数改造
改造下createStore函数,检测第二个中间件参数,如果有就把createStore和reducer交给中间件进行扩展处理,代码如下
/**
创建一个store
@param reducer Reducer函数
@param enhancer 增强,也就是中间件
*/
function createStore(reducer, enhancer) {
if (enhancer) {
// 如果有加强,则加强下store.dispatch
return enhancer(createStore)(reducer) // 柯理化
}
let currentState; // 当前保存的状态值
// ........
}
执行createStore(reducer, applyMiddleware(thunk, logger)),等同于applyMiddleware(thunk, logger)(createStore)(reducer)
2.applyMiddleware函数框架
第一步可以推断出applyMiddleware函数接收第一个参数是中间件,返回函数(2次)接收的分别是createStore、reducer的柯里化过程。如下:
function applyMiddleware(...middlewares) {
return createStore => reducer => {
const store = createStore(reducer); // 这个还是原来的store。和createStore函数执行的没有区别
let dispatch = store.dispatch;
// todo 加强dispatch
// 执行一次dispatch,相当于 所有中间件函数 依次执行和store.dispatch执行
// dispatch = ...
return {...store, dispatch}; // 返回的还是那些东西,只是dispatch被包装过(加强)
}
}
到目前为止,已经初步完成了applyMiddleware函数的架子,从上面看,我们剩下要做的就是加强dispatch。同时我们要知道:执行一次dispatch,相当于所有中间件函数依次执行和store.dispatch执行。
开始将store.dispatch赋值给dispatch变量,我们想要强化dispatch,也就是要给dispatch重新赋值,让dispatch可以实现依次执行的目的
3.compose函数
到此时,就需要使用到上面的洋葱模型,来将各个中间件进行包装,先写好compose函数
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
上面的compose函数,返回的是一个待执行的函数,看下下面结果
function fn1(arg) {
console.info("执行fn1")
return arg + 1;
}
function fn2(arg) {
console.info("执行fn2")
return arg + 1;
}
function fn3(arg) {
console.info("执行fn3")
return arg + 1;
}
var newFn = compose(fn1, fn2, fn3)
newFn(1)
/**
执行fn3
执行fn2
执行fn1
4
*/
上面的newFn等同于(...args) => fn1(fn2(fn3(...args)))
4.dispatch增强
如果你熟悉redux-looger中间件,可以看到每次修改都会打印修改前后的值变化。那么中间件也要具备访问状态库的能力
const midapi = {
getState: store.getState,
dispatch: action => dispatch(action), // 这里执行的dispatch是增强后的dispatch,会在后面进行修改,完成增强
}
const chain = middlewares.map((middleware) => middleware(midapi))
这样每一个中间件就都可以通过getState访问当前状态库值,以及控制更新状态库的dispatch。同时中间件函数是下面这样
const middleware = ({ dispatch, getState }) {
return next => ...
}
然后就是让中间件函数依次执行(洋葱模型)得到一个增强后的dispatch
dispatch = compose(...chain)(store.dispatch)
然后将这个增强的dispatch返回,就完成了applyMiddleware函数的功能
applyMiddleware完整代码
function applyMiddleware(...middlewares) {
return createStore => reducer => {
const store = createStore(reducer);
let dispatch = store.dispatch;
const midapi = {
getState: store.getState,
dispatch: (action) => dispatch(action) // 这里执行的dispatch,就是下面compose赋值的dispatch,也就是增强的dispatch
}
// chain就是中间件函数的返回值数组,这个数组里每个函数都能访问到状态管理库
const chain = middlewares.map((middleware) => middleware(midapi))
dispatch = compose(...chain)(store.dispatch); // 实现dispatch的增强并返回
return {...store, dispatch};
}
}
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
// 这里的args指向的是 dispatch = compose(...chain)(store.dispatch) 传入的 store.dispatch
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
logger中间件源码
function looger({ dispatch, getState }) {
return next => action => {
const prevState = getState();
console.info("值更新前", prevState);
const nextValue = next(action);
const nextState = getState();
console.info("值更新后", nextState);
return nextValue;
}
}
- 上面logger函数接收的参数
{dispatch, getState}就是midapi - next:接收应该是store.dispatch。所以注意,logger要作为applyMiddleware
最后一个参数 - action:plain object
Redux-thunk中间件源码
function thunk({ dispatch, getState }) {
return next => action => {
if (typeof action === "function") {
return action(dispatch, getState)
}
return next(action);
}
}
- action判断
- action是函数,则执行这个action函数,并把dispatch、getState作为参数传递给这个action函数,执行dispatch的时机交给action函数
- action是object,则直接执行next,并把action传递给下一层(logger)
- 注意:action接收到的dispatch函数((action) =>
dispatch(action)),函数内执行的dispatch,已经不是store.dispatch。而是增强后的dispatch(dispatch = compose(...))
总结
以applyMiddleware(thunk, logger)为例再梳理一下applyMiddleware到底做了什么
- 1、执行
const chain = middlewares.map((middleware) => middleware(midapi)),等同于[thunk(midapi), thunk(midapi)] - 2、执行
dispatch = compose(...chain)(store.dispatch)时,等同于thunk(midapi)(logger(midapi)(原生dispatch)),所以logger的next是原生dispatch;而thunk的next是logger(midapi)(原生dispatch)。最终增强后的dispatch是一个接收action为参数的可执行函数(其实也就是thunk的action => ...) - 3、参考上文asyncAdd函数,执行
store.dispatch(() => ...),也就是执行增强后的dispatch,第一步进入thunk- action是函数:将增强dispatch、getState作为参数传入action函数,dispatch执行时机交给action函数
- action是object:直接执行next(action)
- 4、当asyncAdd的action函数执行dispatch后,再一次调用了增强dispatch,会重复第三次的判断,只是此时接收到的action是一个object。就会执行next(action),从第二步我们已经知道next是
logger(midapi)(原生dispatch),进入logger后,会执行logger的next(原生dispatch)实现修改store值
所以thunk会进入两次
如果对compose函数方式实现的中间件难以消化,可以参考好文推荐,用next = store.dispatch的方式来实现next的修改,实现组合调用,可以有助于你消化和理解dispatch变化的过程
React-Redux实现原理
上文中已经看到,如果我想要监听Redux数据变化的唯一方式是通过subscribe的方式。如何优雅的处理这样的监听,就是React-Redux要做的事。
使用规则
同样的,在梳理原理之前先看使用规则
// 1.Provider包裹
import { Provider } from "react-redux"
import store from "./store";
<Provider store={store}>
<App/>
</Provider>
// 2.使用connect消费
class App extends React.Component {
// ...
render() {
return <button onClick={this.props.add.bind(this, 1)}>{this.props.count}</button>;
}
}
function mapStateToProps(store) {
return {
count: store.count,
}
}
function mapDispatchToProps(dispatch) {
return {
add: (data) => dispatch({ type: "xx", payload: data })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
Provider
Provider其实就是一个组件,接收store并放入到context中供全局使用
import React, { createContext } from 'react'
const initialStoreState = {}
export const ProviderContext = createContext(initialStoreState)
export default class Provider extends React.Component {
constructor(props) {
super(props)
this.state = {
store: props.store
}
}
render() {
return (
<ProviderContext.Provider value={this.state}>
{this.props.children}
</ProviderContext.Provider>
)
}
}
connect
Provider是数据提供,那么connect就是消费了,通过它的使用规则connect(mapStateToProps, mapDispatchToProps)(App)可以推导出connect是一个函数,接收两个参数,返回一个HOC高阶函数
import { ProviderContext } from "./provider"
function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
return class Connect extends React.Component {
static contextType = ProviderContext;
componentDidMount() {
// store变化的时候更新。简单实现
this.context.store.subscribe(() => this.forceUpdate())
}
render() {
return (
<Component
// 传入该组件的props,需要由connect这个高阶组件原样传回原组件
{ ...this.props }
{ ...mapStateToProps(this.context.store.getState()) }
{ ...mapDispatchToProps(this.context.store.dispatch) }
/>
)
}
}
}
}
除了上面之外,还有一些hooks用法,比如useSelector、useDispatch等,都是通过context实现,原理一致
QA
执行dispatch后打印state(store.getState()),是修改前还是修改后的?
- 1.如果没有使用中间件的情况下,直接执行dispatch修改状态库值,打印的是修改后的
- 2.如果使用了如thunk中间件的情况下,store.dispatch是增强后diapatch
- 如果dispatch接收的是一个函数,那么答应的肯定是修改前的状态值。
- 如果接收的是一个object的action,则是修改后的状态值
写在最后
这篇文章是我的第 88 篇文章,翻阅了N多文档和文章,也是目前为止耗时最久、字数最多的一篇。函数式编程本身就是比较费解,强烈建议下载上面code在本地调试看下函数调用过程,啰里吧嗦也是希望能够把问题讲清楚,也在努力提高“讲故事”的能力