1

Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。除了和 React 一起用外,还支持其它界面库。 它体小精悍(只有2kB,包括依赖)。

三大原则

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

console.log(store.getState())

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

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

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

使用纯函数来执行修改

为了描述 action 如何改变 state tree ,你需要编写 reducers。

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) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}

import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)

Action

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作

// Action 创建函数
export const ADD_TODO = 'ADD_TODO';
export function addTodo(text) {
  return { type: ADD_TODO, text }
}

// 发起dispatch
dispatch(addTodo(text))

Action 创建函数也可以是异步非纯函数。

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意:

  1. 不要直接修改 state,而是返回新对象
  2. 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。

Store

Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
import { createStore } from 'redux'
import todoApp from './reducers'

const store = createStore(todoApp)

Middleware

在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。

它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // 允许我们 dispatch() 函数
    loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
  )
)

数据流

严格的单向数据流是 Redux 架构的设计核心。这意味着应用中所有的数据都遵循相同的生命周期,遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)。
  2. Redux store 调用传入的 reducer 函数。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

Side Effects: 异步网络请求本地读取 localStorage/Cookie 等外界操作

总结

Redux这种单向数据流的库有很明显的优缺点

可预测性

action创建函数 reducer都是纯函数

stateaction 是简单对象

state 可以使用 immutable 持久化数据

整套流程职责非常清晰,数据可追踪可回溯,能很好保证项目稳定性

可扩展性

通过 middleware 定制 action 的处理,通过 reducer enhancer 扩展 reducer 等等

管理麻烦

redux 的项目通常要分 reducer, action, saga, component 等等,开发中需要来回切换

redux-saga

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。

核心术语

Effect

一个 effect 就是一个 Plain Object JavaScript 对象,包含一些将被 saga middleware 执行的指令。

使用 redux-saga 提供的工厂函数来创建 effect。 举个例子,你可以使用 call(myfunc, 'arg1', 'arg2') 指示 middleware 调用 myfunc('arg1', 'arg2') 并将结果返回给 yield effect 的那个 Generator。

Task

一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task:

function* saga() {
  ...
  const task = yield fork(otherSaga, ...args)
  ...
}

阻塞调用/非阻塞调用

阻塞调用的意思是,Saga 在 yield Effect 之后会等待其执行结果返回,结果返回后才会恢复执行 Generator 中的下一个指令。

非阻塞调用的意思是,Saga 会在 yield Effect 之后立即恢复执行。

function* saga() {
  yield take(ACTION)              // 阻塞: 将等待 action
  yield call(ApiFn, ...args)      // 阻塞: 将等待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
  yield call(otherSaga, ...args)  // 阻塞: 将等待 otherSaga 结束

  yield put(...)                   // 阻塞: 将同步发起 action (使用 Promise.then)

  const task = yield fork(otherSaga, ...args)  // 非阻塞: 将不会等待 otherSaga
  yield cancel(task)                           // 非阻塞: 将立即恢复执行
  // or
  yield join(task)                             // 阻塞: 将等待 task 结束
}

Watcher/Worker

指的是一种使用两个单独的 Saga 来组织控制流的方式。

  • Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。
  • Worker: 处理 action 并结束它。
function* watcher() {
  while(true) {
    const action = yield take(ACTION)
    yield fork(worker, action.payload)
  }
}

function* worker(payload) {
  // ... do some stuff
}

Saga 辅助函数

redux-saga 提供了一些辅助函数,包装了一些内部方法,用来在一些特定的 action 被发起到 Store 时派生任务。

让我们通过常见的 AJAX 例子来演示一下。每次点击 Fetch 按钮时,我们发起一个 FETCH_REQUESTED 的 action。 我们想通过启动一个从服务器获取一些数据的任务,来处理这个 action。

首先我们创建一个将执行异步 action 的任务:

import { call, put } from 'redux-saga/effects'

export function* fetchData(action) {
   try {
      // 发起请求
      const data = yield call(Api.fetchUser, action.payload.url);
      // 创建action
      yield put({type: "FETCH_SUCCEEDED", data});
   } catch (error) {
      yield put({type: "FETCH_FAILED", error});
   }
}

然后在每次 FETCH_REQUESTED action 被发起时启动上面的任务。

import { takeEvery } from 'redux-saga'

function* watchFetchData() {
  yield* takeEvery('FETCH_REQUESTED', fetchData)
}

还有很多不同作用的辅助函数

  • takeEvery(pattern, saga, ...args)
  • takeEvery(channel, saga, ...args)
  • takeLatest(pattern, saga, ..args)
  • takeLatest(channel, saga, ..args)
  • takeLeading(pattern, saga, ..args)
  • takeLeading(channel, saga, ..args)
  • throttle(ms, pattern, saga, ..args)

声明式 Effects

redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给 middleware 的指令以执行某些操作

举个例子,假设我们有一个监听 PRODUCTS_REQUESTED action 的 Saga。每次匹配到 action,它会启动一个从服务器上获取产品列表的任务。

import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'

function* watchFetchProducts() {
  yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}

假设我们想测试上面的 generator:

const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 我们期望得到什么?

我们想要检查 generator yield 的结果的第一个值。在我们的情况里,这个值是执行 Api.fetch('/products') 这个 Promise 的结果。 在测试过程中,执行真正的服务(real service)是一个既不可行也不实用的方法,所以我们必须 模拟(mock) Api.fetch 函数。 也就是说,我们需要将真实的函数替换为一个假的,这个假的函数并不会真的发送 AJAX 请求而只会检查是否用正确的参数调用了 Api.fetch

实际上我们需要的只是保证 fetchProducts 任务 yield 一个调用正确的函数,并且函数有着正确的参数。

相比于在 Generator 中直接调用异步函数,我们可以仅仅 yield 一条描述函数调用的信息。也就是说,我们将简单地 yield 一个看起来像下面这样的对象:

// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']  
  }
}

这样的话,在测试 Generator 时,所有我们需要做的就是,将 yield 后的对象作一个简单的 deepEqual 来检查它是否 yield 了我们期望的指令

出于这样的原因,redux-saga 提供了一个不一样的方式来执行异步调用。

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

现在我们不立即执行异步调用,相反,call 创建了一条描述结果的信息。就像在 Redux 里你使用 action 创建器,创建一个将被 Store 执行的、描述 action 的纯文本对象。 call 创建一个纯文本对象描述函数调用。redux-saga middleware 确保执行函数调用并在响应被 resolve 时恢复 generator。

这让你能容易地测试 Generator,就算它在 Redux 环境之外。因为 call 只是一个返回纯文本对象的函数而已。

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

还有很多不同作用的辅助函数Effect 创建器

  • take(pattern)
  • take.maybe(pattern)
  • take(channel)
  • take.maybe(channel)
  • put(action)
  • put.resolve(action)
  • put(channel, action)
  • call(fn, ...args)
  • call([context, fn], ...args)
  • call([context, fnName], ...args)
  • apply(context, fn, args)
  • cps(fn, ...args)
  • cps([context, fn], ...args)
  • fork(fn, ...args)
  • fork([context, fn], ...args)
  • spawn(fn, ...args)
  • spawn([context, fn], ...args)
  • join(task)
  • join(...tasks)
  • cancel(task)
  • cancel(...tasks)
  • cancel()
  • select(selector, ...args)
  • actionChannel(pattern, [buffer])
  • flush(channel)
  • cancelled()
  • setContext(props)
  • getContext(prop)

Dispatch Actions

假设每次保存之后,我们想发起一些 action 通知 Store 数据获取成功了

//...

function* fetchProducts(dispatch)
  const products = yield call(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}

与我们在上一节中看到的从 Generator 内部直接调用函数,有着相同的缺点。如果我们想要测试 fetchProducts 接收到 AJAX 响应之后执行 dispatch, 我们还需要模拟 dispatch 函数。

我们需要同样的声明式的解决方案。只需创建一个对象来指示 middleware 我们需要发起一些 action,然后让 middleware 执行真实的 dispatch。 这种方式我们就可以同样的方式测试 Generator 的 dispatch:只需检查 yield 后的 Effect,并确保它包含正确的指令。

redux-saga 为此提供了另外一个函数 put,这个函数用于创建 dispatch Effect。

import { call, put } from 'redux-saga/effects'
//...

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // 创建并 yield 一个 dispatch Effect
  yield put({ type: 'PRODUCTS_RECEIVED', products })
}

现在,我们可以像上一节那样轻易地测试 Generator:

import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 期望一个 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 创建一个假的响应对象
const products = {}

// 期望一个 dispatch 指令
assert.deepEqual(
  iterator.next(products).value,
  put({ type: 'PRODUCTS_RECEIVED', products }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)

现在我们通过 Generator 的 next 方法来将假的响应对象传递到 Generator。在 middleware 环境之外, 我们可完全控制 Generator,通过简单地模拟结果并还原 Generator,我们可以模拟一个真实的环境。 相比于去模拟函数和窥探调用(spying calls),模拟数据要简单的多。

错误处理

我们假设远程读取因为某些原因失败了,API 函数 Api.fetch 返回一个被拒绝(rejected)的 Promise。

我们希望通过在 Saga 中发起 PRODUCTS_REQUEST_FAILED action 到 Store 来处理那些错误。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

// ...

function* fetchProducts() {
  try {
    const products = yield call(Api.fetch, '/products')
    yield put({ type: 'PRODUCTS_RECEIVED', products })
  }
  catch(error) {
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
  }
}

为了测试故障案例,我们将使用 Generator 的 throw 方法。

import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 期望一个 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 创建一个模拟的 error 对象
const error = {}

// 期望一个 dispatch 指令
assert.deepEqual(
  iterator.throw(error).value,
  put({ type: 'PRODUCTS_REQUEST_FAILED', error }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })"
)

我们传递一个模拟的 error 对象给 throw,这会引发 Generator 中断当前的执行流并执行捕获区块(catch block)。

你也可以让你的 API 服务返回一个正常的含有错误标识的值。例如, 你可以捕捉 Promise 的拒绝操作,并将它们映射到一个错误字段对象。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

function fetchProductsApi() {
  return Api.fetch('/products')
    .then(response => ({ response }))
    .catch(error => ({ error }))
}

function* fetchProducts() {
  const { response, error } = yield call(fetchProductsApi)
  if (response)
    yield put({ type: 'PRODUCTS_RECEIVED', products: response })
  else
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}

一个登录流程例子

import { take, put, call, fork, cancel } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  } finally {
      // finally 区块执行在任何类型的完成上(正常的 return, 错误, 或强制取消), 返回该 generator 是否已经被取消
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    // fork return a Task object
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT') yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}

loginFlow

  1. 监听 LOGIN_REQUEST 等待发起action
  2. 从外部获得参数,以 非阻塞调用 的形式执行请求
  3. 监听 LOGOUTLOGIN_ERROR 等待发起
  4. 如果属于 LOGOUT 则取消上面请求
  5. 两种发起都会执行清理流程

authorize

调用authorize请求

  • 成功

    1. 发起LOGIN_SUCCESS保存数据
    2. 执行Api.storeItem
    3. 返回token
  • 错误: 发起LOGIN_ERROR
  • 增加取消逻辑

总结

  • 功能强大,多种辅助函数和API,通过这些可以把所有业务逻辑放到saga,优雅而强大,并且保持 Redux 的纯粹
  • 可测试性,可以另辟蹊跷达到功能测试的效果
  • 创建复杂,灵活细粒化的写法提高编写和理解门槛

Dva

dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。( Inspired by elm and choo. )

Model

他最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起

比如:

app.model({
  namespace: 'products',
  state: {
    list: [],
    loading: false,
  },
  subscriptions: [
    function(dispatch) {
      dispatch({type: 'products/query'});
    },
  ],
  effects: {
    ['products/query']: function*() {
      yield call(delay(800));
      yield put({
        type: 'products/query/success',
        payload: ['ant-tool', 'roof'],
      });
    },
  },
  reducers: {
    ['products/query'](state) {
      return { ...state, loading: true, };
    },
    ['products/query/success'](state, { payload }) {
      return { ...state, loading: false, list: payload };
    },
  },
});

在有 dva 之前,我们通常会创建 sagas/products.js, reducers/products.jsactions/products.js,然后在这些文件之间来回切换。

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,

  • 如果是同步行为会直接通过 Reducers 改变 State
  • 如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State

State

State 表示 Model 的状态数据,可以是任意类型值

操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。

Action

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数

dispatch({
  type: 'add',
});

dispatch 函数

dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

dispatch({
  type: 'user/add', // 如果在 model 外调用,需要添加 namespace
  payload: {}, // 需要传递的信息
});

Reducer

$$ type Reducer<S, A> = (state: S, action: A) => S $$

接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。

Effect

Effect 被称为副作用,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。

Subscription

$$ ({ dispatch, history }, done) => unlistenFunction $$

Subscriptions 是一种从 获取数据的方法,它来自于 elm。在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。

import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent({dispatch}) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

官网说的比较笼统,实际上它的流程大概如下

  1. key的名称没有任何约束,只是用于在保存,最大作用用来取消监听
  2. dispatch只能作用当前model所在的reducereffects
  3. 只会在调用 app.start() 的时候,遍历所有 model 中的 subscriptions 执行一遍。
  4. 配置的函数需要返回一个函数,该函数应该用来取消订阅的该数据源。调用app.unmodel()执行

Dva 图解

最常见的 Web 类示例之一: TodoList = Todo list + Add todo button

图解一: React 表示法

按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是 <App/>

<TodoList/> <Todo/> 以及<AddTodoBtn/> 本身不维持任何 state, 完全由父节点<App/> 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component

图解二: Redux 表示法

React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store

与图一相比, 几个明显的改进点:

  1. 状态及页面逻辑从 <App/>里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer
  2. <TodoList/><AddTodoBtn/>都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新
  3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能

这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好

图解三: 加入 Saga

  1. 点击创建 Todo 的按钮, 发起一个 type = addTodo 的 action
  2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type = addTodoSucc 的 action, 提示创建成功, 反之则发送 type = addTodoFail 的 action 即可

图解四: Dva 表示法

Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面
  2. 增加了一个 Subscriptions, 用于收集其他来源的 action
  3. model 写法很简约, 类似于 DSL 或者 RoR
app.model({
  namespace: 'count',
  state: {
    record: 0,
    current: 0,
  },
  reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return { 
          ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  },
});

Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果