lbwa / set.sh-stale

✍A place which is used to share my programming experiences in Chinese. 一个分享代码经历的地方。
https://set.sh
0 stars 0 forks source link

Redux 基本核心概念 #28

Open lbwa opened 5 years ago

lbwa commented 5 years ago
import { createStore } from 'redux'

/**
 * This is a reducer, a pure function with (state, action) => state signature.
 * 这是一个 reducer,一个形如 (state, action) => state signature 的 **纯函数**。
 * It describes how an action transforms the state into the next state.
 * reducer 描述例如一个 action 如何将一个 state 转换为一个新的 state。
 *
 * The shape of the state is up to you: it can be a primitive, an array, an object,
 * state 的结构取决于开发者:它可以是原始类型值,数组,对象
 * or even an Immutable.js data structure. The only important part is that you should
 * 或者甚至是一个 Immutable.js 的数据结构。唯一重要的点是开发者
 * not mutate the state object, but return a new object if the state changes.
 * 不应该修改 state 对象,而是当 state 变化时,返回一个新的 state 对象。
 *
 * In this example, we use a `switch` statement and strings, but you can use a helper that
 * follows a different convention (such as function maps) if it makes sense for your
 * project.
 */
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// Create a Redux store holding the state of your app.
// 创建一个 Redux store 来托管你的 app 的 state
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter)

// You can use subscribe() to update the UI in response to state changes.
// 你可以使用 subscribe() 来根据 response 更新 state
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// 通常情况下,你最好使用一个视图绑定库,而不是直接使用 subscribe()。
// However it can also be handy to persist the current state in the localStorage.
// 然而它也可以方便地在 localStorage 中持久化当前 state

store.subscribe(() => console.log(store.getState()))

// The only way to mutate the internal state is to dispatch an action.
// 修改内部 state 的唯一方式是 dispatch 一个 action
// The actions can be serialized, logged or stored and later replayed.
// action 可被序列化,打印,存储或者稍后重放
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

而不是直接修改 state,你可以指定任意修改 objectmutations,我们叫这种 mutations 叫做 actions。然后你写下一个叫做 reducer纯函数用于决定每一个 actions 是如何转换整个 appstate

在一个典型的 Redux app 中,只有唯一一个 storeroot reducing 函数(即 root reducer)。随着你的 app 的增长,你可以将 root reducer 拆分为分布在 state 树不同部分的更小的 reducer。这就像在一个 React app 中只存在一个根组件,但它是由许多更小的组件组合而成的。

这个架构似乎看起来对于一个 counter 应用是有点杀鸡用牛刀(overkill)了,但是这种模式的绝妙之处在于它可以很好的拓展至一个庞大并且复杂的 app。它也是非常强劲的开发者工具,因为可追踪每个引起 mutation 的源头 action。你可以记录用户绘画并且通过重放每一个 action 来重现用户会话。

lbwa commented 5 years ago

核心概念

lbwa commented 5 years ago

三个原则

Redux 可被描述为三项基本原则:

即整个应用的 state 是被存储在一个包含唯一 object 树的 store 中的。

单一 store 树便于探测和 Debug 应用的状态,并且便于在开发模式下持久化应用的 state。并且可传统技术上难以实现 撤销/恢复 的操作。

console.log(store.getState())

/* Prints
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/

唯一修改 state 的方式是派发一个 action,其中出发 action 的参数是一个描述了发生了什么的对象。

这是为了确保 视图请求回调 均无法直接修改 state。因为所有的 state 改变都是中心化的并且是按照严格的顺序一步一步执行的。没有细小的竞争条件需要注意。actions 作为一个朴素的 object,它可以被打印,序列化,存储,或为了 debug 或测试来在稍后重放。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

为了展示当前应用的 state 树是如何转变的,你必须书写 pure reducers(纯函数始终返回一个新值,旧值将被作为记录存储,这样就可是实现 time travel,起到记录 state 树变化的目的)。

Reducer 是一个可接受一个先前 stateaction纯函数,并且返回一个修改后的新的 state 对象。应始终记住返回一个新的 state 对象,而不是修改传入的 state 对象。你可以在你的 app 中创建单个 reducer,随着你的 app 的增长,可将 reducer 分离以用于管理指定 state 树的部分 state。因为 reducer 只是一个函数,那么你可以控制调用这些 reducer 的顺序,传递额外的数据,或者甚至可以为共有的 task(如分页)创建可复用的 reducer

import { combineReducers, createStore } from 'redux'

function visibilityFilter (state = 'SHOW_ALL', action)  {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

function todos (state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]

    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        return index === action.index
          ? Object.assign({}, todo, {
            completed: true
          })
          : todo
      })

    default:
      return state
  }
}

const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
lbwa commented 5 years ago

Actions

Actions 是携带了从 app 发送到 store 的数据载荷。这些载荷是 store唯一 信息来源。你可以使用 store.dispatch() 来发送 actions(即载荷)。

const ADD_TODO = 'ADD_TODO'
// Actions
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Actions 是一个朴素的 JavaScript 对象。Actions 必须有 type 属性以用于指定将要执行的动作(action)。通常应该将 type 定义为字符串常量。一旦你的 app 足够庞大,你可能会想将这些 types 移动到一个单独的模块。

import { ADD_TODO, REMOVE_TODO } from './actionTypes'

关于抽离 action type 的说明

action type 定义到一个单独的文件中并不是必要的步骤,或者仅仅定义 action type 中的一部分。对于规模较小的项目,可能直接使用字符串字面量来定义 action type 更为简单。然而,这有一些在庞大的 codebases 中显式地声明变量的好处。阅读 Reducing Boilerplate 来获得更多的实践性建议来保持你的 codebase 简洁。

在一个 action 对象上除了 type 属性以外其他的属性,你都可以任意定义。如果你感兴趣,可阅读 [Flux Standard Action] 来获得如何组建 action 对象的推荐。

Action Creators

Action creators 是创建 action 的函数。很容易将 actionaction creator 概念混为一谈,所以尽可能的使用正确的术语。

Redux 中,action creator 简单返回一个 action:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

传统的 Flux 架构中,action creators 被调用时经常会触发一个 dispatch,如下:

function addTodoWithDispatch(text) {
  const action = {
    type: ADD_TODO,
    text
  }
  dispatch(action)
}

而在 Redux 中并不是这样。相反地,传入结果到 dispatch() 函数中来实现初始化一个 dispatch

dispatch(addTodo(text))
dispatch(completeTodo(index))

可选地,你可以创建一个实现绑定的自动 dispatchaction creator

const boundAddTodo => dispatch(addTodo(text))
const boundCompleteTodo => dispatch(completeTodo(index))

现在你可以直接调用他们:

boundAddTodo(text)
boundCompleteTodo(index)

dispatch 函数在 store 中可直接通过 store.dispatch() 的形式访问,但是更多象 react-redux 中的 connect() 你可以直接调用这些帮手来访问 dispatch。你可以使用 boundActionCreators() 来在一个 dispatch() 函数中绑定多个 action creators

action creator 可以是 异步 的,并可有 副作用。你可阅读 提高指南中的async actions来学习如何监听 AJAX 响应,并结合 action creators 到异步控制流中。

示例代码

actions.js

/**
 * action types
 */
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/**
 * other constants
 */
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/**
 * action creators
 */
export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}
lbwa commented 5 years ago

Reducers

Reducers 表明了应用的 state 如何响应 actions 的修改,并发送到 store 中。记住 actions 只描述了发生了什么,但是没有描述应用的 state 是如何发生改变的。

设计 state 的结构

Redux 中,所有的应用 state 都是被作为单个对象存储的。

在一个足够复杂的 App 中,你可能希望相互引用他们。我们建议你保持 state 尽可能的没有嵌套。保证每一个数据都有一个 ID 作为 key 在一个对象中存储。并且可使用该 ID 来从其他的数据中来引用该数据。这种方案在 normalizr's 文档中有更多的细节。例如,在 state 中使用 todosById: { id -> todo }todos:array<id> 是一个非常好的方案(译者:利于嵌套数据的查找)。

Handling Actions

Reducer 是一个接收 action 和将要被修改的 state,并且返回新的修改后的 state

;(previousState, action) => newState

之所以叫 reducer 是因为它是一种会当作 Array.prototype.reduce(reducer, ?initialValue) 的参数的函数。reducer 保持纯洁性是非常重要的。下面是你永远都 不应该 在一个 reducer 内部做的事情(即定义纯函数的最低要求):

我们将在 [advanced walkthrough] 谈论如何执行函数副作用。而现在,只需要记住 reducer 必须 是纯洁的。 给定相同的阐述,reducer 因该计算下一个 state 并返回它。没有惊喜,没有副作用,没有 API 调用。没有值的修改(赋值),只有单纯的计算

有了这些概念,让我们开始编写我们的 reducer,并逐步理解之前定义的 actions

我们首先指定初始 state。因为 Redux 在初始传入一个 undefined state 来调用我们的 reducer,所以这是我们返回初始 state 的机会:

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    // handle `SET_VISIBILITY_FILTER`
    case SET_VISIBILITY_FILTER:
      // 为了保持函数纯洁性,始终通过 return 一个新值来达到修改原值的目的
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })

    default:
      return state
  }

  // For now, don't handle any actions
  // and just return the state given to us.
  return state
}

值得注意的是:

  1. 我们没有修改 state。我们用 Object.assign 创建了一个 state 的副本。调用 Object.assign(state, { visibilityFilter: action.filter }) 是不正确的:因为会修改第一个参数,即 state。你 必须 提供一个空的对象作为第一个参数。你同样可使用 扩展运算符 即 `{ ...state, ...newState } 来实现同样的目的。

  2. 我们默认返回之前的 state。 对于任意未知的 action 返回先前传入的 state 对象是十分重要的。

Handling More Actions

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

因为我们想在不发生重排的情况下修改指定的项的值,我们就必须创建一个具有相同项的数组除了特定项。如果你发现你经常书写这样的操作,推荐使用像 immutability-helperupdeep 甚至像 Immutable 这样原生支持深度更新的库。只需要记住除非你首先克隆了 state 对象,否则永远不要在 state 对象内部做任何赋值操作。

lbwa commented 5 years ago

Store

在之前章节,我们 定义了 actions 用来表述 发生了什么 这一事实,并 定义了reducers 表述了根据这些 actions 做出的更新改变。

store 将是一个和 actionsreducers 组合在一起的对象。store 有如下职责:

在一个 Redux 应用中,应该始终只有单个 store 当你想分离数据处理逻辑时,你应使用 reducers composition 而不是创建多个 store

如果你有一个 reducer,那么创建一个 store 也是非常简单的。在之前的章节中,我们使用 combineReducers() 来合并多个 reducers 为一个。我们现在引入之前的 root reducers,并将它传入 createStore() 中。

import { createStore } from 'redux'
import todoApp from './reducers'

const store = createStore(todoApp)

可选地,你可向 createStore() 传入第二个参数作为初始 state。这对于客户端 state 保持与服务端 Redux 应用的 state 是非常有帮助的。

const store = createStore(todoApp, window.STATE_FROM_SERVER)

分发 Actions

import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// 打印初始 state
console.log(store.getState())

// 每次 state 改变,都会打印新的 state
// 注意 subscribe() 返回的一个函数可用于注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))

// 分发一些 actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 的更新
unsubscribe()
lbwa commented 5 years ago

Flux standard action

Flux standard action

Example

一个基础的 Flux Standard Action:

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'  
  }
}

一个展示了一个错误的的 Flux standard action, 类似于一个 rejected 的 Promise 实例。

{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true
}

Actions

一个 action 必须

一个 action 可能

一个 action 必须不能包含typepayloaderrormeta 以外的属性。

type

一个 actiontypeconsumer 标识了被触发的 action 的实质(意译,即是哪一类 action 的意思)。type 是一个字符串常量。如果两个 type 相同,那么他们 必须 严格相等(===)。

payload

可选属性 payload 可以是任意类型值。它表示了 action 的载荷。任何关于 actiontypeaciton 的状态的信息都应该是 payload 属性的组成部分。

按照惯例,若 errortrue 值,payload 应该 是一个 error 对象。这类似与一个携带一个 error 对象的 rejected 状态的 Promise 实例。

error

若一个 aciton 表示一个 error,那么可选属性 error 可以 被设置为 true

一个 包含 error 属性为 trueaction 类似与一个 rejected 状态的 Promise 实例。按照惯例,payload 应该 是一个 error 对象。

error 属性有任何非 true 值,包含 undefinednull,那么这个 action不应该 被解读为一个 error

meta

The optional meta property MAY be any type of value. It is intended for any extra information that is not part of the payload.

可选属性 meta 可以 是任意类型值。它表示任何不属于 payload 属性的额外信息。

Utility functions

The module flux-standard-action is available on npm. It exports a few utility functions.

lbwa commented 5 years ago

异步的 Action Creators

我们如何使用定义好的同步 action creators 来和网络请求一起工作?标准的做法是使用 Redux Thunk 中间件。该中间件有一个独立的 package 称为 redux-thunk。我们稍后会解释中间件是如何工作的。现在,只有一件重要的事是需要读者知道的:通过使用指定的中间件,一个 action creator 可以返回一个函数而不是一个 action 对象。在这种方法中,action creator 成为了一个 thunk

Thunk 在计算机科学中,一个 thunk 是指一个用于一个子程序向另外一个子程序注入额外计算结果的子程序。Thunk 主要用于注入直至需要该结果的计算结果,或在其他子程序的开端或结尾插入操作。它同时可简单地被理解为一个不带参数的函数,直到该函数被调用。

当一个 action creator 返回一个函数,那么该函数将被 redux thunk 中间件执行。该函数不必是纯函数;因此该函数允许有包含执行异步 API 调用的副作用。该函数也可分发 action —— 就像我们之前定义的同步的 actions 一样。

import fetch from 'cross-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
  return {
    type: REQUEST_POSTS,
    subreddit
  }
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
  return {
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
  return {
    type: INVALIDATE_SUBREDDIT,
    subreddit
  }
}

// 来见识我们第一个 thunk action creator!
// 尽管现在的函数内部不同于先前的 action creator,但你仍可以就像先前的 action creator
// 的调用方式来调用异步的 action creator:store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {
  // Thunk middleware knows how to handle functions.
  // Thunk 中间件知道如何处理这些异步 action creator 函数。

  // It passes the dispatch method as an argument to the function,
  // 中间件会把 dispatch 方法作为本函数的一个参数传入,

  // thus making it able to dispatch actions itself.
  // 进而使得它自己能够分发对应的 action。

  return function(dispatch) {
    // First dispatch: the app state is updated to inform
    // 第一个 dispatch; 更新 App 状态以通知 API 调用开始。
    // that the API call is starting.

    dispatch(requestPosts(subreddit))

    // The function called by the thunk middleware can return a value,
    // 通过 Thunk 中间件调用的函数可返回一个值,

    // that is passed on as the return value of the dispatch method.
    // 该值可作为 dispatch 方法的返回值传递。

    // In this case, we return a promise to wait for.
    // 在这种情况下,我们返回一个处于等待状态的 Promise 实例

    // This is not required by thunk middleware, but it is convenient for us.
    // 对于 thunk 中间件,返回 Promise 实例并不是必须的,但这可为我们提供很多的便利。

    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        // Do not use catch, because that will also catch
        // 不要使用 catch 函数

        // any errors in the dispatch and resulting render,
        // 因为所有的错误会在 dispatch 和 结果渲染中被处理,

        // causing a loop of 'Unexpected batch number' errors.
        // 使用 catch 将导致一个 `Unexpected batch number` 的错误
        // https://github.com/facebook/react/issues/6895
        error => console.log('An error occurred.', error)
      )
      .then(json =>
        // We can dispatch many times!
        // 我们可以 dispatch 很多次!

        // Here, we update the app state with the results of the API call.
        // 在这,我们随着 API 调用的返回结果来更新 app 的 state

        dispatch(receivePosts(subreddit, json))
      )
  }
}

Mind map