Redux其实很简单(原理篇)

在上一篇文章中,咱们经过一个示例页面,了解到Redux的使用方法以及各个功能模块的做用。若是还不清楚Redux如何使用,能够先看看Redux其实很简单(示例篇),而后再来看本文,理解起来会更加轻松。git

那么在这一篇文章中,笔者将带你们编写一个完整的Redux,深度剖析Redux的方方面面,读完本篇文章后,你们对Redux会有一个深入的认识。github

核心API

这套代码是笔者阅读完Redux源码,理解其设计思路后,自行总结编写的一套代码,API的设计遵循与原始一致的原则,省略掉了一些没必要要的API。redux

createStore

这个方法是Redux核心中的核心,它将全部其余的功能链接在一块儿,暴露操做的API供开发者调用。数组

const INIT = '@@redux/INIT_' + Math.random().toString(36).substring(7)

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

  let state = initialState
  const listeners = []
  const store = {
    getState () {
      return state
    },
    dispatch (action) {
      if (action && action.type) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
      }
    },
    subscribe (listener) {
      if (typeof listener === 'function') {
        listeners.push(listener)
      }
    }
  }

  if (typeof initialState === 'undefined') {
    store.dispatch({ type: INIT })
  }

  if (typeof enhancer === 'function') {
    return enhancer(store)
  }

  return store
}

在初始化时,createStore会主动触发一次dispach,它的action.type是系统内置的INIT,因此在reducer中不会匹配到任何开发者自定义的action.type,它走的是switch中default的逻辑,目的是为了获得初始化的状态。app

固然咱们也能够手动指定initialState,笔者在这里作了一层判断,当initialState没有定义时,咱们才会dispatch,而在源码中是都会执行一次dispatch,笔者认为没有必要,这是一次多余的操做。由于这个时候,监听流中没有注册函数,走了一遍reducer中的default逻辑,获得新的state和initialState是同样的。dom

第三个参数enhancer只有在使用中间件时才会用到,一般状况下咱们搭配applyMiddleware来使用,它能够加强dispatch的功能,如经常使用的logger和thunk,都是加强了dispatch的功能。异步

同时createStore会返回一些操做API,包括:函数

  • getState:获取当前的state值
  • dispatch:触发reducer并执行listeners中的每个方法
  • subscribe:将方法注册到listeners中,经过dispatch来触发

applyMiddleware

这个方法经过中间件来加强dispatch的功能。性能

在写代码前,咱们先来了解一下函数的合成,这对后续理解applyMiddleware的原理大有裨益。this

函数的合成

若是一个值要通过多个函数,才能变成另一个值,就能够把全部中间步骤合并成一个函数,这叫作函数的合成(compose)

举个例子

function add (a) {
  return function (b) {
    return a + b
  }
}

// 获得合成后的方法
let add6 = compose(add(1), add(2), add(3))

add6(10) // 16

下面咱们经过一个很是巧妙的方法来写一个函数的合成(compose)。

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

上述代码巧妙的地方在于:经过数组的reduce方法,将两个方法合成一个方法,而后用这个合成的方法再去和下一个方法合成,直到结束,这样咱们就获得了一个全部方法的合成函数。

有了这个基础,applyMiddleware就会变得很是简单。

import { compose } from './utils'

export default function applyMiddleware (...middlewares) {
  return store => {
    const chains = middlewares.map(middleware => middleware(store))
    store.dispatch = compose(...chains)(store.dispatch)

    return store
  }
}

光看这段代码可能有点难懂,咱们配合中间件的代码结构来帮助理解

function middleware (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      // do something
      dispatch(action)
      // do something
    }
  }
}

能够看出,chains是函数f1的数组,经过compose将所欲f1合并成一个函数,暂且称之为F1,而后咱们将原始dispatch传入F1,通过f2函数一层一层地改造后,咱们获得了一个新的dispatch方法,这个过程和Koa的中间件模型(洋葱模型)原理是同样的。

为了方便你们理解,咱们再来举个例子,有如下两个中间件

function middleware1 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(1)
      dispatch(action)
      console.log(1)
    }
  }
}

function middleware2 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(2)
      dispatch(action)
      console.log(2)
    }
  }
}

// applyMiddleware(middleware1, middleware2)

你们猜一猜以上的log输出顺序是怎样的?

好了,答案揭晓:1, 2, (原始dispatch), 2, 1。

为何会这样呢?由于middleware2接收的dispatch是最原始的,而middleware1接收的dispatch是通过middleware1改造后的,我把它们写成以下的样子,你们应该就清楚了。

console.log(1)

/* middleware1返回给middleware2的dispatch */
console.log(2)
dispatch(action)
console.log(2)
/* end */

console.log(1)

三个或三个以上的中间件,其原理也是如此。

至此,最复杂最难理解的中间件已经讲解完毕。

combineReducers

因为Redux是单一状态流管理的模式,所以若是有多个reducer,咱们须要合并一下,这块的逻辑比较简单,直接上代码。

export default function combineReducers (reducers) {
  const availableKeys = []
  const availableReducers = {}

  Object.keys(reducers).forEach(key => {
    if (typeof reducers[key] === 'function') {
      availableKeys.push(key)
      availableReducers[key] = reducers[key]
    }
  })

  return (state = {}, action) => {
    const nextState = {}
    let hasChanged = false

    availableKeys.forEach(key => {
      nextState[key] = availableReducers[key](state[key], action)

      if (!hasChanged) {
        hasChanged = state[key] !== nextState[key]
      }
    })

    return hasChanged ? nextState : state
  }
}

combineReucers将单个reducer塞到一个对象中,每一个reducer对应一个惟一键值,单个reducer状态改变时,对应键值的值也会改变,而后返回整个state。

bindActionCreators

这个方法就是将咱们的action和dispatch链接起来。

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

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

  const boundActionCreators = {}

  Object.keys(actionCreators).forEach(key => {
    let actionCreator = actionCreators[key]

    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })

  return boundActionCreators
}

它返回一个方法集合,直接调用来触发dispatch。

中间件

在本身动手编写中间件时,你必定会惊奇的发现,原来这么好用的中间件代码居然只有寥寥数行,却能够实现这么强大的功能。

logger

function getFormatTime () {
  const date = new Date()
  return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds()
}

export default function logger ({ getState }) {
  return next => action => {
    /* eslint-disable no-console */
    console.group(`%caction %c${action.type} %c${getFormatTime()}`, 'color: gray; font-weight: lighter;', 'inherit', 'color: gray; font-weight: lighter;')
    // console.time('time')
    console.log(`%cprev state`, 'color: #9E9E9E; font-weight: bold;', getState())
    console.log(`%caction    `, 'color: #03A9F4; font-weight: bold;', action)

    next(action)

    console.log(`%cnext state`, 'color: #4CAF50; font-weight: bold;', getState())
    // console.timeEnd('time')
    console.groupEnd()
  }
}

thunk

export default function thunk ({ getState }) {
  return next => action => {
    if (typeof action === 'function') {
      action(next, getState)
    } else {
      next(action)
    }
  }
}

这里要注意的一点是,中间件是有执行顺序的。像在这里,第一个参数是thunk,而后才是logger,由于假如logger在前,那么这个时候action多是一个包含异步操做的方法,不能正常输出action的信息。

心得体会

到了这里,关于Redux的方方面面都已经讲完了,但愿你们看完可以有所收获。

可是笔者其实还有一个担心:每一次dispatch都会从新渲染整个视图,虽然React是在虚拟DOM上进行diff,而后定向渲染须要更新的真实DOM,可是咱们知道,通常使用Redux的场景都是中大型应用,管理庞大的状态数据,这个时候整个虚拟DOM进行diff可能会产生比较明显的性能损耗(diff过程其实是对象和对象的各个字段挨个比较,若是数据达到必定量级,虽然没有操做真实DOM,也可能产生可观的性能损耗,在小型应用中,因为数据较少,所以diff的性能损耗能够忽略不计)。

本文源码地址:https://github.com/ansenhuang/redux-demo

相关文章
相关标签/搜索