[React] Redux 源码解读

in #react5 years ago

redux


redux

介绍

我们一起来研究下当前最流行的React状态管理容器Redux,本文基于Redux v3.7.2 ,Redux用一个单独的状态树对象(state tree object)保存整个应用的状态,这个对象不能直接被改变(immutable),当数据变化了,一个新的对象就会被创建(通过actions和reducers)

Redux有如下优点:

  • 可预测
    始终有一个唯一的准确的数据源(single source of truth),就是store,通过actions和reducers来保证整个应用状态同步,做到绝不混乱
  • 易维护
    具备可预测的结果和严格的组织结构让代码更容易维护
  • 易测试
    编写可测试代码的首要准则是编写可以仅做一件事并且独立的小函数(single responsibility principle),Redux的代码几乎全部都是这样的函数:短小·纯粹·分离

实例代码

举个小栗子🌰看看应用代码中如何使用Redux
例子取自Redux examples 里面的counter

import React           from 'react'
import ReactDOM        from 'react-dom'
import { createStore } from 'redux'
import Counter         from './components/Counter'
// import counter      from './reducers'
const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

const store = createStore(counter)
const rootEl = document.getElementById('root')

const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
    onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
  />,
  rootEl
)

render()
store.subscribe(render)

整体结构

screen shot 2018-02-17 at 12 48 20 pm

utils/warning.js

utils目录下面的warning.js负责控制台错误日志的输出,用于非生产环境下(process.env.NODE_ENV !== 'production')抛出错误便于debug:

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    // This error was thrown as a convenience so that if you enable
    // "break on all exceptions" in your console,
    // it would pause the execution at this line.
    throw new Error(message)
  } catch (e) { }
}

index.js

入口文件index.js主要用于引出公共方法API供外面调用,isCrushed那段用于非生产环境

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
    'This means that you are running a slower development build of Redux. ' +
    'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
    'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
    'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

createStore.js

createStore.js用于Store的生成,接受3个参数:

  • reducer函数,接受当前state tree和一个action,返回新的state tree
  • preloadedState,初始状态树,如果你使用combineReducers,必须确保初始state tree object的keys和combineReducers的keys保持一致
  • enhancer,用来强化store,比如middleware / time travel,Redux自带的唯一enhancer是applyMiddleware()

getState()

用来返回当前state tree

replaceReducer(nextReducer)

用来替换掉当前store用的reducer,可用于动态按需加载或者热替换等场景

subscribe(listener)

订阅函数,用来注册监听事件,并返回取消订阅的函数
每当dispatch一个action时注册的listener被调用,为实现实时性,监听函数listener加入到nextListeners数组中,而dispatch事件使用currentListeners数组

dispatch(action)

用来分发action修改state tree的唯一方式,action必须是plain object并带有type属性

  • 将当前state tree和action传入Reducer,返回的新的state tree用于更新当前state tree
  • 按顺序调用currentListeners数组中的listener
  • 返回action

observable()

用于与observable/reactive lib互操作,当dispatch一个action时,调用注册的observer的next方法

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

export const ActionTypes = {
  INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  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') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }
    let isSubscribed = true
    ensureCanMutateNextListeners()
    nextListeners.push(listener)
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }
      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?'
      )
    }
    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
  }

  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          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
      }
    }
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

bindActionCreators.js

将action creators转换成具有同名keys的对象,用dispatch把每个action creator包起来,这样就可以直接调用它们,将包好的action creator往下传到一个组件上,却不让这个组件觉察到Redux的存在,松耦合

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
      `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

combineReducers.js

用来合并Reducers,将多个Reducer函数构成的对象转换为单个Reducer函数,当应用较大时Reducers可以按照模块拆分(domain model && SRP),代码结构比较清晰,它会调用每个子Reducer,并收集结果组成一个单一的state tree object,其keys与传递的子Reducer的keys相对应
对每一个Reducer,其初始state不可以为undefined,但可以为null,对传入的action,如果action为undefined,应该返回初始state,如果是未知的action,应该返回当前state

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

compose.js

用来组合传入的多个函数,在中间件时会用到,最终结果是把各个函数从右至左串联起来,相当于:
compose(a,b) = (...args) => a(b(...args))

export default 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)))
}

applyMiddleware.js

用于强化Store
上面👆分析createStore.js我们知道,中间件可以作为createStore的第二个或者第三个参数传入,如果有中间件,执行结果相当于applyMiddleware(…middlewares)(createStore)(reducer, preloadedState)

const store = createStore(reducer,applyMiddleware(…middlewares))
// or
const store = createStore(reducer,{},applyMiddleware(…middlewares))

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
}

从下面👇的代码可以看到,相当于用compose把各个middleware串联起来,传入dispatch得到更新的dispatch
为了串联所有middleware,其接受三层参数,第一层是Store,第二层是下一个middleware,第三层才是action,代码如下:

export function middleware({ dispatch, getState }) {
  return next => action =>
     { return next(action) }
}
// or
export function createMiddleware() {
  return ({ dispatch, getState }) => next => action =>
    { return next(action) }
}

问:middlewareAPI中的dispatch为何用匿名函数包起来?
答:经过applyMiddleware()之后的dispatch是更新过的,包起来后,只要dispatch更新,middlewareAPI中的dispatch也会变化

举个栗子🌰,"臭名昭著"的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.withExtraArgument = createThunkMiddleware;
export default thunk;
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

GitHub repo for Redux

Sort:  

Hello @andy2046! This is a friendly reminder that you have 3000 Partiko Points unclaimed in your Partiko account!

Partiko is a fast and beautiful mobile app for Steem, and it’s the most popular Steem mobile app out there! Download Partiko using the link below and login using SteemConnect to claim your 3000 Partiko points! You can easily convert them into Steem token!

https://partiko.app/referral/partiko

Congratulations @andy2046! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.28
TRX 0.12
JST 0.032
BTC 61672.72
ETH 2996.85
USDT 1.00
SBD 3.78