Redux中的编程艺术

Redux源码分析已经满大街都是了。可是大多都是介绍如何实现,实现原理。而忽略了Redux代码中隐藏的知识点和艺术。为何称之为艺术,是这些简短的代码蕴含着太多前端同窗应该掌握的JS知识以及巧妙的设计模式的运用。前端

createStore 不只仅是一个API

...
export default function createStore(reducer, preloadedState, enhancer) {
  ...
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    ...
  }

  function getState() {
    ...
    return currentState
  }

  function subscribe(listener) {
    ...
  }

  function dispatch(action) {
    ...
    return action
  }

  function replaceReducer(nextReducer) {
    ...
  }

  function observable() {
    ...
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
复制代码

这段代码,蕴含着不少知识。react

首先是经过闭包对内部变量进行了私有化,外部是没法访问闭包内的变量。其次是对外暴露了接口来提供外部对内部属性的访问。这实际上是典型的“沙盒模式”。面试

沙盒模式帮咱们保护内部数据的安全性,在沙盒模式下,咱们只能经过return出来的开放接口才能对沙盒内部的数据进行访问和操做。redux

虽然属性被保护在沙盒中,可是因为JS语言的特性,咱们没法彻底避免用户经过引用去修改属性。设计模式

subscribe/dispatch 订阅发布模式

subscribe 订阅

Redux经过subscribe接口注册订阅函数,并将这些用户提供的订阅函数添加到闭包中的nextListeners中。数组

最巧妙的是考虑到了会有一部分开发者会有取消订阅函数的需求,并提供了取消订阅的接口。安全

这个接口的'艺术'并不只仅是实现一个订阅模式,还有做者严谨的代码风格。前端工程师

if (typeof listener !== 'function') {
  throw new Error('Expected the listener to be a function.')
}
复制代码

充分考虑到入参的正确性,以及经过isDispatchingisSubscribed来避免意外发生。闭包

其实这个实现也是一个很简单的高阶函数的实现。是否是常常在前端面试题里面看到?(T_T)app

这让我想起来了。不少初级,中级前端工程师调用完addEventListener就忘记使用removeEventListener最终致使不少闭包错误。因此,记得在不在使用的时候取消订阅是很是重要的。

dispatch 发布

经过Reduxdispatch接口,咱们能够发布一个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.')
}
复制代码

不得不说,做者在代码健壮性的考虑是很是周全的,真的是自叹不如,我如今基本上是只要本身点不出来问题就直接提测。 (T_T)

下面的代码更严谨,为了保障代码的健壮性,以及整个ReduxStore对象的完整性。直接使用了try { ... } finally { ... }来保障isDispatching这个内部全局状态的一致性。

再一次跪服+掩面痛哭 (T_T)

后面就是执行以前添加的订阅函数。固然订阅函数是没有任何参数的,也就意味着,使用者必须经过store.getState()来取得最新的状态。

observable 观察者

从函数字面意思,很容易猜到observable是一个观察者模式的实现接口。

function observable() {
  const outerSubscribe = subscribe
  return {
    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
    }
  }
}
复制代码

在开头,就将订阅接口进行了拦截,而后返回一个新的对象。这个对象为用户提供了添加观察对象的接口,而这个观察对象须要具备一个next函数。

combineReducers 又双叒叕见“高阶函数”

function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  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
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  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
    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
  }
}
复制代码

再一次被做者的严谨所折服,从函数开始就对参数的有效性进行了检查,而且只有在非生产模式才进行这种检查。并在assertReducerShape中对每个注册的reducer进行了正确性的检查用来保证每个reducer函数都返回非undefined值。

哦!老天,在返回的函数中,又进行了严格的检查(T_T)。而后将每个reducer的返回值从新组装到新的nextState中。并经过一个浅比较来决定是返回新的状态仍是老的状态。

bindActionCreators 仍是高阶函数

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

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
}
复制代码

我平时是不多用这个API的,可是这并不阻碍我去欣赏这段代码。可能这里是我惟一可以吐槽大神的地方了for (let i = 0; i < keys.length; i++) {,固然他在这里这么用其实并不会引发什么隐患,可是每次循环都要取一次length也是须要进行一次多余计算的(^_^)v,固然上面代码也有这个问题。

其实在开始位置的return dispatch(actionCreator.apply(this, arguments))apply(this)的使用更是很是的666到飞起。

通常咱们会在组件中这么作:

import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)

class TodoListContainer extends Component {
  componentDidMount() {
    let { dispatch } = this.props
    let action = TodoActionCreators.addTodo('Use Redux')
    dispatch(action)
  }

  render() {
    let { todos, dispatch } = this.props

    let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    console.log(boundActionCreators)

    return <TodoList todos={todos} {...boundActionCreators} /> } } export default connect( state => ({ todos: state.todos }) )(TodoListContainer) 复制代码

当咱们使用bindActionCreators建立action发布函数的时候,它会自动将函数的上下文(this)绑定到当前的做用域上。可是一般我为了解藕,并不会在action的发布函数中访问this,里面只存放业务逻辑。

再一个还算能够吐槽的地方就是对于Object的判断,对于function的判断重复出现屡次。固然,单独拿出来一个函数来进行调用,性能代价要比直接写在这里要大得多。

applyMiddleware 强大的聚合器

import compose from './compose'

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 => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
复制代码

经过前面的代码,咱们能够发现applayMiddleware其实就是包装enhancer的工具函数,而在createStore的开始,就对参数进行了适配。

一般咱们会像下面这样注册middleware

const store = createStore(
  reducer,
  preloadedState,
  applyMiddleware(...middleware)
)
复制代码

或者

const store = createStore(
  reducer,
  applyMiddleware(...middleware)
)
复制代码

因此,咱们会惊奇的发现。哦,原来咱们把applyMiddleware调用放到第二个参数和第三个参数都是同样的。因此咱们也能够认为createStore也实现了适配器模式。固然,貌似有一些牵强(T_T)。

关于applyMiddleware,也许最复杂的就是对compose的使用了。

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
复制代码

经过以上代码,咱们将全部传入的middleware进行了一次剥皮,把第一层高阶函数返回的函数拿出来。这样chain实际上是一个(next) => (action) => { ... }函数的数组,也就是中间件剥开后返回的函数组成的数组。 而后经过compose对中间件数组内剥出来的高阶函数进行组合造成一个调用链。调用一次,中间件内的全部函数都将被执行。

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)))
}
复制代码

所以通过compose处理后,传入中间件的next实际上就是store.dispatch。而这样处理后返回的新的dispatch,就是通过applyMiddleware第二次剥开后的高阶函数(action) => {...}组成的函数链。而这个函数链传递给applyMiddleware返回值的dispatch属性。

而经过applyMiddleware返回后的dispatch被返回给store对象内,也就成了咱们在外面使用的dispatch。这样也就实现了调用dispatch就实现了调用全部注册的中间件。

结束语

Redux的代码虽然只有短短几百行,可是蕴含着不少设计模式的思想和高级JS语法在里面。每次读完,都会学到新的知识。而做者对于高阶函数的使用是你们极好的参考。

固然本人涉足JS开发时间有限。会存在不少理解不对的地方,但愿大咖指正。

相关文章
相关标签/搜索