前言
前几天写了一篇 react 另一个状态管理工具 Unstated 的源码解析.
开启了我的看源码之路. 想一想用了好长时间的 redux, 但从没有深究过原理, 遇到报错更是懵逼, 所以就啃了一遍它的源码, 写了这篇文章,
分享我对于它的理解.
API 概览
看一下 redux 源码的 index.JS, 看到了我们最常用的几个 API:
- createStore
- combineReducers
- bindActionCreators
- applyMiddleware
- compose
不着急分析, 我们先看一下 Redux 的基本用法:
- import React from 'react'
- import ReactDOM from 'react-dom'
- import { createStore } from 'redux'
- const root = document.getElementById('root')
- // reducer 纯函数
- const reducer = (state = 0, action) => {
- switch (action.type) {
- case 'INCREMENT':
- return state + 1
- case 'DECREMENT':
- return state - 1
- default:
- return state
- }
- }
- // 创建一个 store
- const store = createStore(reducer)
- const render = () => ReactDOM.render(
- <div>
- <span>{store.getState()}</span>
- <button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button>
- <button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button>
- </div>,
- root
- )
- render()
- // store 订阅一个更新函数, 待 dispatch 之后, 执行这个更新函数, 获取新的值
- store.subscribe(render)
这里实现的是一个点击按钮加减数字的效果, 点击触发的行为, 与展示在页面上的数字变化, 都是通过 redux 进行的. 我们通过这个例子来分析一下 redux 是怎么工作的:
使用 reducer 创建一个 store, 便于我们通过 store 来与 redux 沟通
页面上通过 store.getState()拿到了当前的数字, 初始值为 0(在 reducer 中)
store.subscribe(render), 订阅更新页面的函数, 在 reducer 返回新的值时, 调用.(实际 subscribe 会把函数推入 listeners 数组, 在之后循环调用)
点击按钮, 告诉 redux, 我是要增加还是减少(调用 dispatch, 传入 action)
调用 dispatch 之后, dispatch 函数内部会调用我们定义的 reducer, 结合当前的 state, 和 action, 返回新的 state
返回新的 state 之后, 调用 subscribe 订阅的更新函数, 更新页面
目前为止, 我们所有的操作都是通过 store 进行的, 而 store 是通过 createStore 创建的, 那么我们来看一下它内部的逻辑
createStore
createStore 总共接收三个参数: reducer, preloadedState, enhancer,
reducer: 一个纯函数, 接收上一个(或初始的)state, 和 action, 根据 action 的 type 返回新的 state
preloadedState: 一个初始化的 state, 可以设置 store 中的默认值,
enhancer: 增强器, 用来扩展 store 的功能
暴露给我们几个常用的 API:
dispatch: 接收一个 action, 是一个 object{type:'a_action_type'}作为参数, 之后其内部会调用 reducer, 根据这个 action, 和当前 state, 返回新的 state.
subscribe: 订阅一个更新页面的函数, 放进 linsteners 数组, 用于在 reducer 返回新的状态的时候被调用, 更新页面.
getState: 获取 store 中的状态
我们先通过接收的参数和暴露出来的 API 梳理一下它的机制:
首先是接收上面提到的三个参数创建一个 store,store 是存储应用所有状态的地方. 同时暴露出三个方法, UI 可以通过 store.getState()获取到 store 中的数据,
store.subscribe(), 作用是让 store 订阅一个更新 UI 的函数, 将这个函数 push 到 listeners 数组中, 等待执行.
store.dispatch()是更新 store 中数据的唯一方法, dispatch 被调用后, 首先会调用 reducer, 根据当前的 state 和 action 返回新的状态. 然后循环调用 listeners 中的更新函数,
更新函数一般是我们 UI 的渲染函数, 函数内部会调用 store.getState()来获取数据, 所以页面会更新.
看一下 createStore 函数的结构
- createStore(reducer, preloadedState, enhancer) {
- // 转换参数
- if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
- enhancer = preloadedState
- preloadedState = undefined
- }
- function getState() {
- // 返回当前的 state, 可以调用 store.getState()获取到 store 中的数据,
- ...
- }
- function subscribe(listener) {
- // 订阅一个更新函数(listener), 实际上的订阅操作就是把 listener 放入一个 listeners 数组
- // 然后再取消订阅, 将更新函数从 listeners 数组内删除
- // 但是注意, 这两个操作都是在 dispatch 不执行时候进行的. 因为 dispatch 执行时候会循环执行更新函数, 要保证 listeners 数组在这时候不能被改变
- ...
- }
- function dispatch(action) {
- // 接收 action, 调用 reducer 根据 action 和当前的 state, 返回一个新的 state
- // 循环调用 listeners 数组, 执行更新函数, 函数内部会通过 store.getState()获取 state, 此时的 state 为最新的 state, 完成页面的更新
- ...
- }
- return {
- dispatch,
- subscribe,
- getState,
- }
- }
结构就是这样, 但是是如何串联起来的呢? 下面来看一下完整的代码(删除了一些)
- createStore(reducer, preloadedState, enhancer) {
- if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
- // 有了这一层判断, 我们就可以这样传: createStore(reducer, initialState, enhancer)
- // 或者这样: createStore(reducer, enhancer), 其中 enhancer 还会是 enhancer.
- enhancer = preloadedState
- preloadedState = undefined
- }
- if (typeof enhancer !== 'undefined') {
- if (typeof enhancer !== 'function') {
- throw new Error('Expected the enhancer to be a function.')
- }
- // enhancer 的作用是扩展 store, 所以传入 createStore 来改造,
- // 再传入 reducer, preloadedState 生成改造后的 store, 这一有一点递归调用的意思
- return enhancer(createStore)(reducer, preloadedState)
- }
- if (typeof reducer !== 'function') {
- throw new Error('Expected the reducer to be a function.')
- }
- let currentReducer = reducer // 当前的 reducer, 还会有新的 reducer
- let currentState = preloadedState // 当前的 state
- let currentListeners = [] // 存储更新函数的数组
- let nextListeners = currentListeners // 下次 dispatch 将会触发的更新函数数组
- let isDispatching = false // 类似一把锁, 如果正在 dispatch action, 那么就做一些限制
- // 这个函数的作用是判断 nextListeners 和 currentListeners 是否是同一个引用, 是的话就拷贝一份, 避免修改各自相互影响
- function ensureCanMutateNextListeners() {
- if (nextListeners === currentListeners) {
- nextListeners = currentListeners.slice()
- }
- }
- function getState() {
- // 正在执行 reducer 的时候, 是不能获取 state 的, 要等到 reducer 执行完, 返回新的 state 才可以获取
- 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
- }
- function subscribe(listener) {
- if (typeof listener !== 'function') {
- throw new Error('Expected the listener to be a function.')
- }
- // 由于 dispatch 函数会在 reducer 执行完毕后循环执行 listeners 数组内订阅的更新函数, 所以要保证这个时候的 listeners 数组
- // 不变, 既不能添加 (subscribe) 更新函数也不能删除 (unsubscribe) 更新函数
- 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#subscribe(listener) for more details.'
- )
- }
- let isSubscribed = true
- ensureCanMutateNextListeners()
- // 将更新函数推入到 listeners 数组, 实现订阅
- 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#subscribe(listener) for more details.'
- )
- }
- isSubscribed = false
- ensureCanMutateNextListeners()
- const index = nextListeners.indexOf(listener)
- // 取消订阅
- nextListeners.splice(index, 1)
- }
- }
- 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?'
- )
- }
- // 正在 dispatch 的话不能再次 dispatch, 也就是说不可以同时 dispatch 两个 action
- if (isDispatching) {
- throw new Error('Reducers may not dispatch actions.')
- }
- try {
- isDispatching = true
- // 获取到当前的 state
- currentState = currentReducer(currentState, action)
- } finally {
- isDispatching = false
- }
- const listeners = (currentListeners = nextListeners)
- // 循环执行当前的 linstener
- for (let i = 0; i <listeners.length; i++) {
- const listener = listeners[i]
- listener()
- }
- return action
- }
- // dispatch 一个初始的 action, 作用是不命中你 reducer 中写的任何关于 action 的判断, 直接返回初始的 state
- dispatch({ type: ActionTypes.INIT })
- return {
- dispatch,
- subscribe,
- getState,
- // observable replaceReducer 和 $$observable 主要面向库开发者, 这里先不做解析
- // replaceReducer,
- // [$$observable]:
- }
- }
- combineReducers
combineReducers 用于将多个 reducer 合并为一个总的 reducer, 所以可以猜出来,
它最终返回的一定是一个函数, 并且形式就是一般的 reducer 的形式, 接收 state 和 action,
返回状态:
- function combine(state, action) {
- ......
- return state
- }
来看一下核心代码:
- export default function combineReducers(reducers) {
- // 获取到所有 reducer 的名字, 组成数组
- const reducerKeys = Object.keys(reducers)
- // 这个 finalReducers 是最终的有效的 reducers
- const finalReducers = {}
- // 以 reducer 名为 key,reducer 处理函数为 key, 生成 finalReducers 对象, 形式如下
- /* {
- * reducerName1: f,
- * reducerName2: f
- * }
- */
- for (let i = 0; i < reducerKeys.length; i++) {
- const key = reducerKeys[i]
- if (process.env.NODE_ENV !== 'production') {
- if (typeof reducers[key] === 'undefined') {
- warning(`No reducer provided for key "${key}"`)
- }
- }
- if (typeof reducers[key] === 'function') {
- finalReducers[key] = reducers[key]
- }
- }
- const finalReducerKeys = Object.keys(finalReducers)
- let unexpectedKeyCache
- if (process.env.NODE_ENV !== 'production') {
- unexpectedKeyCache = {}
- }
- let shapeAssertionError
- // assertReducerShape 用来检查这每个 reducer 有没有默认返回的 state,
- // 我们在写 reducer 时候, 都是要在 switch 中加一个 default 的, 来默认返回初始状态
- try {
- assertReducerShape(finalReducers)
- } catch (e) {
- shapeAssertionError = e
- }
- // 这个函数, 就是上边说的返回的最后的那个终极 reducer, 传入 createStore,
- // 然后在 dispatch 中调用, 也就是 currentReducer
- // 这个函数的核心是根据 finalReducer 中存储的所有 reducer 信息, 循环, 获取到每个 reducer 对应的 state,
- // 并依据当前 dispatch 的 action, 一起传入当前循环到的 reducer, 生成新的 state, 最终, 将所有新生成的
- // state 作为值, 各自的 reducerName 为键, 生成最终的 state, 就是我们在 reduxDevTool 中看到的 state 树, 形式如下:
- /* {
- * reducerName1: {
- * key: 'value'
- * },
- * reducerName2: {
- * key: 'value'
- * },
- * }
- */
- return function combination(state = {}, action) {
- if (shapeAssertionError) {
- throw shapeAssertionError
- }
- if (process.env.NODE_ENV !== 'production') {
- const warningMessage = getUnexpectedStateShapeWarningMessage(
- state,
- finalReducers,
- action,
- unexpectedKeyCache
- )
- if (warningMessage) {
- warning(warningMessage)
- }
- }
- let hasChanged = false
- // 存放最终的所有的 state
- const nextState = {}
- for (let i = 0; i < finalReducerKeys.length; i++) {
- // 获取每个 reducer 的名字
- const key = finalReducerKeys[i]
- // 获取每个 reducer
- const reducer = finalReducers[key]
- // 获取每个 reducer 的旧状态
- const previousStateForKey = state[key]
- // 调用该 reducer, 根据这个 reducer 的旧状态, 和当前 action 来生成新的 state
- const nextStateForKey = reducer(previousStateForKey, action)
- // 以各自的 reducerName 为键, 新生成的 state 作为值, 生成最终的 state object,
- nextState[key] = nextStateForKey
- // 判断所有的 state 变化没变化
- hasChanged = hasChanged || nextStateForKey !== previousStateForKey
- }
- // 变化了, 返回新的 state, 否则, 返回旧的 state
- return hasChanged ? nextState : state
- }
- }
- applyMiddleware
redux 原本的 dispatch 方法只能接受一个对象作为 action
用户操作 -> dispatch(action) -> reducer(prevState, action) -> 新的 state -> 界面
这么直接干脆的操作固然好, 可以让每一步的操作可追踪, 方便定位问题, 但是带来一个坏处, 比如, 页面需要发请求获取数据, 并且把数据放到 action 里面,
最终通过 reducer 的处理, 放到 store 中. 这时, 如何做呢?
用户操作 -> dispatch(action) -> middleware(action) -> 真正的 action -> reducer(prevState, action) -> 新的 state -> 界面
重点在于 dispatch(action) -> middleware(action) 这个操作, 这里的 action 可以是一个函数, 在函数内我们就可以进行很多操作, 包括调用 API,
然后在调用 API 成功后, 再 dispatch 真正的 action. 想要这么做, 那就是需要扩展 redux(改造 dispatch 方法), 也就是使用增强器: enhancer:
- const store = createStore(rootReducer,
- applyMiddleware(thunk),
- )
applyMiddleware(thunk)就相当于一个 enhancer, 它负责扩展 redux, 说白了就是扩展 store 的 dispatch 方法.
既然要改造 store, 那么就得把 store 作为参数传递进这个 enhancer 中, 再吐出一个改造好的 store. 吐出来的这个 store 的 dispatch 方法, 是 enhancer 改造 store 的最终实现目标.
回顾一下 createStore 中的这部分:
- if (typeof enhancer !== 'undefined') {
- if (typeof enhancer !== 'function') {
- throw new Error('Expected the enhancer to be a function.')
- }
- // 把 createStore 传递进 enhancer
- return enhancer(createStore)(reducer, preloadedState)
- }
看下上边的代码, 首先判断 enhancer, 也就是 createStore 的第三个参数不为 undefined 且为函数的时候, 那么去执行这个 enhancer.
我们看到 enhancer(createStore), 是把 createStore 传入, 进行改造, 先不管这个函数返回啥, 我们先看它执行完之后还需要的参数
(reducer, preloadedState), 是不是有点眼熟呢? 回想一下 createStore 的调用方法, createStore(reducer, state).
由此可知 enhancer(createStore)返回的是一个新的 createStore, 而这个 createStore 是被改造过后的, 它内部的 dispatch 方法已经不是原来的了. 至此, 达到了改造 store 的效果.
那到底是如何改造的呢? 先不着急, 我们不妨先看一个现成的中间件 redux-thunk. 要了解 redux 中间件的机制, 必须要理解中间件是怎么运行的.
我们先来看用不用它有什么区别:
一般情况下, dispatch 的 action 是一个纯对象
- store.dispatch({
- type:'EXPMALE_TYPE',
- payload: {
- name:'123',
- }
- })
使用了 thunk 之后, action 可以是函数的形式
- function loadData() {
- return (dispatch, getState) => { // 函数之内会真正 dispatch action
- callApi('/url').then(res => {
- dispatch({
- type:'LOAD_SUCCESS',
- data: res.data
- })
- })
- }
- }
- store.dispatch(loadData()) // 派发一个函数
一般情况下, dispatch 一个函数会直接报错的, 因为 createStore 中的 dispatch 方法内部判断了 action 的类型. redux-thunk 帮我们做的事就是改造 dispatch, 让它可以 dispatch 一个函数.
看一下 redux-thunk 的核心代码:
- function createThunkMiddleware(extraArgument) {
- return ({ dispatch, getState }) => next => action => {
- if (typeof action === 'function') {
- return action(dispatch, getState, extraArgument);
- }
- return next(action);
- };
- }
- const thunk = createThunkMiddleware();
这里的三个箭头函数是函数的柯里化.
真正调用的时候, 理论上是这样 thunk({ dispatch, getState })(next)(action).
其中, thunk({ dispatch, getState})(next)这部分, 看它执行时接收的参数是一个 action, 那么它必然是一个 dispatch 方法, 在此处相当于改造过后的 dispatch, 而这部分会在 applyMiddleware 中去调用,(下边会讲到)
然后从左往右看,{ dispatch, getState }是当前 store 的 dispatch 和 getState 方法, 是最原始的, 便于在经过中间件处理之后, 可以拿到最原始的 dispatch 去派发真正的 action.
next 则是被当前中间件改造之前的 dispatch. 注意这个 next, 他与前边的 dispatch 并不一样, next 是被 thunk 改造之前的 dispatch, 也就是说有可能是最原始的 dispatch, 也有可能是被其他中间件改造过的 dispatch.
为了更好理解, 还是翻译成普通函数嵌套加注释吧
- function createThunkMiddleware(extraArgument) {
- return function({ dispatch, getState }) { // 真正的中间件函数, 内部的改造 dispatch 的函数是精髓
- return function(next) { // 改造 dispatch 的函数, 这里的 next 是外部传进来的 dispatch, 可能是被其他中间件处理过的, 也可能是最原本的
- return function(action) { // 这个函数就是改造过后的 dispatch 函数
- if (typeof action === 'function') {
- // 如果 action 是函数, 那么执行它, 并且将 store 的 dispatch 和 getState 传入, 便于我们 dispatch 的函数内部逻辑执行完之后 dispatch 真正的 action,
- // 如上边示例的请求成功后, dispatch 的部分
- return action(dispatch, getState, extraArgument);
- }
- // 否则说明是个普通的 action, 直接 dispatch
- return next(action);
- }
- }
- }
- }
- const thunk = createThunkMiddleware();
总结一下: 说白了, redux-thunk 的作用就是判断 action 是不是一个函数, 是就去执行它, 不是就用那个可能被别的中间件改造过的, 也可能是最原始的 dispatch(next)去派发这个 action.
那么接下来看一下 applyMiddleware 的源码:
- export default function applyMiddleware(...middlewares) {
- return createStore => (...args) => {
- const store = createStore(...args)
- let dispatch = () => {
- throw new Error(
- 'Dispatching while constructing your middleware is not allowed.' +
- 'Other middleware would not be applied to this dispatch.'
- )
- }
- const middlewareAPI = {
- getState: store.getState,
- dispatch: (...args) => dispatch(...args)
- }
- const chain = middlewares.map(middleware => {
- // 假设我们只是用了 redux-thunk, 那么此时的 middleware 就相当于 thunk, 可以往上看一下 thunk 返回的函数,
- // 就是这个: function({ dispatch, getState }), 就会明白了
- return middleware(middlewareAPI)
- })
- // 这里的 compose 函数的作用就是, 将所有的中间件函数串联起来, 中间件 1 结束, 作为参数传入中间件 2, 被它处理,
- // 以此类推最终返回的是被所有中间件处理完的函数, 最开始接收 store.dispatch 为参数, 层层改造后被赋值到新的 dispatch 变量中
- dispatch = compose(...chain)(store.dispatch)
- return {
- ...store,
- dispatch
- }
- }
- }
先看最简单的情况: 假设我们只使用了一个 middleware(redux-thunk), 就可以暂时抛开 compose, 那么这里的逻辑就相当于
dispatch = thunk(middlewareAPI)(store.dispatch)
是不是有点熟悉? 在 redux-thunk 源码中我们分析过:
真正调用 thunk 的时候, thunk({ dispatch, getState })(next)(action)
其中, thunk({ dispatch, getState })(next)这部分, 相当于改造过后的 dispatch, 而这部分会在 applyMiddleware 中去调用
所以, 这里就将 store 的 dispatch 方法改造完成了, 最后用改造好的 dispatch 覆盖原来 store 中的 dispatch.
来总结一下,
中间件和 redux 的 applyMiddleware 的关系. 中间件 (middleware) 会帮我们改造原来 store 的 dispatch 方法
而 applyMiddleware 会将改造好的 dispatch 方法应用到 store 上(相当于将原来的 dispatch 替换为改造好的 dispatch)
理解中间件的原理是理解 applyMiddleware 机制的前提
另外说一下, 关于 redux-thunk 的一个参数: extraArgument 这个参数不是特别重要的, 一般是传入一个实例, 然后在我们需要在真正 dispatch 的时候需要这个参数的时候可以获取到, 比如传入一个 axios 的 Instance, 那么在请求时候就可以直接用这个 instance 去请求了
- import axiosInstance from '../request'
- const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument(axiosInstance)))
- function loadData() {
- return (dispatch, getState, instance) => {
- instance.get('/url').then(res => {
- dispatch({
- type:'LOAD_SUCCESS',
- data: res.data
- })
- })
- }
- }
- store.dispatch(loadData())
总结
到这里, redux 几个比较核心的概念就讲解完了, 不得不说写的真简洁, 函数之间的依赖关系让我一度十分懵逼, 要理解它还是要用源码来跑一遍例子,
一遍一遍地看.
总结一下 redux 就是创建一个 store 来管理所有状态, 触发 action 来改变 store. 关于 redux 的使用场景是非常灵活的, 可以结合各种库去用, 我用惯了 react, 用的时候还要配合 react-redux.
来源: https://segmentfault.com/a/1190000019232219