为什么Redux需要reducers是纯函数

本文转载自:众成翻译 译者:mjzhang 链接:http://www.zcfy.cc/article/2515 原文:https://medium.freecodecamp.com/why-redux-needs-reducers-to-be-pure-functions-d438c58ae468#.mvfdm0whk

你可能知道Redux依赖函数式编程中的纯函数。这究竟是什么意思呢?

下面的这张图片是来自Redux示例中的一个Todo应用。它目前有四个Todo任务。它展示了所有已完成和未完成的任务,我们可以看到第四个任务为已完成状态。图片的右侧展示了存储在Redux当中的当前state。这是一个包含了所有详细信息的Javascript对象。

图片的右侧展示了存储在Redux当中的当前state。这是一个包含了所有详细信息的Javascript对象。

这是Redux精妙之处。

图注:左边: Todo app <- -> 右边: Redux stated

现在让我们切换第四个Todo任务的状态使其变为未完成。下图右边是应用中新的Redux的state:

图注:应用发生改变时,Redux 更新state

现在如果你去Reducer当中查看“TOGGLE_TODO”部分,“TOGGLE_TODO”是切换应用中一个Todo项的完成和未完成状态,它的代码如下 (点击查看源码):

当你切换一个Todo项的状态,将发生如下过程:reducer函数接收一个表示旧的state的对象(也就是函数的输入),然后通过复制旧对象的所有属性(像idtext)和用新的属性覆盖旧的属性(completed属性)来创建一个新的对象。

纯函数

从本质上讲,纯函数的定义如下:不修改函数的输入值,依赖于外部状态(比如数据库,DOM和全局变量),同时对于任何相同的输入有着相同的输出结果。

举个例子,下面的add函数不修改变量a或b,同时不依赖外部状态,对于相同的输入始终返回相同的结果。

const add = (a, b) => a + b //pure function

现在我们回过头去看reducer函数,它符合上述纯函数的所有特征,因此我们说reducer是纯函数。

但是为什么reducer必须为纯函数?

让我们来看看如果reducer不是纯函数会发生什么。我们注释掉之前reducer中返回新对象的代码部分,然后我们直接修改state的completed属性。

case 'TOGGLE_TODO':
      if (state.id !== action.id) {
        return state;
      }
      // return {
      //   ...state,
      //   completed: !state.completed
      // }
      //mutate the state’s completed prop directly
      state.completed = !state.completed;//change original object
      return state;
      default: ...

做上述改变后,我们触发TOGGLE_TODO,会发现没有任何变化发生。

下图是Redux的部分源码。

我们阅读源码可以看到,Redux接收一个给定的state(对象),然后通过循环将state的每一部分传递给每个对应的reducer。如果有发生任何改变,reducer将返回一个新的对象。如果不发生任何变化,reducer将返回旧的state。

Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同(译者注:也就是Javascript对象浅比较)。如果你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。

但是,我们仍然有一些关键问题没有解答:

  • 为什么Redux这样设计?

  • 为什么Redux不在其他地方复制一份旧的state,然后将其和reducers返回的对象进行比较?

  • 为什么Redux要将这个负担交给开发者?

答案只有一个:因为比较两个Javascript对象所有的属性是否相同的的唯一方法是对它们进行深比较。

但是深比较在真实的应用当中代价昂贵,因为通常js的对象都很大,同时需要比较的次数很多。

因此一个有效的解决方法是作出一个规定:无论何时发生变化时,开发者都要创建一个新的对象,然后将新对象传递出去。同时,当没有任何变化发生时,开发者发送回旧的对象。也就是说,新的对象代表新的state。

必须注意到你只能使用slice(译者注:此处slice类似数组的slice方法,具体可以使用本文例子中解构赋值等方法进行slice)或者类似的机制去复制旧的值到新的对象里。

现在使用了新的策略之后,你能够比较两个对象通过使用!==比较两个对象的存储位置而不是比较两个对象的所有属性。同时当两个对象不同的时候,你就能知道新的对象已经改变了旧的state(也就是说,JavaScript对象当中的某些属性的值发生了变化)。这正是Redux所采取的策略。

这就是为什么Redux需要reducers是纯函数的原因!