redux 作为 react 数据状态管理首选库,重要性不言而喻了,这里主要从源码的角度学习 redux 和 react-redux 工作原理,因为源码中用到了一些函数式编程概念,比如函数组合,因此该文章还有一些函数式编程知识。
实现 redux
redux 的主要内容有
- createStore api
- applyMiddleware && compose api
- 工具函数:combineReducers && bindActionCreators
实现 createStore
基本使用:传入一个纯函数 reducer 和 enhancer,数据存储就是用的闭包实现。函数本身十分简单,中间件增强可能会拐个弯,函数返回 getState,dispatch 和 subscribe 函数。
基本代码如下:
export function createStore(reducer, enhancer) {
// 如果有中间件,则直接调用中间件
if (enhancer) {
return enhancer(createStore)(reducer);
}
let currentState = {};
let currentListeners = [];
function getState() {
return currentState;
}
function subscribe(listener) {
currentListeners.push(listener);
}
function dispatch(action) {
currentState = reducer(currentState, action)
currentListeners.forEach(v => v());
return action;
}
// 命中default,发起初始化
dispatch({ type: '@@MINI/MINI-REDUX' })
return { getState, subscribe, dispatch };
}
实现 applyMiddleware 和 compose
中间件机制通常需要 getState
和 dispatch
函数,比如 thunk
组件就是增强 dispatch
能力,使得 dispatch
支持函数的能力
中间件是会存在很多个的,这里涉及到一个函数组合。代码如下
// compose(fn1,fn2,fn3) => (args) => fn1(fn2(fn3(args)))
export function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
// reduce 特别适合做累加/累乘/累计调用操作
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
实现 applyMiddleware 功能
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
// 构建 midApi
const midApi = {
getState: store.getState,
// 不能直接写成 store.dispatch,为了能在 middleware 中调用 compose 的最新的 dispatch(闭包),精彩
dispatch: (...args) => dispatch(...args)
}
// 绑定 midApi
const middlewareChain = middlewares.map(middleware => middleware(midApi));
// 绑定 next
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch
}
}
}
实现 thunk 中间件
thunk 的功能其实十分简单,因为 dispatch
默认是支持对象参数的,thunk 使得它支持传递函数的功能,源码其实十分简单
const thunk = ({ dispatch, getState }) => next => action => {
// 如果符合我们的要求,需要重新dispatch,调用dispatch即可
if (typeof action === 'function') {
// 这样函数中就可以拿到 dispatch 和 getState 啦
return action(dispatch, getState)
}
// 如果不符合我们的要求,直接调用下一个中间件,使用next
return next(action)
}
export default thunk;
实现 bindActionCreators
这是一个工具函数,如果不使用它,我们需要手动一个一个 dispatch
每一个 action
,它只是提供了批量处理的能力。因此源码其实也十分简单,简化版大致如下
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args));
}
export function bindActionCreators(creators, dispatch) {
return Object.keys(creators).reduce((ret, item) => {
ret[item] = bindActionCreator(creators[item], dispatch);
return ret;
}, {})
}
实现 react-redux
react-redux 的主要内容为
- 提供
Provider
组件 - 提供
connect
函数
有了 Redux 设计后,接下来的工作就是如何在 React 中正确工作,react-redux 这一部分我们可以基于 React Context api 轻松实现,以下是基于 React 的旧 Context api 实现,注意在 React 16 中该部分 api 已被重新设计。
Provider
组件的功能具体为
- 接收 store props,实现全局数据管理
- 使用 Context api 进行数据共享
具体代码如下
export class Provider extends React.Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context);
this.store = props.store;
}
render() {
return this.props.children
}
}
接下来就是 connect
实现了,其是一个典型的高阶函数设计,接受 mapStateToProps
和 mapDispatchToProps
用来得知需要从 store 中获取哪些数据,返回一个函数用来接受组件,最终返回组件,因此我们可以轻松得到一下原型设计
export function connect(mapStateToProps = state => state, mapDispatchToProps = {}) {
return function(WrapComponent) {
return class ConnectComponent extends React.Component {
}
}
}
接下来思考功能
- 使用 Context api 获取 store 数据
- 执行
mapStateToProps
和mapDispatchToProps
只传递需要的数据 - 订阅数据变化,实现组件重新更新
代码如下
export function connect(mapStateToProps = state => state, mapDispatchToProps = {}) {
return function(WrapComponent) {
return class ConnectComponent extends React.Component {
static contextTypes = {
store: PropTypes.object
}
constructor(props, context) {
super(props);
this.state = {
props: {}
};
}
componentDidMount() {
const { store } = this.context;
store.subscribe(() => this.update());
this.update();
}
update() {
const { store } = this.context;
const stateProps = mapStateToProps(store.getState());
// action 执行执行没有意义,需要 dispatch 才能更新 reducer 数据
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
this.setState({
props: {
...this.state.props,//本身的props
...stateProps,
...dispatchProps
}
})
}
render() {
// 将所有数据作为 props 透传
return <WrapComponent {...this.state.props}></WrapComponent>
}
}
}
}
函数式编程
imperative 和 declarative
- 指令式编程(imperative)关注告诉计算机如何一步步的做某件事情,这也是大多数人习惯的代码风格;
- 声明式编程(declarative)则关注每一步的输出,函数式编程则从本质上更具声明式特点。
- 声明式编程比指令式编程要容易理解。计算机擅长指令式,而我们擅长声明式。
如果我们合理的使用了函数式编程,我们的代码主体可能就会变成一个个「独立且拥有合适名称的纯函数」,若干「纯函数的组合」,「集中处理的副作用」。想要了解整体代码逻辑,只需去阅读函数组合在语意上做了什么事情,副作用做了什么,想要了解具体某个纯函数做了什么时,可以单独去查看其定义,而不用去关心其它的函数。
纯函数非常容易写测试,这就能保障我们大多数的代码不会容易出问题,如果真的出现了 bug ,bug 可能主要会出现在少数副作用相关的地方,能更容易被找到和修复。
关于函数的参数,你可能听说过两个词:Arguments 和 parameters:parameters 的个数可以通过 fn.length 获取,其在定义时决定,而arguments.length 属性则在运行时决定。
在 JS 中,return 返回的值也可以是一个函数,这其实是我们熟悉的高阶函数。实际上高阶函数可能算是函数式编程的基石之一了,函数式编程从某种意义上讲就是在不断的创建各种高阶函数。
函数式编程倾向于使用一元函数,但是实际上我们很难保证所用的函数都是一元函数,存在一些办法我帮我们进行元数的转换,有两种常见的方案,partial(偏函数)和 curry(柯里化)。
- 偏函数:partial(偏函数)是一种减少函数元数(arity)的方法,其核心是,它会创建一个新函数,这个新函数的一些参数是预设好的。
- curry(柯里化):柯里化则可以看作偏函数的一种特例,其元数会被减少到 1 ,其通过一系列连续的函数调用实现,每个函数都接受一个参数,一旦所有的参数被这些函数调用具体化了,原始函数会基于收集到的所有参数执行。
写好一个函数
- 函数的名称
- 减少副作用
- 纯函数
编程实际上离不开副作用,我们应该做的实际上是尽可能的减少副作用,我们应当确保副作用是我们故意添加的(而非无意的引入代码中的),而且添加的副作用应当尽可能的明显。
函数组合
- 狭义上讲,组合指的是将一个函数的输出立即传入另外一个函数当作输入
- 关于 Composition ,有很多工具函数,比如 compose 和 pipe
- compose: 函数从右向左执行;
- pipe: 函数从左向右执行;
递归
- 递归是函数式编程中常见的模式。相比循环而言,递归可能可读性更高。
- 递归需要满足以下两个条件:
- 基线条件(base case):函数不再调用自己的条件
- 递归条件(recursive case): 函数调用自己的条件
- 递归虽然易懂,但其最大的缺点在于耗内存。
- tail calls,可能大家听说最多的递归优化方案就是尾调用,不过尾调用需要系统,语言,编译器,运行环境等等都支持才行,目前只有 Safari 浏览器支持,需要在 strict 模式,且递归直接返回函数时才生效;
- CPS:一种欺骗性的突破栈内存限制的方法;
- trampolines:一种真实有效的节省内存的方式;
列表操作:常见的有 map,filter,reduce 三种
一些其它的概念
- transduction
- monad