Redux 源码分析
— SourceCode — 1 min read
Table of Contents
序
随着前端应用逐渐变大,状态也越来越复杂,需要一种状态管理的方案,Redux 就是其中一种
总结起来 React 状态管理方案就分为两类:
外部 Model,通过事件修改状态,组件通过监听订阅状态(通过 Context 连接 React)
Redux
Mobx
EventBus
hox
通过 React Context 提升状态(包括修改状态的方法),在组件中拿到并使用 Context
unstated
unstated-next
reunx
使用外部 Model 的模式由于不依赖于 React,所以不仅适用于 React,在各个框架甚至原生应用中都可以使用;而第二种依赖于 React Context 仅适用于 React 应用,但是十分轻量
MVC(backbone.js...)由于当应用变得很大时,会有多个 View 和多个 Model,(双向的)数据流就会变的非常混乱
所以出现了 Flux,以单向数据流管理状态,React 推崇的核心也是单向数据流,Flux 中单向数据流可以看作是在其整体架构上的延伸,Redux 是也不是 Flux,它遵循了 Flux 的单向数据流的理念,同时简化了 Flux
Flux/Redux 这种以单向数据流管理状态更像是一种设计模式
👀 Redux 对比 Mobx
Mobx 和 Redux 的目标都是管理好应用状态,但是最根本的区别在于对数据的处理方式不同。
Redux 认为,数据的一致性很重要,为了保持数据的一致性,要求Store 中的数据尽量范式化,也就是减少一切不必要的冗余,为了限制对数据的修改,要求 Store 中数据是不可改的(Immutable),只能通过 action 触发 reducer 来更新 Store。
Mobx 也认为数据的一致性很重要,但是它认为解决问题的根本方法不是让数据范式化,而是不要给机会让数据变得不一致。所以,Mobx 鼓励数据干脆就“反范式化”,有冗余没问题,只要所有数据之间保持联动,改了一处,对应依赖这处的数据自动更新,那就不会发生数据不一致的问题。
Redux 和 Mobx 都是一个特定时期的产物,不要因为某个工具或者技术炫酷或者热门而去用它,要根据自己的工作需要去选择工具和技术
📜 Redux 三大原则
单一数据源
状态是只读的
状态修改均有纯函数完成
相比 Flux 可以看出,Redux 没有 Dispatcher,Store 是单一的,Store 通过 Reducers 纯函数获取新的 State,使得状态的修改变得简单、纯粹、可测试,并且可以追踪每次变化,实现了时间旅行
🗜 middleware
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
它提供了一个分类处理 action 的机会。在 middleware 中,你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会
3.1 Redux 为什么需要 middleware
middleware 大部分都是用来处理异步,因为 Redux 内部是 state = reducer(state, action)
调用,所以 Redux 的 reducer 必须要是一个纯函数,不能有任何的副作用,以至 Redux 必须引入了 middleware,在 action 没有到 reducer 的时候就拦截 action 并进行副作用的操作
也正因为 reducer 是纯函数,我们通过 middleware 进行的异步操作和在一些大项目中对 action 的特殊需求的一些处理,才可以很好的进行组织管理
3.2 流程控制
由于 middleware 中的操作最终都需要 dispatch 一个 plain object,不同 middleware 中也需要 dispatch 进行跳转,所以导致 dispatch 处的逻辑难以抽象过程,除非传入 dispatch 参数,以至于 middleware 内部逻辑是一种通过 dispatch 的流程控制
🐦 高阶 reducer
higher-order-reducer:: reducer => reducer
高阶 reducer 就是指将 reducer 作为参数或者返回值的函数
combineReducers 其实就是一个高阶 reducer,因为 combineReducers 就是将一个 reducer 对象作为参数,最后返回顶层的 reducer
reducer 的复用:
我们两个组件,它们的功能是相同的,都是 LOAD_DATA,我们能不能让它们共用一个 reducer?
不能,因为当一个组件出发 action 时,另一个组件的状态也会改变
所以在一个应用中,不同模块间的 actionType 必须是全局唯一的
我们可以用增加前缀的方法解决:
1const generateReducer = (prefix, state) => {2 const LOAD_DATA = prefix + 'LOAD_DATA'3 const defaultStaet = {}45 return (state = defaultStaet, action) => {6 switch (action.type) {7 case LOAD_DATA:8 return {9 ...state,10 data: action.payload,11 }12 default:13 return state14 }15 }16}reducer 增强
redux-undo:
1const undoable = reducer => {2 const defaultState = {3 past: [],4 current: reducer(undefined, {}),5 future: [],6 }78 return (state = defaultState, action) => {9 const { past, current, future } = state1011 switch (action.type) {12 case '@@redux-undo/UNDO':13 const previous = past[past.length - 1]14 const newPast = past.slice(0, past.length - 1)1516 return {17 past: newPast,18 current: previous,19 future: [current, ...future],20 }21 case '@@redux-undo/REDO':22 const next = future[0]23 const newFuture = future.slice(1)2425 return {26 past: [...past, current],27 current: next,28 future: newFuture,29 }30 default:31 const newCurrent = reducer(current, action)3233 return {34 past: [...past, current],35 current: newCurrent,36 future: [],37 }38 }39 }40}
📝 源码分析
5.1 createStore
createStore 其实就把 reducer 包了一层,根本没有 store,只是暴露出几个接口,方便操作,并用发布订阅模式实现了 state 改变时执行订阅的函数的功能,然后在一开始实现 enhancer,供 applyMiddleware 使用
reducer、preloadedState/initialState 都得自己写,Redux 为我们干的很少
1const createStore = (reducer, preloadedState, enhancer) => {2 if (typeof enhancer !== undefined) {3 return enhancer(createStore)(reducer, preloadedState)4 }56 let currentReducer = reducer7 let state = preloadedState8 let listeners = []910 function getState() {11 return state12 }1314 function subscribe(listener) {15 // 利用闭包防止多次 unsubscribe16 let isSubscribed = true17 listeners.push(listener)1819 return function unsubscribe() {20 if (!isSubscribe) return2122 const index = listeners.indexOf(lintener)23 listeners.splice(index, 1)24 isSubscribe = false25 }26 }2728 function dispatch(action) {29 state = currentReducer(state, action) // 确定了 reducer 中不能做异步操作30 // 依次执行订阅31 listenters.forEach(listener => listener())3233 return action34 }3536 function replaceReducer(nextReducer) {37 currentReducer = nextReducer3839 dispatch({ type: Symbol('REPLACE') })40 }4142 // dispacth 一个没有的 action,仅用来第一次初始化 state43 dispatch({ type: Symbol('INIT') })4445 return {46 dispatch,47 subscribe,48 getState,49 replaceState,50 }51}
5.2 combineReducers
combineReducers 使我们更方便的组织 state,它干的就是将所有的 reducers 执行,使相应的 reducer 更新相应的 state,最终得到总共的新的 state
1const combineReducers = reducers => (state, action) => (2 Object.entries(reducers).reducer((nextState, [key, reducer]) => {3 const previousStateForKey = state[key]4 const nextStateForKey = reducer(previousStateForKey, action)56 return nextState[key] = nextStateForKey7 }, {})8)
5.3 compose
compose 就是典型的函数式编程中的组合函数,用于组合多个 enhancers 成一个 enhancer
1const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))
与本文无关,记录一个 async compose:asyncPipe
5.4 applyMiddleware
applyMiddleware 可以对特定类型的 action 进行操作
createStore 中的 dispatch 由于有对 action 是否为 plainObject 的检测,所以只能 dispatch plainObject,而中间件可以穿入其他类型的 action(redux-thunk...)
实现就是通过传入旧的 createStore 返回增强过的 store,从旧的 createStore 中提取出原来 dispatch,然后对原来的 dispatch 进行包裹(增强),检测特定类型的 action 并进行的一些操作,然后仍然创建一个 plainObject 的 action,使用原来的 dispatch 派发掉
1// 对应 enhancer(createStore)(reducer, preloadedState)2export default function applyMiddleware(...middlewares) {3 return createStore => (...args) => {4 const store = createStore(...args)5 let dispatch = () => {6 throw new Error(7 'Dispatching while constructing your middleware is not allowed. ' +8 'Other middleware would not be applied to this dispatch.'9 )10 }1112 // 这里一开始传入报错的 dispatch,防止在组合 middlewares 时 dispatch13 // 之后 dispacth 改变,根据作用域得到增强后的 dispatch14 const middlewareAPI = {15 getState: store.getState,16 dispatch: (...args) => dispatch(...args)17 }18 const chain = middlewares.map(middleware => middleware(middlewareAPI))19 // 组合所有 middleware(enhancer),把原来的 dispatch 传入,得到包裹(增强)的 dispatch20 // 把这行 compose 分开写就是 dispatch = thunk(logger(timer(store.dispatch)))21 dispatch = compose(...chain)(store.dispatch)2223 // 返回新的 store24 return {25 ...store,26 dispatch27 }28 }29}
简单的 middleware,在打印日志之前输出当前的时间戳:
1// 获取 dispatch 和 getState 方法,方便内部使用,返回接收 dispatch 的函数2// 这里 next 就对应了上一个中间件返回出来的 dispatch3// action 就是之后开发者写的要派发的 action4const timerMiddleware = ({ dispatch, getState }) => next => action => {5 console.log(`timer: ${new Date().getTime()}`)6 next(action)7}
对应 redux-thunk 源码:
1// 其实 thunk 就是 ({ dispatch, getState }) => next => action => ... 这个函数2// 再包一层是为了获取额外的参数,以供一些需求使用3function createThunkMiddleware(extraArgument) {4 return ({ dispatch, getState }) => next => action => {5 if (typeof action === 'function') {6 return action(dispatch, getState, extraArgument);7 }89 // action 不是 function 就用原来的 dispatch 派发掉10 return next(action);11 };12}1314const thunk = createThunkMiddleware();15thunk.withExtraArgument = createThunkMiddleware;1617export default thunk;
ref
深入 react 技术栈