探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)

Redux 在几天前(2018.04.18)发布了新版本,6 commits 被合入 master。从诞生起,到现在 4.0 版本,Redux 保持了使用层面的平滑过渡。同时前不久, React 也从 15 升级到 16 版本,开发者并不须要做出太大的变更,便可“无痛升级”。可是在版本迭代的背后不少有趣的设计值得了解。Redux 这次升级一样如此。前端

本文将今后次版本升级展开,从源代码改动入手,进行分析。经过后文内容,相信读者可以在 JavaScript 基础层面有更深认识。react

本文支持前端初学者学习,同时更适合有 Redux 源码阅读经验者,核心源码并不会重复分析,更多将聚焦在升级改动上。git

改动点总览

此次升级改动点一共有 22 处,最主要体如今 TypeScript 使用、CommonJS 和 ES 构建、关于 state 抛错三方面上。对于工程和配置的改动,咱们再也不多费笔墨。主要从代码细节入手,基础入手,着重分析如下几处改动:github

  • 中间件 API dispatch 参数处理;
  • applyMiddleware 改动;
  • bindActionCreators 对 this 透明化处理;
  • dispatching 时,对 state 的冻结;
  • Plain Object 类型判断;

话很少说,咱们直接进入正题。redux

applyMiddleware 参数处理

这项改动由 Asvarox 提出。熟悉 Redux 源码中 applyMiddleware.js 设计的读者必定对 middlewareAPI 并不陌生:对于每一个中间件,均可以感知部分 store,即 middlewareAPI。这里简单展开一下:api

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

建立一个中间件 store:数组

let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
复制代码

咱们看,applyMiddleware 是个三级 curry 化的函数。它将陆续得到了三个参数,第一个是 middlewares 数组,[mid1, mid2, mid3, ...],第二个是 Redux 原生的 createStore,最后一个是 reducer;promise

applyMiddleware 利用 createStore 和 reducer 建立了一个 store,而后 store 的 getState 方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量。middlewares 数组经过 map 方法让每一个 middleware 带着 middlewareAPI 这个参数分别执行一遍。执行完后,得到 chain 数组,[f1, f2, ... , fx, ...,fn],接着 compose 将 chain 中的全部匿名函数,[f1, f2, ... , fx, ..., fn],组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn] 将会从右到左依次执行。以上解释改动自:pure render 专栏app

好了,把中间件机制简要解释以后,咱们看看此次改动。故事源于 Asvarox 设计了一个自定义的中间件,这个中间件接收的 dispatch 须要两个参数。他的“杰做”就像这样:框架

const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));
复制代码

对比传统编写中间件的套路:

const middleware = store => next => action => {...}
复制代码

咱们能清晰地看到他的这种编写方式会有什么问题:在原有 Redux 源码基础上,actionCreator 参数后面的 args 将会丢失。所以他提出的改动点在:

const middlewareAPI = {
       getState: store.getState,
-      dispatch: (action) => dispatch(action)
+      dispatch: (...args) => dispatch(...args)
     }
复制代码

若是你好奇他为何会这样设计本身的中间件,能够参考 #2501 号 issue。我我的认为对于需求来讲,他的这种“奇葩”方式,能够经过其余手段来规避;可是对于 Redux 库来讲,将 middlewareAPI.dispatch 参数展开,确实是更合适的作法。

此项改动咱们点到为止,再也不钻牛角尖。应该学到:基于 ES6 的不定参数与展开运算符的妙用。虽然一直在说,一直在提,但在真正开发程序时,咱们仍然要时刻注意,并养成良好习惯。

基于此,一样的改动也体如今:

export default function applyMiddleware(...middlewares) {
  -  return (createStore) => (reducer, preloadedState, enhancer) => {
  -  const store = createStore(reducer, preloadedState, enhancer)
  +  return (createStore) => (...args) => {
  +  const store = createStore(...args)
     let dispatch = store.dispatch
     let chain = []
复制代码

这项改动由 jimbolla 提出。

bindActionCreators 对 this 透明化处理

Redux 中的 bindActionCreators,达到 dispatch 将 action 包裹起来的目的。这样经过 bindActionCreators 建立的方法,能够直接调用 dispatch(action) (隐式调用)。可能不少开发者并不经常使用,因此这里稍微展开,在 action.js 文件中, 咱们定义了两个 action creators:

function action1(){
  return {
   type:'type1'
  }
}
function action2(){
  return {
   type:'type2'
  }
}
复制代码

在另外一文件 SomeComponent.js 中,咱们即可以直接使用:

import { bindActionCreators } from 'redux';
import * as oldActionCreator from './action.js'

class C1 extends Component {
  constructor(props) { 
    super(props);

    const {dispatch} = props;
    this.boundActionCreators = bindActionCreators(oldActionCreator, dispatch);
  }

  componentDidMount() {
    // 由 react-redux 注入的 dispatch:
    let { dispatch } = this.props;
    let action = TodoActionCreators.addTodo('Use Redux');
    dispatch(action);
  }

  render() {
  	// ...
  	let { dispatch } = this.props;
  	let newAction = bindActionCreators(oldActionCreator, dispatch)
  	return <Child {...newAction}></child>
  }
}
复制代码

这样一来,咱们在子组件 Child 中,直接调用 newAction.action1 就至关于调用 dispatch(action1),如此作的好处在于:没有 store 和 dispatch 的组件,也能够进行动做分发。

通常这个 API 应用很少,至少笔者不太经常使用。所以上面作一个简单介绍。有经验的开发中必定不难猜出 bindActionCreators 源码作了什么,连带着此次改动:

function bindActionCreator(actionCreator, dispatch) {
-  return (...args) => dispatch(actionCreator(...args))
+  return function() { return dispatch(actionCreator.apply(this, arguments)) }
 }
复制代码

咱们看此次改动,对 actionCreator 使用 apply 方法,明确地进行 this 绑定。那么这样作的意义在哪里呢?

我举一个例子,想象咱们对原始的 actionCreator 进行 this 绑定,并使用 bindActionCreators 方法:

const uniqueThis = {};
function actionCreator() {
  return { type: 'UNKNOWN_ACTION', this: this, args: [...arguments] }
};
const action = actionCreator.apply(uniqueThis,argArray);
const boundActionCreator = bindActionCreators(actionCreator, store.dispatch);
const boundAction = boundActionCreator.apply(uniqueThis,argArray);
复制代码

咱们应该指望 boundAction 和 action 一致;且 boundAction.this 和 uniqueThis 一致,都等同于 action.this。这如此的指望下,这样的改动无疑是必须的。

对 state 的冻结

Dan Abramov 认为,在 reducer 中使用 getState() 和 subscribe() 方法是一种反模式。store.getState 的调用会使得 reducer 不纯。事实上,原版已经在 reducer 执行过程当中,禁用了 dispatch 方法。源码以下:

function dispatch(action) {
    // ...

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    var listeners = currentListeners = nextListeners
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]()
    }

    return action
  }
复制代码

同时,此次修改在 getState 方法以及 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.'
  )
}
复制代码

笔者认为,这样的作法毫无争议。显式抛出异常无心是合理的。

Plain Object 类型判断

Plain Object 是一个很是有趣的概念。此次改动围绕判断 Plain Object 的性能进行了激烈的讨论。最终将引用 lodash isPlainObject 的判断方法改成 ./utils/isPlainObject 中本身封装的作法:

- import isPlainObject from 'lodash/isPlainObject';
+ import isPlainObject from './utils/isPlainObject'
复制代码

简单来讲,Plain Object:

指的是经过字面量形式或者new Object()形式定义的对象。

Redux 此次使用了如下代码来进行判断:

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
复制代码

若是读者对上述代码不理解,那么须要补一下原型、原型链的知识。简单来讲,就是判断 obj 的原型链有几层,只有一层就返回 true。若是还不理解,能够参考下面示例代码:

function Foo() {}

// obj 不是一个 plain object
var obj = new Foo();

console.log(typeof obj, obj !== null);

let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
  proto = Object.getPrototypeOf(proto)
}

// false
var isPlain = Object.getPrototypeOf(obj) === proto;
console.log(isPlain);
复制代码

而 loadash 的实现为:

function isPlainObject(value) {
  if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
    return false
  }
  if (Object.getPrototypeOf(value) === null) {
    return true
  }
  let proto = value
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }
  return Object.getPrototypeOf(value) === proto
}

export default isPlainObject
复制代码

isObjectLike 源码:

function isObjectLike(value) {
  return typeof value == 'object' && value !== null
}
复制代码

baseGetTag 源码:

const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  if (!(symToStringTag && symToStringTag in Object(value))) {
    return toString.call(value)
  }
  const isOwn = hasOwnProperty.call(value, symToStringTag)
  const tag = value[symToStringTag]
  let unmasked = false
  try {
    value[symToStringTag] = undefined
    unmasked = true
  } catch (e) {}

  const result = toString.call(value)
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag
    } else {
      delete value[symToStringTag]
    }
  }
  return result
}
复制代码

根据 timdorr 给出的对比结果,dispatch 方法中:

master: 4690.358ms
nodash: 82.821ms
复制代码

这一组 benchmark 引起的讨论天然少不了,也引出来了 Dan Abramov。笔者对此不发表任何意见,感兴趣的同窗可自行研究。从结果上来看,摒除了部分对 lodash 的依赖,在性能表现上说服力加强。

展望和总结

提到 Redux 发展,天然离不开 React,React 新版本一经推出,极受追捧。尤为是 context 这样的新 API,某些开发者认为将逐渐取代 Redux。

笔者认为,围绕 React 开发应用,数据状态管理始终是一个极其重要的话题。可是 React context 和 Redux 并非彻底对立的

首先 React 新特性 context 在大型数据应用的前提下,并不会减小模版代码。而其 Provider 和 Consumer 的一一对应特性,即 Provider 和 Consumer 必须来自同一次 React.createContext 调用(能够用 hack 方式解决此“局限”),仿佛 React 团队对于此特性的发展方向设计主要体如今小型状态管理上。若是须要实现更加灵活和直接的操做,Redux 也许会是更好的选择。

其次,Redux 丰富的生态以及中间件等机制,决定了其在很大程度上具备不可替代性。毕竟,已经使用 Redux 的项目,迁移成本也将是极大的,至少须要开发中先升级 React 以支持新版 context 吧。

最后,Redux 做为一个“发布订阅系统”,彻底能够脱离 React 而单独存在,这样的基因也决定了其后天与 React 自己 context 不一样的性征。

我认为,新版 React context 是对 React 自己“短板”的长线补充和完善,将来大几率也会有所打磨调整。Redux 也会进行一系列迭代,但就如同此次版本升级同样,将趋于稳定,更多的是细节上调整。

退一步讲,React context 的确也和 Redux 有千丝万缕的联系。任何类库或者框架都具备其短板,Redux 一样也如此。咱们彻底可使用新版 React context,在使用层面来规避 Redux 的一些劣势,模仿 Redux 所能作到的一切。如同 didierfranc 的 react-waterfall,国内@方正的 Rectx,都是基于新版 React context 的解决方案。

最后,我很赞同@诚身所说: 选择用什么样的工具历来都不是决定一个开发团队成败的关键,根据业务场景选择恰当的工具,并利用工具反过来约束开发者,最终达到控制总体项目复杂度的目的,才是促进一个开发团队不断提高的核心动力。

没错,真正对项目起到决定性做用的仍是是开发者自己,完善基础知识,提高开发技能,让咱们从 Redux 4.0 的改动看起吧。

广告时间: 若是你对前端发展,尤为对 React 技术栈感兴趣:个人新书中,也许有你想看到的内容。关注做者 Lucas HC,新书出版将会有送书活动。

Happy Coding!

PS: 做者 Github仓库 和 知乎问答连接 欢迎各类形式交流!

个人其余几篇关于React技术栈的文章:

从setState promise化的探讨 体会React团队设计思想

React 应用设计之道 - curry 化妙用

组件复用那些事儿 - React 实现按需加载轮子

经过实例,学习编写 React 组件的“最佳实践”

React 组件设计和分解思考

从 React 绑定 this,看 JS 语言发展和框架设计

作出Uber移动网页版还不够 极致性能打造才见真章**

React+Redux打造“NEWS EARLY”单页应用 一个项目理解最前沿技术栈真谛

相关文章
相关标签/搜索