redux中的reduce_从Reduce到Redux:通过构建Redux了解Redux

redux中的reduce

by Johnny Snelgrove

通过约翰尼·斯内格罗夫

从Reduce到Redux:通过构建Redux了解Redux (From Reduce to Redux: Understanding Redux by Building Redux)

The two most important techniques I’ve discovered to help with understanding a concept quickly are simplification and learning by doing. Redux is an extremely popular JavaScript library for developing “predictable state containers for JavaScript apps.” It takes a functional approach to modeling data, which challenges the traditional MVC pattern.

我发现可以帮助您快速理解概念的两个最重要的技术是简化和边做边学Redux是一个非常流行JavaScript库,用于开发“ JavaScript应用程序的可预测状态容器”。 它采用一种功能化的方法来对数据建模,这对传统的MVC模式提出了挑战。

Many developers, including myself, found this paradigm shift difficult. However, understanding this approach is incredibly rewarding and valuable. The concepts transcend languages and frameworks, and professionally, many modern front ends are adopting Redux and its associated functional paradigms to handle their client-side data layer.

包括我在内的许多开发人员都发现这种模式转换很困难。 但是,了解这种方法是非常有益和有价值的。 这些概念超越了语言和框架,并且在专业上,许多现代前端都在采用Redux及其关联的功能范式来处理其客户端数据层。

In this post, we’ll build a simplified Redux library from scratch in order to really understand Redux. Starting with a simple sum function, we’ll gradually build up to a Redux-style state management system for a simple game agent.

在本文中,我们将从头开始构建一个简化的Redux库,以真正了解Redux。 从一个简单的求和函数开始,我们将逐步构建一个用于简单游戏代理的Redux风格的状态管理系统。

什么减少? (What’s Reduce?)

The key to understanding Redux lies in understanding the power of the reduce function. From the Mozilla Docs:

理解Redux的关键在于理解reduce函数的功能。 从Mozilla Docs中

“The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.”
“ reduce()方法对一个累加器和数组中的每个元素(从左到右)应用一个函数,以将其减小为单个值。”

If this doesn’t make sense to you, fear not. The power of reduce comes from its generality, which also makes it difficult to describe. To remember what reduce() does, just remember that reduce rhymes with deduce. The reduce function deduces the next state given an existing state and a transition rule. It does this for each element in an array, from left to right, passing the result along in series, then returning the final result. Here is a possible implementation of reduce():

如果这对您没有意义,请不要担心。 reduce的强大之处在于它的通用性,这也使其难以描述。 要记住reduce()的作用,只需记住用ducuce 减少押韵。 在给定现有状态和转换规则的情况下,reduce函数推导出下一个状态。 它从左到右对数组中的每个元素执行此操作,将结果依次传递,然后返回最终结果。 这是reduce()的可能实现:

function reduce (collection, transitionFn, initialState) {  let accumulator = initialState || collection[0]  for (let i = (initialState ? 0 : 1); i < collection.length; i++) {    accumulator = transitionFn(accumulator, collection[i])  }  return accumulator}

Side Note: the transition function actually accepts four arguments: the accumulator, the current value (i.e. collection[i]), the index of the current value, and the collection itself. However, for demonstration purposes, the index and collection arguments are omitted here as they’re irrelevant.

注意:转换函数实际上接受四个参数:累加器,当前值(即collection [i]),当前值的索引以及集合本身。 但是,出于演示目的,此处省略了index和collection参数,因为它们是不相关的。

Lucky for us, reduce() is already a builtin array method in JavaScript, and uses the calling array as the collection to reduce. Now that we have an idea of what the reduce function is, it’s time to dive deeper and start exploring how we can use it to model the state of a game agent.

对我们来说幸运的是, reduce()已经是JavaScript中的内置数组方法,并且使用调用数组作为减少的集合。 既然我们已经了解了reduce函数的含义,那么该深入了解并开始研究如何使用它来对游戏代理状态进行建模了。

使用减少 (Using Reduce)

To understand the power of reduce, we’ll start off with the canonical reducer function, sum():

要了解减小的功率,我们将使用规范减速功能,SUM()开始

function sum (nums) {  return nums.reduce((state, nextVal) => state + nextVal)}
sum([1, 2, 3, 4]) // => 10

This example never gave me that “Aha!” moment. Probably because it obscures the function signature and isn’t very exciting. Here’s the same example with everything spelled out explicitly:

这个例子从来没有给我“啊哈!” 时刻。 可能是因为它模糊了函数签名并且不是很令人兴奋。 这是同一示例,其中所有内容均已明确说明:

function sum (nums) {  function transition (prevState, nextVal) {    return prevState + nextVal  }  const [initialState, ...tail] = nums  return tail.reduce(transition, initialState)}
sum([1, 2, 3, 4]) // => 10

note: const [initialState, …tail] = nums uses ES6 destructuring to split the array into the first element (initialState) and the remaining elements (tail).

注意: const [initialState, …tail] = nums使用ES6 解构将数组拆分为第一个元素(initialState)和其余元素(tail)。

Here we can see that the reduce function takes a transition function as its first argument, and an initial starting state as its second argument. By default, reduce uses the first item in the array as the initial state if no initial state is supplied.

在这里,我们可以看到reduce函数将转换函数作为其第一个参数,并将初始启动状态作为其第二个参数。 默认情况下,如果未提供初始状态,则reduce将数组中的第一项用作初始状态。

变得具体 (Getting Specific)

To conceptually move towards modeling more interesting data, we can rewrite sum with domain-specific variable names:

为了从概念上转向建模更有趣的数据,我们可以使用特定于域的变量名重写sum

function move (steps) {  return steps.reduce((state, direction) => state + direction)}
xPosition = -2xPosition = xPosition + move([-1, -1, 0, 1, 1, 1])console.log(xPosition) // => -1

This is still the same summation function, but now it’s a bit clearer how it might be used in a real-world application. Our game character starts with an initial position of -2, which we then combine with a list of directions to determine the new position. Each value in the array passed into the move function can be thought of as an action that will tell the reducer how to mutate its state. Here our actions don’t have names, but by following some simple conventions, we arrive at the basis of redux:

它仍然是相同的求和函数,但是现在更加清楚了如何在实际应用程序中使用它。 我们的游戏角色从初始位置-2开始,然后将其与方向列表结合以确定新位置。 可以将传递给move函数的数组中的每个值视为一个动作 ,该动作将告诉reducer如何改变其状态。 这里我们的动作没有名称,但是通过遵循一些简单的约定,我们得出了redux的基础:

let store = 0 // initial position
const reducer = (state, action) => {  switch (action.type) {    case 'MOVE_LEFT':      return state - action.distance    case 'MOVE_RIGHT':      return state + action.distance    case 'WAIT':    default:      return state  }}
console.log(store) // => 0
store = [  {type: 'MOVE_LEFT', distance: 2 },  {type: 'MOVE_LEFT', distance: 3 },  {type: 'MOVE_RIGHT', distance: 7 },  {type: 'WAIT'}].reduce(reducer, store)
console.log(store) // => 2

If we agree that all of our array elements will be objects with a type field, then we can start explicitly handling actions in the reducer. Furthermore, by passing the existing store as the initial state to reduce() then overwriting it with the result, we can start transforming data across multiple calls to reduce.

如果我们同意所有数组元素都是具有类型字段的对象,那么我们可以开始在reducer中显式处理动作。 此外,通过将现有存储作为初始状态传递给reduce(),然后用结果覆盖它,我们可以开始跨多个调用转换数据以进行reduce。

We also arrive at a concept similar to that of a class with instance variables and methods. In OOP, everything in store might be an instance variable, and the action types would be methods:

我们还得出了一个类似于带有实例变量和方法的类的概念。 在OOP中, 存储中的所有内容都可能是一个实例变量,而操作类型将是方法:

class Mover {  constructor (x) {    this.x = x  }
moveLeft (distance) {    this.x -= distance  }
moveRight (distance) {    this.x += distance  }}
let agent = new Mover(0)agent.moveLeft(1)agent.moveLeft(1)agent.moveRight(1)
复杂数据 (Complex Data)

At this point, our character can only move left and right, and has no other interesting properties. To make things more interesting, and to extend this concept into multidimensional data, let’s add the ability to move up and down, and give the player some health:

此时,我们的角色只能左右移动,没有其他有趣的属性。 为了使事情变得更加有趣,并将这一概念扩展到多维数据中,让我们增加上下移动的能力,并为播放器提供一些健康:

let store = { x:0, y:0, health: 100 } // initial state
const reducer = (state, action) => {  switch (action.type) {    case 'MOVE_LEFT':      return { ...state, x: state.x - action.distance }    case 'MOVE_RIGHT':      return { ...state, x: state.x + action.distance }    case 'MOVE_UP':      return { ...state, y: state.y - action.distance }    case 'MOVE_DOWN':      return { ...state, y: state.y + action.distance }    case 'TAKE_DAMAGE':      return { ...state, health: state.health - action.damage }    case 'DRINK_POTION':      return { ...state, health: state.health + action.health }    case 'WAIT':    default:      return state  }}
console.log(store) // => { x:0, y:0, health: 100 }store = [  {type: 'MOVE_LEFT', distance: 2 },  {type: 'MOVE_LEFT', distance: 3 },  {type: 'MOVE_RIGHT', distance: 7 },  {type: 'WAIT'},  {type: 'MOVE_DOWN', distance: 7 },  {type: 'TAKE_DAMAGE', damage: 50 },  {type: 'DRINK_POTION', health: 25 },  {type: 'MOVE_UP', distance: 2 },].reduce(reducer, store)console.log(store) // => { x:2, y:5, health: 75 }

Here the state is an object with shape {x: Float, y: Float, health: Float}. The reducer must return a new object with the same shape. To return a new object, we use ES6 object destructuring (e.g. {...state}) to create a copy of the passed-in state object, then overwrite the field we’d like to update in one concise declarative expression: return {...oldState, key: newKeyVal}. Now we’re cookin’ with fire!

这里的状态是形状为{x: Float, y: Float, health: Float} 。 减速器必须返回具有相同形状的新对象。 要返回一个新对象,我们使用ES6对象分解(例如{...state} )创建传入状态对象的副本,然后使用一个简洁的声明性表达式覆盖我们要更新的字段: return {...oldState, key: newKeyVal} 。 现在我们在用火做饭!

泛化和封装 (Generalizing and Encapsulating)

To wrap this logic up and make stores general and reusable, we can write a createStore function to encapsulate the state and provide a consistent API for reading the state and dispatching actions:

为了包装此逻辑并使存储具有通用性和可重用性,我们可以编写一个createStore函数来封装状态,并提供一致的API来读取状态和调度动作:

const createStore = (reducer, initialState) => {  let store = initialState || reducer(undefined, {type: 'INIT'})  return {    dispatch: (action) => {      store = [action].reduce(reducer, store)    },    getState: _ => store  }}
var moverReducer = (state = { x:0, y:0 }, action) => {  switch (action.type) {    case 'MOVE_LEFT':      return { ...state, x: state.x - action.distance }    case 'MOVE_RIGHT':      return { ...state, x: state.x + action.distance }    case 'MOVE_UP':      return { ...state, y: state.y - action.distance }    case 'MOVE_DOWN':      return { ...state, y: state.y + action.distance }    case 'WAIT':    default:      return state  }}
let agent = createStore(moverReducer)agent.dispatch({type:'MOVE_UP', distance: 1})agent.dispatch({type:'MOVE_LEFT', distance: 2})agent.dispatch({type:'MOVE_RIGHT', distance: 4})agent.dispatch({type:'MOVE_DOWN', distance: 2})agent.getState() // => { x:-2, y:0 }

Here we can either pass createStore() an initial state (maybe something we load from localStorage), or it will initialize using the reducer’s default state argument and a dummy action. Our state is encapsulated using a closure, and the only way to read and write to it is through the returned getState() and dispatch() methods, respectively.

在这里,我们既可以传递createStore()的初始状态(也许是我们从localStorage加载的某种状态),也可以使用reducer的默认状态参数和虚拟操作进行初始化。 我们的状态使用闭包进行封装,唯一的读取和写入方法是分别通过返回的getState()dispatch()方法。

At this point, we’ve arrived at a basic but useful version of the Redux API! We’ve omitted store enhancers and subscriptions, however, since they’re primarily used for side effects and reactively updating a view. In the final section, we’ll simply use a render loop and top-level code to handle these cases and keep things simple.

至此,我们已经到达了Redux API的基本但有用的版本! 但是,我们省略了商店增强器和订阅,因为它们主要用于副作用和被动更新视图。 在最后一节中,我们将简单地使用渲染循环和顶级代码来处理这些情况并使事情简单。

利弊 (Pros and Cons)

The first clear benefit of the reducer approach is that everything is easily serializable. We could easily use localStorage to save and load the state, serialize action sequences, send actions via WebSockets or HTTP requests, and so on, all without building out handlers to translate JSON payloads to instance method calls.

Reducer方法的第一个明显好处是,所有内容都易于序列化。 我们可以轻松地使用localStorage来保存和加载状态,序列化操作序列,通过WebSocket或HTTP请求发送操作等,而无需构建处理程序即可将JSON有效负载转换为实例方法调用。

In addition, since reducers should be pure functions, there’s no guarantee that unexpected side effects won’t occur in other parts of your application via updating a model’s data. A store is purely concerned with data modeling and logic. This makes our data models extremely portable, as they’re not concerned with their runtime environment. The same reducers could potentially be used in a node.js cli app, a web app, or a native app via something like React Native. Porting the application becomes a matter of writing platform-specific side effects and view code.

另外,由于reducers应该是纯函数 ,因此无法保证通过更新模型数据不会在应用程序的其他部分中发生意外的副作用。 一家商店纯粹是在关注数据建模和逻辑。 这使我们的数据模型非常可移植,因为它们与运行时环境无关。 可以通过诸如React Native之类的东西在node.js cli应用程序,Web应用程序或本机应用程序中使用相同的reducers。 移植应用程序成为编写特定于平台的副作用和查看代码的问题。

Finally, I personally find reducers to be elegant. The concept is closer to a mathematical equation that is setting values in a model from a controller script. Check out the Q-learning formula as an example. Its signature is a state/action pair! This makes it easier to translate a formula to code.

最后,我个人认为减速器优雅。 该概念更接近于通过控制器脚本在模型中设置值的数学方程式。 以Q学习公式为例。 它的签名是状态/动作对! 这样可以更轻松地将公式转换为代码。

The downside is that redux doesn’t have a strong opinion on how to handle side effects (for example, rendering to the DOM, logging to the console, saving to localStorage, starting an Ajax request, and so on). You cannot build an interesting application without side effects, so this can be a little frustrating.

缺点是redux对如何处理副作用 (例如,渲染到DOM,登录到控制台,保存到localStorage,启动Ajax请求等)没有强烈的意见。 您不能在没有副作用的情况下构建有趣的应用程序,因此这可能会有些令人沮丧。

The solution is generally to put this code in action creating methods, middleware, or move it to the top level of your application (not ideal). However, it can often be beneficial to write model code with this constraint, as it forces you to write easily testable code and focus on the logic of what you’re modeling.

解决方案通常是将这些代码放入创建方法,中间件的操作中,或将其移至应用程序的顶层(不理想)。 但是,在这种约束下编写模型代码通常是有益的,因为它迫使您编写易于测试的代码,并专注于建模对象的逻辑。

Other downsides include lots of boilerplate to accomplish simple tasks like incrementing a counter, and the general cognitive load it takes to move away from object-oriented concepts. However, these are what make our models so portable and powerful!

其他缺点包括许多样板来完成简单的任务,例如增加计数器,以及摆脱面向对象概念所需的一般认知负担。 但是,这些正是我们的模型如此轻巧和强大的原因!

包扎和徘徊 (Wrapping Up and Wandering Around)

To wrap this all up, we can add an update loop to dispatch random actions and render the agent’s state (here I’m using React, but we could use any view layer we like). At every tick, the agent either moves in a direction, waits, takes a potion or takes damage. If the agent’s health is at zero, it resets.

总结一下,我们可以添加一个更新循环来调度随机动作并呈现代理的状态(这里我使用的是React,但是我们可以使用我们喜欢的任何视图层)。 每次跳动时,特工要么向某个方向移动,要么等待,要么服药,要么受到伤害。 如果代理的运行状况为零,它将重置。

Notice how logic is starting to accumulate in the top level update/render loop. In addition, we’ve had to duplicate the code for the initial reducer state to reset the agent when its health hits zero.

请注意,逻辑是如何开始在顶级更新/渲染循环中累积的。 另外,我们必须复制初始还原器状态的代码,以在运行状况为零时重置代理。

We’ll address these and other issues in the next article, but for now, it’s enough to notice that logic can live in at least two places: the reducer or the action. In the next article, we’ll look at how to choose where to place that logic, and proceed to dive deeper, making our simulation more sophisticated using function composition, higher-order reducers, and action creators as we develop an increasingly intelligent game agent.

我们将在下一篇文章中讨论这些问题以及其他问题,但是到目前为止,足以注意到逻辑可以存在于至少两个地方:reduce或action。 在下一篇文章中,我们将研究如何选择放置逻辑的位置,并继续深入研究,在开发功能日益智能的游戏代理时,我们将使用功能组合,高阶缩减器和动作创建者来使仿真更加复杂。

翻译自: https://www.freecodecamp.org/news/from-reduce-to-redux-understanding-redux-by-building-redux-918ef08abafe/

redux中的reduce

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值