前言
相信学过 React
的朋友对 Redux
一定不陌生,作为 JavaScript
的状态管理容器,它不仅能够在 react
中使用,同时也能在原生 JS
项目或者Vue
项目中使用它。不过因为其设计理念与 react
相似,并且和 react
一起使用时会有较好的开发体验,以至于很多人一提到 redux
就会联想到 react
。
redux 的使用
- 创建
Action
js
// === redux/constant.js ===
// 该模块适用于定义action对象中type类型的常量值
// 目的只有一个:便于管理的同时防止单词写错
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
// === redux/action.js ===
// action 定义成函数,方便 dispatch 的使用,使用时直接 dispatch(addTodo(值))
// 如果定义为 const addTodo = 'ADD_TODO',则 dispatch({type: addTodo, count: 值})
import { INCREMENT, DECREMENT } from './constant'
// 同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })
// 异步action,就是指action的值为函数,异步action中一般都会调用同步action
export const createDecrementAsyncAction = (data, delay) => {
return (dispatch) => {
setTimeout(() => {
// 通知redux
dispatch(createIncrementAction(data))
}, delay)
}
}
- 创建
Reducer
js
// === redux/reducer.js ===
/**
state: 初始值
action: dispatch传过来的对象
1. 该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2. reducer 函数会接收到两个参数,分别为:之前状态(preState), 动作对象(action)
*/
import { INCREMENT, DECREMENT } from './constant'
const initState = 0
export default function countReducer (state = initState, action) {
// 从action对象中获取:type,data
const { type, data } = action
switch (type) {
case INCREMENT:
return state + data
case DECREMENT:
return state - data
default:
return state
}
}
- 创建
Store
用来连接action
和reducer
js
// === redux/store.js ===
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import reducer from './reducer'
// 引入 redux-thunk 用于支持异步action
import thunk from 'redux-thunk'
// 创建store对象并暴露
// 如果有多个reducer,可以使用 combineReducers 整合
// 例:const rootReducer = combineReducers({reducer1: xxx, reducer2: xxx})
export default createStore(reducer, applyMiddleware(thunk))
- 配合
react-redux
在react
中使用
注:redux
中提供了 subscribe
方法订阅 store
里的 state
变化,如果在 react
工程中不想使用 react-redux
,则可在入口文件 index.js
中使用 subscribe
方法来更新视图
js
// state 发生改变会自动调用回调函数,订阅
store.subscribe(() => {
// 渲染App到页面
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>, document.getElementById('root'))
})
react-redux
中提供了 connect
方法,用来连接视图(react组件)
和 redux
,使得 redux
中的 state
发生改变,能够及时通知视图的更新,由于我们平时大多数写的是函数式组件,则可以依赖 react-redux
中提供的 useSelector
和 useDispatch
两个api来操作redux
和使用 connect()
一样,你首先应该将整个应用包裹在 <Provider>
中,使得 store 暴露在整个组件树中。
js
import store from './redux/store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
在组件中使用
js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
createIncrementAction,
createDecrementAction,
createDecrementAsyncAction
} from '../redux/action'
function Counter() {
const dispatch = useDispatch()
// 如果有多个reducer,则回调函数中 state 就是根,根据对应的作用域去找对应的属性
// 例如:combineReducers({a: xxx, b: xxx}) => useSelector(state => state.a)
const state = useSelector(state => state)
return <>
<div>当前值:{state}</div>
<button onClick={() => {
dispatch(createIncrementAction(1))
}}>加1</button>
<button onClick={() => {
dispatch(createDecrementAction(2))
}}>减2</button>
<button onClick={() => {
dispatch(createDecrementAction(10))
}}>异步加10</button>
</>
}
Redux 源码解析
当你熟练使用 redux
以后,在没有看其源码之前,你是否会有以下一些疑问:
store
是如何将action
和reducer
串联在一起?为什么我们dispatch
一个action,它就会自动帮我们找到对应reducer
中 switch 语句里的 case?- 多个
reducer
是如何整合在一起的,它生成了一颗什么样的状态树? - redux 的
中间件
是怎么运作的? - 为什么配合
react-redux
使用,state的改变会引起视图的改变?
带着疑问,我们从 redux
的核心源码 createStore
讲起
createStore 做了什么?
creaeStore 是 reudx 使用的入口函数 , 源码位置:src/createStore.ts ,我们把 ts 类型标注去掉,稍作改造,实现一个简单的 createStore
并做讲解
js
function createStore(reducer, preloadedState, enhance) {
// reducer 必须是一个函数,有两参数 state 和 action
let currentReducer = reducer
// 初始状态值, currentState 就是状态树
let currentState = preloadedState
let currentListeners = []; // 当前订阅函数 => 源码使用 Map 构造
// 判断中间件
if (enhance !== undefined && typeof enhance === 'function') {
return enhance(createStore)(reducer, preloadedState)
}
// dispatch 方法的实现,默认action是个对象
function dispatch(action) {
currentState = currentReducer(currentState, action)
// state改变,执行订阅的函数
currentListeners.forEach((listener) => listener());
return action
}
function getState() {
return currentState
}
// 订阅和取消订阅必须要成对出现
function subscribe(listener) {
currentListeners.push(listener);
return () => {
const index = currentListeners.indexOf(listener);
currentListeners.splice(index, 1);
};
}
dispatch({ type: 'init' }) // 源码这里的type是字符串拼接随机数,保证不命中我们自己写的type
return {
dispatch,
getState,
subscribe
}
}
在调用 createStore
函数时,我们可以看到,函数内部执行了一次 dispatch
,并返回一个对象。此时调用dispatch
函数传入的参数是{type: 'init'}
我们以上面 redux 的使用
的代码为例
js
function dispatch(action) {
// currentState 的初始值是 preloadedState, action = {type: 'init'}
currentState = currentReducer(currentState, action)
// state改变,执行订阅的函数
currentListeners.forEach((listener) => listener());
// return currentState
}
// 此时的currentReducer就是, action = {type: 'init'}
function countReducer (state = initState, action) {
const { type, data } = action
switch (type) {
case INCREMENT:
return state + data
case DECREMENT:
return state - data
default:
return state
}
}
由于 ation.type
未命中,currentReducer返回的就是 countReducer中initState的值,即整个应用的状态就是 currentState = 0
这时候,如果我们手动 dispatch一个action的话
,他就会执行对应的 reducer
并把新的值赋值给 currentStae
,此时 redux
的状态就发生了改变,然后依次执行订阅列表中的函数通知给组件 。那么,当我们有多个 reducer
的时候,它又是怎么工作的呢?
combineReducers 做了什么?
combineReducers
的作用是将多个reducer(函数)整合成一个reducer(函数)
js
function combineReducers(reducers) {
// reducers 是一个对象,{reducer1: xxx, reducer2: xxx}
const reducerKeys = Object.keys(reducers)
return function combination(state = {}, action) {
let nextState = {}
for (let i = 0; i < reducerKeys.length; i++) {
const _key = reducerKeys[i]
const reducer = reducers[_key]
var previousStateForKey = state[_key];
const nextStateForKey = reducer(previousStateForKey, action)
nextState[_key] = nextStateForKey
}
return nextState
}
}
export default combineReducers
不难看出,当我们有多个 reducer
的时候,combineReducers
会返回一个函数,因为 createStore
的第一个参数必须是函数。 假设我们现在有 user
和 article
两个 reducer
js
function user(state, action) { ... }
function article(state, action) { ... }
// combineReducers 返回一个combination函数, rootReducer就是根reducer函数
const rootReducer = combineReducers({user: user, article})
export default createStore(rootReducer)
当createStore内部初始化第一次 dispatch
的时候
js
function dispatch(action) {
currentState = currentReducer(currentState, action)
}
// 此时的 currentReducer ,也就是 rootReducer 等价于
function combination(state = {}, action) {
// action = {type: 'init'}, state = preloadedState
let nextState = {}
// reducerKeys = ['user', 'article']
for (let i = 0; i < reducerKeys.length; i++) {
const _key = reducerKeys[i]
// reducers 闭包作用,值为 {user, article}
const reducer = reducers[_key]
var previousStateForKey = state[_key];
// 调用 user or aritcle 的reducer
const nextStateForKey = reducer(previousStateForKey, action)
nextState[_key] = nextStateForKey
}
return nextState
}
由上可见,存在多个 reducer
的时候,createStore
自身内部初始化调用 dispatch
后,会依次调用每一个子 reducer
,并返回一棵状态树({user: 值, article: 值}
),当我们手动调用一次dispatch
的时候,同样也会循环执行每一个 reducer
,去命中 action.type
, 并把新的状态树返回给 currentState
,也就是说,如果你传入的 action.type
的值同时命中多个 reducer 里switch的case
,则它们的状态将都发生变化。
redux 中间件的原理是什么?
中间件的使用
js
const store = createStore(rootReducer, applyMiddleware(xxx1, xxx2))
// 等价于
const store = applyMiddleware(xx1,xx2)(createStore)(rootReducer)
redux 中间件的出现意在增强 action 的能力,如果有多个中间件,用,
隔开,并从右
往左
依次执行
js
// 柯里化
function applyMiddleware(...middlewares) {
return function (createStore) {
return function (reducer, preloadedState) {
const store = createStore(reducer, preloadedState)
let _dispatch = () => {
console.log('内部dispatch, 啥也不干')
};
const midapi = {
getState: store.getState,
dispatch: (action) => {
console.log('_dispatch', _dispatch)
_dispatch(action)
}
}
const chain = middlewares.map(middleware => middleware(midapi))
_dispatch = compose(...chain)(store.dispatch)
console.log('=====_dispatch=====增强后的', _dispatch)
return { ...store, dispatch: _dispatch }
}
}
}
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)));
}
export default applyMiddleware
上面代码第一次看的时候是不是会有点懵?为什么这么多方法嵌套?还记得我们在创建createStore
里有这么一行代码
js
function createStore(reducer, preloadedState, enhance) {
// ...
if (enhance !== undefined && typeof enhance === 'function') {
// enhance => applyMiddleware(xxx1,xx2)
return enhance(createStore)(reducer, preloadedState)
}
// ...
}
当我们使用中间件的时候,createStore 会将自身传入给applyMiddleware,最终返回一个函数, 这时就可以认为原本的 createStore
发生了变化,只是在原本的基础上又包装了一层,换个形式而已
js
// 包装后的createStore
function creaeStore(reducer, preloadedState) {
// _createStore 是之前的引用,这里就不在复述
const store = _createStore(reducer, preloadedState)
let _dispatch = () => {
console.log('内部dispatch, 啥也不干')
};
const midapi = {
getState: store.getState,
dispatch: (action) => {
// 增强后的dispatch,刚开始是啥也不干,
_dispatch(action)
}
}
const chain = middlewares.map(middleware => middleware(midapi))
// 一顿操作后,这里把给_dispatch重新赋值,即增强后的dispath,
// 利用引用类型关系,midapi里的dispatch函数里的_dispatch也会随之改变
_dispatch = compose(...chain)(store.dispatch)
console.log('=====_dispatch=====增强后的', _dispatch)
return { ...store, dispatch: _dispatch }
}
写一个简单的中间件
上面的代码或许你看的有点不明白,我们写一个简单的中间件来结合讲解
js
function createThunkMiddleware() {
return function middleware({ dispatch, getState }) {
return (next) => (action) => {
/**
* dispatch 增强后的dispatch
* next 上一个中间件返回的dispatch, 源头是store.dispatch
*
*/
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
}
}
const thunk = createThunkMiddleware()
export default thunk
const chain = middlewares.map(middleware => middleware(midapi))
会依次调用中间件(函数)返回一个个 middleware
函数,此时 chain
的值为
js
chain = [
(action) => {
if (typeof action === 'function') {
// 闭包效果,midapi 中增强后的dispatch,thunk中间件提供我们手动dispatch的时候可以是一个函数,即这个action是一个函数
return action(dispatch, getState, extraArgument)
}
// next 上一个中间件返回的dispatch
return next(action)
}
]
}
至于为何中间件是从右
到左
执行的,那就归功于 compose
这个函数了
js
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)));
}
// 看不懂?解析一下
f1 = () => {}
f2 => () => {}
f3 => () => {}
f = null
function compose(funcs = [f1, f2, f3]) {
funcs.reduce((a, b) => {
return (...args) => a(b(...args))
// 第一次循环 a = f1, b = f2 , return (...args) => f1(f2(...args))
// 第二次循环 a = (...args) => f1(f2(...args)) 上一次return的, b = f3
// 则 return (...args) => a(b(...args)) 就等价于
// a = function(...args) {
// return f1(f2(...args))
// }
// 调用函数a(b),把b作为参数传入args就是函数b,此时是f3
// 也就是说
// a(b(...args)) => f1(f2(b(...arg))) => f1(f2((f3(...args))))
// ... 以此类推
}
)
}
compose
最终返回一个 (args) => {...}的函数,再次调用传入原始 store.dispatch
,则得到经过每一个中间件依次增强后的 dispatch
总结
redux
的源码还是相对比较简单的,以上只是写了一些核心代码块,官方源码仅仅了了几百行,至于 react-redux
的源码也通俗易懂,这里暂不赘述。