手撸的乞丐版如下,仅实现了最基础的功能,订阅,派遣,获取三个功能,不过已经可以简单使用了,做个计数器是没啥问题,而且可以更简单粗暴的看到redux的核心功能
注意:该篇篇幅较长,建议收藏后慢慢看,也可以直接去github上拉下来自己试一试~《github传送门》
--------------------------------------------------------------正文开始-------------------------------------------------------------
简易版redux,真正的redux源码在文章最后面,源码较长,将近300行,刨去注释和兼容开发版不到100行,处理的场景比较多。我自己写的这个redux仅实现了它的核心功能,没有去管太多的兼容问题,但是代码少一点,大家对它的接受程度也会高一点,希望大家能够看懂。
const REDUX_INIT_TYPE = "@@REDUX_INIT_TYPE"
export function createStore(reducer, preloadedState, enhancer) {
/**
* 为什么要交换一下?
* 看名字,可以大概知道第二个参数preloadedState代表了预设的State,如果不传就是空的,
* 也就是state的默认值,这个在初始化的时候有用到,也就是dispatch中对应的第一次reducer
*
* 一般的reducer会有default选项,会返回默认的state,而且在定义reducer的时候,会给state传一个默认的值,也就导致了,如果preloadedState不传,就会默认读取用户自己设置的init_state
*
* 当传了preloadedState,但是没有传enhancer,且preloadedState是一个方法,那么说明用户可能传错了,那么,需要将第二个参数移到第三个参数上,并将preloadedState置为undefined
* 否则初始化state的时候,是无法正常初始化的(参数有默认值,但是接收到的不是undefined的时候,就不会自动设置为默认值!
*/
if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
enhancer = preloadedState;
preloadedState = undefined;
}
// 如果强化器是一个方法,那么直接返回强化器强化的createStore对reducer的执行操作,也就是包装一下
if (typeof enhancer === "function") {
return enhancer(createStore)(reducer);
}
let currentState = preloadedState;
const currentListenerList = [];
function getState() {
return currentState;
}
/**
* 派发操作,让reducer执行该操作对state进行更新
* 之后 依次调用所有的监听器
*/
function dispatch(action) {
currentState = reducer(currentState, action);
currentListenerList.forEach((listener) => listener());
return action;
}
// 订阅监听器
function subscribe(listener) {
currentListenerList.push(listener);
}
// 初始化currentState,因为该type在reducer中找不到对应的操作,所以走的默认值
dispatch({
type: REDUX_INIT_TYPE,
});
return {
getState,
dispatch,
subscribe,
};
}
写到这里,redux已经可以用了,不过只能用同步的,对于setTimeout那种是不支持的。
// 申请中间件
export function applyMiddleware(...middleWares) {
return (createStore) => (...args) => {
// 根据传进来的参数,创建一个新的store
let store = createStore(...args);
// 先把原来的dispatch保存下来,后面需要引用他
let oldDispatch = store.dispatch;
/**
* 把中间件都执行一下,然后返回一个新的数组,
* 中间件执行后都会返回一个函数,接受dispatch作为参数
* 如 thunk的源码
* function createThunkMiddleware(extraArgument) {
* return ({ dispatch, getState }) => next => action => {
* if (typeof action === 'function') {
* return action(dispatch, getState, extraArgument);
* }
* return next(action);
* };
* }
* export default createThunkMiddleware();
* 实际上返回的就是
* ({ dispatch, getState }) => next => action => {
* if (typeof action === 'function') {
* return action(dispatch, getState);
* }
* return next(action);
* };
* 这里面接受一个对象参数,{dispatch, getState}
* 返回一个函数,next为参数,这个next实际上就是一个dispatch,同样接受一个action作为参数,返回一个结果
* next => action => {
* if (typeof action === 'function') {
* return action(dispatch, getState);
* }
* return next(action);
* }
* 与之前写的dispatch做个对比
* function dispatch(action) {
* currentState = reducer(currentState, action);
* return action;
* }
* 基本造型是一样的,只不过这里面是对dispatch进行了一次强化,
* 如果dispatch接受的action是一个函数,那么就先执行一下这个函数,再返回结果,否则,就直接返回执行后的结果
* 这也就是最开始为什么dispatch要返回一个action的原因,用于将这个action再传给下一个中间件,也就是这一次执行的所有的中间件都是用的同一个action
*
* 每个中间件都是接受一个dispatch做参数,执行完之后,都是返回一个action(执行操作),当做下一个中间件的dispatch的action
*
* 所以先创建一个通用数据
* 这里的dispatch是最原始的dispatch
*/
const midData = {
getState: store.getState,
dispatch: (...args) => oldDispatch(...args)
}
let newMiddleWares = middleWares.map(mw => mw(midData));
// 然后利用middleWares对store的dispath进行强化
let newDispatch = compose(...newMiddleWares)(store.dispatch);
return {
...store,
dispatch: newDispatch
}
};
}
写完applyMiddleWare,就可以支持第三方中间件了,比如异步操作(setTimeout,ajax等),打印state数据等……
/**
* 顺序执行中间件,由[a,b] => a(b(...args));
* 这个方法简直是太酷了,不看源码我估计是不会想到用这个方法来做函数递归调用了。
* 还是对数组的方法不太熟悉啊!
*/
export function compose(...funcs) {
return funcs.reduce((prevFn, currentFn) => (...args) => currentFn(prevFn(...args)));
}
自己手写的logger中间件,真正的redux-logger很炫酷,它对console.log进行了一些美化,具体的美化可以跳转 redux-logger的core.js
我之前也写过一个console.log的简易用法指北,有兴趣的可以去看看 《传送门》
export const fake_logger = ({dispatch, getState}) => nextDispatch => action => {
console.log(action.type + "执行了!!!");
console.log(getState())
return nextDispatch(action);
}
export const fake_thunk = ({dispatch, getState}) => nextDispatch => action => {
if(typeof action === "function") {
return action(dispatch, getState)
}
return nextDispatch(action);
}
写到这里,redux部分已经完成了,在我自己写的例子中可以“完美”运行,开心一下,但是在react中使用起来很麻烦,每个组件都需要在state变化后手动去调用this.forceUpdate()对组件进行强制刷新,很不方便,所以才有了react-redux,react-redux就很好的支持了redux在react中的使用。下面开始搞react-redux。
先看看怎么使用的
// 父组件
const App = () => {
return (
<Provider store={store}>
<RootRouter />
</Provider>
);
}
// 子组件
const Wrapper = ({ counter }) => {
return (
<p>{counter.xxx}</p>
)
}
export default connect(({ counter }) => ({
counter
}), (dispath) => ({
add() {
dispath({
type: "add"
})
}
}))(Wrapper);
当我看到Provider的时候,我第一时间就想到了context,不熟悉的同学可以去看一下我之前写的关于context的文章《传送门》,然后我就按照自己的猜测去写了一版,代码如下
import React, { createContext, Component } from "react";
const { Provider, Consumer } = createContext(null);
export class SProvider extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.store.subscribe(() => {
this.forceUpdate();
});
}
render() {
/**
* 刚开始我是直接把this.props.store传给value的,但是会导致组件不刷新,我认为应该是react在做浅比较的时候,认为方法都是一样的,因为state是通过getState方法获取的
* 所以我把state和dispatch提出来,这样对比的时候,state就会变化了(事实证明也成功了。。)
* 我还没看react-redux的源码,,如果有错误,欢迎大家指出来,留言给我说,求轻喷T_T
*/
return <Provider value={{
state: this.props.store.getState(),
dispatch: this.props.store.dispatch
}}>{this.props.children}</Provider>;
}
}
/**
* connect是一个高阶函数,他对接收到的组件经行了一次二次封装,并将用户传入的参数都用redux执行一次,再以props的形式传给组件
* @params {function} getStateFn 接受state作为参数,返回一个用户自己定义的对象
* @params {function | object} dispatchs 有可能是一个对象,内部是key: action
* function接受dispatch作为参数,返回一个用户自己定义的对象
* 都会返回key: () => dispatch(action);
*/
export function connect(getStateFn, dispatchs) {
return (PropsComponent) => (props) => (
<Consumer>
{(store) => {
let newDispatchs = {};
// 如果传进来的是一个对象,那么需要对这个对象的每个元素用dispatch包装一下
if(typeof dispatchs === "object") {
Object.keys(dispatchs).forEach((item) => {
newDispatchs[item] = () => store.dispatch(dispatchs[item])
})
} else if(typeof dispatchs === "function") {
newDispatchs = dispatchs(store.dispatch);
}
return <PropsComponent {...getStateFn(store.state)} {...newDispatchs} {...props} />;
}}
</Consumer>
);
}
反正是可以运行了,待我后面再看看react-redux的源码,如果有出入,再来更改,我相信自己的判断!干吧得!冲鸭!
大家能看到这句话是真的不容易,可否帮我点个赞呀~十分感谢!!!
真正的源码开始,有对源码的注释的翻译,有兴趣的可以看一下。或者去github直接下载他的源码包《传送门》
import $$observable from 'symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
/**
* Creates a Redux store that holds the state tree.
* 创建一个状态管理的容器,来存储这个状态树
* The only way to change the data in the store is to call `dispatch()` on it.
* 只有一种方法去更新这个容器里的状态,就是调用dispatch去通知他
*
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several reducers
* into a single reducer function by using `combineReducers`.
*
* @param {Function} reducer A function that returns the next state tree, given
* the current state tree and the action to handle.
* 给定当前状态树和要处理的操作,返回下一个状态树的函数。
*
* @param {any} [preloadedState] The initial state. You may optionally specify it
* to hydrate the state from the server in universal apps, or to restore a
* previously serialized user session.
* If you use `combineReducers` to produce the root reducer function, this must be
* an object with the same shape as `combineReducers` keys.
*
* 初始状态。您可以选择在universal apps中指定它来从服务器获取状态,或者恢复以前序列化的用户会话。如果您使用‘combineReducers’来生成根还原函数,那么这个对象的形状必须与‘combineReducers’键相同。
*
* @param {Function} [enhancer] The store enhancer. You may optionally specify it
* to enhance the store with third-party capabilities such as middleware,
* time travel, persistence, etc. The only store enhancer that ships with Redux
* is `applyMiddleware()`.
* enhancer是增强器的意思
* 存储增强器。您可以有选择地指定它,以使用第三方功能(如中间件、时间旅行、持久性等)增强存储。Redux附带的惟一存储增强器是“applyMiddleware()”。
*
* @returns {Store} A Redux store that lets you read the state, dispatch actions
* and subscribe to changes.
*/
export default function createStore(reducer, preloadedState, enhancer) {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
// 看起来您正在传递几个存储增强器(store enhancers)给createStore()
// 这是不被支持的
// 把他们组合在一起,成为有一个简单的函数
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}
// 当第二个参数preloadedState是一个方法,且没有第三个参数,就把第二个参数和第三个参数交换值
/**
* 为什么要交换一下?
* 看名字,可以大概知道第二个参数preloadedState代表了预设的State,如果不传就是空的,
* 也就是state的默认值,这个在初始化的时候有用到,也就是dispatch中对应的第一次reducer
*
* 一般的reducer会有default选项,会返回默认的state,而且在定义reducer的时候,会给state传一个默认的值,也就导致了,如果preloadedState不传,就会默认读取用户自己设置的init_state
*
* 当传了preloadedState,但是没有传enhancer,且preloadedState是一个方法,那么说明用户可能传错了,那么,需要将第二个参数移到第三个参数上,并将preloadedState置为undefined
* 否则初始化state的时候,是无法正常初始化的(参数有默认值,但是接收到的不是undefined的时候,就不会自动设置为默认值!
*/
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
/**
* 当enhancer不是空的,结合上面的代码就可以得出,如果有后面的两个参数之一,就会执行当前语句
* 所以也就是说,如果第二个参数为方法,则默认就会变成第三个参数
* enhancer应该是一个高阶组件,利用中间件对createStore进行一次封装
**/
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
// 预料的reducer应该是一个函数
throw new Error('Expected the reducer to be a function.')
}
/**
* 如果preloadedState不是一个函数,而是一个值或者对象什么的,才会走到这里
**/
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
/**
* This makes a shallow copy of currentListeners so we can use
* nextListeners as a temporary list while dispatching.
* 这将创建一个currentListener的浅拷贝,因此我们可以在分派时将nextListener用作临时列表
* This prevents any bugs around consumers calling
* subscribe/unsubscribe in the middle of a dispatch.
*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
/**
* Reads the state tree managed by the store.
*
* @returns {any} The current state tree of your application.
*/
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
/**
* Adds a change listener. It will be called any time an action is dispatched,
* and some part of the state tree may potentially have changed. You may then
* call `getState()` to read the current state tree inside the callback.
* 添加更改侦听器。
* 在分派操作的任何时候都会调用它,并且状态树的某些部分可能已经更改。
* 然后可以调用' getState() '来读取回调函数中的当前状态树。
*
* You may call `dispatch()` from a change listener, with the following
* caveats:
*
* 1. The subscriptions are snapshotted just before every `dispatch()` call.
* If you subscribe or unsubscribe while the listeners are being invoked, this
* will not have any effect on the `dispatch()` that is currently in progress.
* However, the next `dispatch()` call, whether nested or not, will use a more
* recent snapshot of the subscription list.
*
* 2. The listener should not expect to see all state changes, as the state
* might have been updated multiple times during a nested `dispatch()` before
* the listener is called. It is, however, guaranteed that all subscribers
* registered before the `dispatch()` started will be called with the latest
* state by the time it exits.
*
* @param {Function} listener A callback to be invoked on every dispatch.
* @returns {Function} A function to remove this change listener.
*/
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
/**
* Dispatches an action. It is the only way to trigger a state change.
* 分派一个动作。这是触发状态更改的唯一方法。
*
* The `reducer` function, used to create the store, will be called with the
* current state tree and the given `action`. Its return value will
* be considered the **next** state of the tree, and the change listeners
* will be notified.
* 用于创建存储的'reduce'函数将被当前状态树和给定的'action'调用。
* 它的返回值将被认为是树的**next**状态,并且会通知更改侦听器。
*
* The base implementation only supports plain object actions. If you want to
* dispatch a Promise, an Observable, a thunk, or something else, you need to
* wrap your store creating function into the corresponding middleware. For
* example, see the documentation for the `redux-thunk` package. Even the
* middleware will eventually dispatch plain object actions using this method.
* 基本实现只支持普通对象操作。
* 如果您希望分派一个Promise、一个Observable、一个thunk或其他东西,您需要将store创建函数封装到相应的中间件中。
* 例如,请参阅“redux-thunk”包的文档。甚至中间件最终也会使用这种方法来调度普通对象操作。
*
* @param {Object} action A plain object representing “what changed”. It is
* a good idea to keep actions serializable so you can record and replay user
* sessions, or use the time travelling `redux-devtools`. An action must have
* a `type` property which may not be `undefined`. It is a good idea to use
* string constants for action types.
*
* @returns {Object} For convenience, the same action object you dispatched.
*
* Note that, if you use a custom middleware, it may wrap `dispatch()` to
* return something else (for example, a Promise you can await).
* 根据传入的action,调用currentReducer对currentState进行修改,并依次调用当前监听队列中的所有监听函数,返回传入的action
*/
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
/**
* Replaces the reducer currently used by the store to calculate the state.
* 替换存储区当前用于计算状态的reducer。
*
* You might need this if your app implements code splitting and you want to
* load some of the reducers dynamically. You might also need this if you
* implement a hot reloading mechanism for Redux.
* 如果您的应用程序实现了代码分割,并且您想动态加载一些reducer,那么您可能需要这个。如果您为Redux实现热重载机制,您可能还需要这个。
*
* @param {Function} nextReducer The reducer for the store to use instead.
* @returns {void}
*/
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
// This action has a similiar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE })
}
/**
* Interoperability point for observable/reactive libraries.
* 可观察/反应库的互操作性点。
*
* @returns {observable} A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
function observable() {
const outerSubscribe = subscribe
return {
/**
* The minimal observable subscription method.
* @param {Object} observer Any object that can be used as an observer.
* The observer object should have a `next` method.
* @returns {subscription} An object with an `unsubscribe` method that can
* be used to unsubscribe the observable from the store, and prevent further
* emission of values from the observable.
*/
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}