随着Redux作为React的状态管理解决方案的兴起,Reducer的概念在JavaScript中开始流行。但不用担心,你不需要学习Redux来理解Reducer。基本上,还原器是用来管理应用程序中的状态的。例如,如果用户在HTML输入框中写了什么,应用程序就必须管理这个UI状态(例如控制组件)。
让我们深入了解一下实现的细节。从本质上讲,还原器是一个函数,它接受两个参数--当前状态和一个动作--并根据这两个参数返回一个新的状态。在一个伪函数中,它可以被表达为:
(state, action) => newState
举例来说,对于将一个数字增加1的情况,它在JavaScript中看起来像下面这样:
function counterReducer(state, action) { return state + 1;}
或者定义为JavaScript箭头函数,对于同样的逻辑,它看起来如下:
const counterReducer = (state, action) => { return state + 1;};
在这种情况下,当前状态是一个整数(如count),还原器函数将count增加1。如果我们将参数state 改名为count ,对于这个概念的新手来说,可能更容易阅读和接近。然而,请记住,count 仍然是状态:
const counterReducer = (count, action) => { return count + 1;};
减速器函数是一个没有任何副作用的纯函数,这意味着给定相同的输入(例如:state 和action ),预期的输出(例如:newState )将总是相同。这使得还原器函数成为推理状态变化和隔离测试的完美选择。你可以用相同的输入作为参数重复相同的测试,并且总是期待相同的输出:
expect(counterReducer(0)).to.equal(1); // successful testexpect(counterReducer(0)).to.equal(1); // successful test
这就是还原器函数的本质。然而,我们还没有触及减速器的第二个参数:行动。action 通常被定义为一个具有type 属性的对象。根据动作的类型,还原器可以执行有条件的状态转换。
const counterReducer = (count, action) => { if (action.type === 'INCREASE') { return count + 1; }
if (action.type === 'DECREASE') { return count - 1; }
return count;};
如果动作type 不符合任何条件,我们就返回不变的状态。测试一个具有多个状态转换的还原器函数--给定相同的输入,它将总是返回相同的预期输出--仍然是前面提到的事实,这在下面的测试案例中得到了证明:
// successful tests// because given the same input we can always expect the same outputexpect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);expect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);
// other state transitionexpect(counterReducer(0, { type: 'DECREASE' })).to.equal(-1);
// if an unmatching action type is defined the current state is returnedexpect(counterReducer(0, { type: 'UNMATCHING_ACTION' })).to.equal(0);
然而,你更有可能看到switch case语句,而不是if else语句,以便为一个还原器函数映射多个状态转换。下面的还原器执行与之前相同的逻辑,但用switch case语句表达:
const counterReducer = (count, action) => { switch (action.type) { case 'INCREASE': return count + 1; case 'DECREASE': return count - 1; default: return count; }};
在这种情况下,count 本身就是一个状态,我们通过增加或减少计数来应用我们的状态变化。然而,你通常不会把一个JavaScript原语(例如,计数的整数)作为状态,而是一个复杂的JavaScript对象。例如,计数可以是我们state 对象的一个属性:
const counterReducer = (state, action) => { switch (action.type) { case 'INCREASE': return { ...state, count: state.count + 1 }; case 'DECREASE': return { ...state, count: state.count - 1 }; default: return state; }};
如果你不能立即理解这里的代码发生了什么,不要担心。最重要的是,一般来说,有两件重要的事情需要理解。
-
**由还原器函数处理的状态是不可改变的。**这意味着传入的状态--作为参数传入--从未被直接改变。因此,还原器函数总是要返回一个新的状态对象。如果你还没有听说过不可变性,你可能想看看不可变的数据结构这个话题。
-
由于我们知道状态是一个不可变的数据结构,我们可以使用JavaScript传播操作符 从传入的状态和我们想要改变的部分(例如:
count属性)创建一个新的状态对象。这样我们就可以确保其他没有从传入的状态对象中触及的属性在新的状态对象中仍然保持原样。
让我们通过另一个例子来看看这两个重要的代码点,我们想通过下面的还原函数来改变一个人对象的姓氏。
const personReducer = (person, action) => { switch (action.type) { case 'INCREASE_AGE': return { ...person, age: person.age + 1 }; case 'CHANGE_LASTNAME': return { ...person, lastname: action.lastname }; default: return person; }};
我们可以在测试环境中用下面的方式来改变一个用户的姓氏。
const initialState = { firstname: 'Liesa', lastname: 'Huppertz', age: 30,};
const action = { type: 'CHANGE_LASTNAME', lastname: 'Wieruch',};
const result = personReducer(initialState, action);
expect(result).to.equal({ firstname: 'Liesa', lastname: 'Wieruch', age: 30,});
你已经看到,通过在我们的reducer函数中使用JavaScript spread操作符,我们为新的状态对象使用了当前状态对象的所有属性,但为这个新对象覆盖了特定的属性(例如:lastname )。这就是为什么你会经常看到传播操作符来保持状态操作的不可变性(=状态不被直接改变)。
同时你也看到了还原器函数的另一个方面。一个为减速器函数提供的操作可以在强制性的操作类型属性旁边有一个可选的有效载荷(例如:lastname )。该有效载荷是执行状态转换的额外信息。例如,在我们的例子中,如果没有额外的信息,还原器就不会知道我们的人的新姓氏。
通常,一个动作的可选有效载荷被放入另一个通用的payload 属性中,以保持动作对象的顶级属性更加通用(.e.g{ type, payload } )。这对于让类型和有效载荷总是并排分开很有用。对于我们之前的代码例子,它将把动作改变成下面的样子。
const action = { type: 'CHANGE_LASTNAME', payload: { lastname: 'Wieruch', },};
还原器函数也必须改变,因为它必须深入到动作的一个层次。
const personReducer = (person, action) => { switch (action.type) { case 'INCREASE_AGE': return { ...person, age: person.age + 1 }; case 'CHANGE_LASTNAME': return { ...person, lastname: action.payload.lastname }; default: return person; }};
基本上你已经学到了你需要知道的所有关于还原器的知识。它们被用来在提供额外信息的动作的帮助下执行从A到B的状态转换。你可以在这个GitHub资源库中找到本教程中的减速器例子,包括测试。在这里,我们再次对所有的内容进行简要介绍。
- 语法:本质上,一个减速器函数被表达为
(state, action) => newState。 - 不变性:状态不会被直接改变。相反,还原器总是创建一个新的状态。
- 状态转换:一个还原器可以有条件性的状态转换。
- 动作:一个普通的动作对象有一个强制性的类型属性和一个可选的有效载荷。
- 类型属性选择条件性状态转换。
- 动作的有效载荷提供了状态转换的信息。