Giter Club home page Giter Club logo

redux-simple-tutorial's Introduction

Redux 简明教程

原文链接(保持更新):https://github.com/kenberkeley/redux-simple-tutorial

写在前面

本教程深入浅出,配套 简明教程、进阶教程(源码精读)以及文档注释丰满的 Demo 等一条龙服务

§ 为什么要用 Redux

当然还有 FluxRefluxMobx 等状态管理库可供选择

抛开需求讲实用性都是耍流氓,因此下面由我扮演您那可亲可爱的产品经理

⊙ 需求 1:在控制台上记录用户的每个动作

不知道您是否有后端的开发经验,后端一般会有记录访问日志的中间件
例如,在 Express 中实现一个简单的 Logger 如下:

var loggerMiddleware = function(req, res, next) {
  console.log('[Logger]', req.method, req.originalUrl)
  next()
}
...
app.use(loggerMiddleware)

每次访问的时候,都会在控制台中留下类似下面的日志便于追踪调试:

[Logger] GET  /
[Logger] POST /login
[Logger] GET  /user?uid=10086
...

如果我们把场景转移到前端,请问该如何实现用户的动作跟踪记录?
我们可能会这样写:

/** jQuery **/
$('#loginBtn').on('click', function(e) {
  console.log('[Logger] 用户登录')
  ...
})
$('#logoutBtn').on('click', function() {
  console.log('[Logger] 用户退出登录')
  ...
})

/** MVC / MVVM 框架(这里以纯 Vue 举例) **/
methods: {
  handleLogin () {
    console.log('[Logger] 用户登录')
    ...
  },
  handleLogout () {
    console.log('[Logger] 用户退出登录')
    ...
  }
}

上述 jQuery 与 MV* 的写法并没有本质上的区别
记录用户行为代码的侵入性极强,可维护性与扩展性堪忧

⊙ 需求 2:在上述需求的基础上,记录用户的操作时间

哼!最讨厌就是改需求了,这种简单的需求难道不是应该一开始就想好的吗?
呵呵,如果每位产品经理都能一开始就把需求完善好,我们就不用加班了好伐

显然地,前端的童鞋又得一个一个去改(当然 编辑器 / IDE 都支持全局替换):

/** jQuery **/
$('#loginBtn').on('click', function(e) {
  console.log('[Logger] 用户登录', new Date())
  ...
})
$('#logoutBtn').on('click', function() {
  console.log('[Logger] 用户退出登录', new Date())
  ...
})

/** MVC / MVVM 框架(这里以 Vue 举例) **/
methods: {
  handleLogin () {
    console.log('[Logger] 用户登录', new Date())
    ...
  },
  handleLogout () {
    console.log('[Logger] 用户退出登录', new Date())
    ...
  }
}

而后端的童鞋只需要稍微修改一下原来的中间件即可:

var loggerMiddleware = function(req, res, next) {
  console.log('[Logger]', new Date(), req.method, req.originalUrl)
  next()
}
...
app.use(loggerMiddleware)

⊙ 需求 3:正式上线的时候,把控制台中有关 Logger 的输出全部去掉

难道您以为有了 UglifyJS,配置一个 drop_console: true 就好了吗?图样图森破,拿衣服!
请看清楚了,仅仅是去掉有关 Logger 的 console.log,其他的要保留哦亲~~~
于是前端的童鞋又不得不乖乖地一个一个注释掉(当然也可以设置一个环境变量判断是否输出,甚至可以重写 console.log

而我们后端的童鞋呢?只需要注释掉一行代码即可:// app.use(loggerMiddleware),真可谓是不费吹灰之力

⊙ 需求 4:正式上线后,自动收集 bug,并还原出当时的场景

收集用户报错还是比较简单的,利用 window.error 事件,然后根据 Source Map 定位到源码(但一般查不出什么)

但要完全还原出当时的使用场景,几乎是不可能的。因为您不知道这个报错,用户是怎么一步一步操作得来的
就算知道用户是如何操作得来的,但在您的电脑上,测试永远都是通过的(不是我写的程序有问题,是用户用的方式有问题)

相对地,后端的报错的收集、定位以及还原却是相当简单。只要一个 API 有 bug,那无论用什么设备访问,都会得到这个 bug
还原 bug 也是相当简单:把数据库备份导入到另一台机器,部署同样的运行环境与代码。如无意外,bug 肯定可以完美重现

在这个问题上拿后端跟前端对比,确实有失公允。但为了鼓吹 Redux 的优越,只能勉为其难了

实际上 jQuery / MV* 中也能实现用户动作的跟踪,用一个数组往里面 push 用户动作即可
但这样操作的意义不大,因为仅仅只有动作,无法反映动作前后,应用状态的变动情况

※ 小结

为何前后端对于这类需求的处理竟然大相径庭?后端为何可以如此优雅?
原因在于,后端具有统一的入口统一的状态管理(数据库),因此可以引入中间件机制统一实现某些功能

多年来,前端工程师忍辱负重,操着卖白粉的心,赚着买白菜的钱,一直处于程序员鄙视链的底层
于是有大牛就把后端 MVC 的开发思维搬到前端,将应用中所有的动作与状态都统一管理,让一切有据可循

使用 Redux,借助 Redux DevTools 可以实现出“华丽如时光旅行一般的调试效果”
实际上就是开发调试过程中可以撤销与重做,并且支持应用状态的导入和导出(就像是数据库的备份)
而且,由于可以使用日志完整记录下每个动作,因此做到像 Git 般,随时随地恢复到之前的状态

由于可以导出和导入应用的状态(包括路由状态),因此还可以实现前后端同构(服务端渲染)
当然,既然有了动作日志以及动作前后的状态备份,那么还原用户报错场景还会是一个难题吗?

§ Store

首先要区分 storestate

state 是应用的状态,一般本质上是一个普通对象
例如,我们有一个 Web APP,包含 计数器 和 待办事项 两大功能
那么我们可以为该应用设计出对应的存储数据结构(应用初始状态):

/** 应用初始 state,本代码块记为 code-1 **/
{
  counter: 0,
  todos: []
}

store 是应用状态 state 的管理者,包含下列四个函数:

  • getState() # 获取整个 state
  • dispatch(action) # ※ 触发 state 改变的【唯一途径】※
  • subscribe(listener) # 您可以理解成是 DOM 中的 addEventListener
  • replaceReducer(nextReducer) # 一般在 Webpack Code-Splitting 按需加载的时候用

二者的关系是:state === store.getState()

Redux 规定,一个应用只应有一个单一的 store,其管理着唯一的应用状态 state
Redux 还规定,不能直接修改应用的状态 state,也就是说,下面的行为是不允许的:

var state = store.getState()
state.counter = state.counter + 1 // 禁止在业务逻辑中直接修改 state

若要改变 state,必须 dispatch 一个 action,这是修改应用状态的不二法门

现在您只需要记住 action 只是一个包含 type 属性的普通对象即可
例如 { type: 'INCREMENT' }

上面提到,state 是通过 store.getState() 获取,那么 store 又是怎么来的呢?
想生成一个 store,我们需要调用 Redux 的 createStore

import { createStore } from 'redux'
...
const store = createStore(reducer, initialState) // store 是靠传入 reducer 生成的哦!

现在您只需要记住 reducer 是一个 函数,负责更新并返回一个新的 state
initialState 主要用于前后端同构的数据同步(详情请关注 React 服务端渲染)

§ Action

上面提到,action(动作)实质上是包含 type 属性的普通对象,这个 type 是我们实现用户行为追踪的关键
例如,增加一个待办事项 的 action 可能是像下面一样:

/** 本代码块记为 code-2 **/
{
  type: 'ADD_TODO',
  payload: {
    id: 1,
    content: '待办事项1',
    completed: false
  }
}

当然,action 的形式是多种多样的,唯一的约束仅仅就是包含一个 type 属性罢了
也就是说,下面这些 action 都是合法的:

/** 如下都是合法的,但就是不够规范 **/
{
  type: 'ADD_TODO',
  id: 1,
  content: '待办事项1',
  completed: false
}

{
  type: 'ADD_TODO',
  abcdefg: {
    id: 1,
    content: '待办事项1',
    completed: false
  }
}

虽说没有约束,但最好还是遵循规范

如果需要新增一个代办事项,实际上就是将 code-2 中的 payload “写入”state.todos 数组中(如何“写入”?在此留个悬念):

/** 本代码块记为 code-3 **/
{
  counter: 0,
  todos: [{
    id: 1,
    content: '待办事项1',
    completed: false
  }]
}

刨根问底,action 是谁生成的呢?

⊙ Action Creator

Action Creator 可以是同步的,也可以是异步的

顾名思义,Action Creator 是 action 的创造者,本质上就是一个函数,返回值是一个 action对象
例如下面就是一个 “新增一个待办事项” 的 Action Creator:

/** 本代码块记为 code-4 **/
var id = 1
function addTodo(content) {
  return {
    type: 'ADD_TODO',
    payload: {
      id: id++,
      content: content, // 待办事项内容
      completed: false  // 是否完成的标识
    }
  }
}

将该函数应用到一个表单(假设 store 为全局变量,并引入了 jQuery ):

<!-- 本代码块记为 code-5 -->
<input type="text" id="todoInput" />
<button id="btn">提交</button>

<script>
$('#btn').on('click', function() {
  var content = $('#todoInput').val() // 获取输入框的值
  var action = addTodo(content) // 执行 Action Creator 获得 action
  store.dispatch(action) // 改变 state 的不二法门:dispatch 一个 action!!!
})
</script>

在输入框中输入 “待办事项2” 后,点击一下提交按钮,我们的 state 就变成了:

/** 本代码块记为 code-6 **/
{
  counter: 0,
  todos: [{
    id: 1,
    content: '待办事项1',
    completed: false
  }, {
    id: 2,
    content: '待办事项2',
    completed: false
  }]
}

通俗点讲,Action Creator 用于绑定到用户的操作(点击按钮等),其返回值 action 用于之后的 dispatch(action)

刚刚提到过,action 明明就没有强制的规范,为什么 store.dispatch(action) 之后,
Redux 会明确知道是提取 action.payload,并且是对应写入到 state.todos 数组中?
又是谁负责“写入”的呢?悬念即将揭晓...

§ Reducer

Reducer 必须是同步的纯函数

用户每次 dispatch(action) 后,都会触发 reducer 的执行
reducer 的实质是一个函数,根据 action.type更新 state 并返回 nextState
最后会用 reducer 的返回值 nextState 完全替换掉原来的 state

注意:上面的这个 “更新” 并不是指 reducer 可以直接对 state 进行修改
Redux 规定,须先复制一份 state,在副本 nextState 上进行修改操作
例如,可以使用 lodash 的 cloneDeep,也可以使用 Object.assign / map / filter/ ... 等返回副本的函数

在上面 Action Creator 中提到的 待办事项的 reducer 大概是长这个样子 (为了容易理解,在此不使用 ES6 / Immutable.js):

/** 本代码块记为 code-7 **/
var initState = {
  counter: 0,
  todos: []
}

function reducer(state, action) {
  // ※ 应用的初始状态是在第一次执行 reducer 时设置的 ※
  if (!state) state = initState
  
  switch (action.type) {
    case 'ADD_TODO':
      var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
      nextState.todos.push(action.payload) 
      return nextState

    default:
    // 由于 nextState 会把原 state 整个替换掉
    // 若无修改,必须返回原 state(否则就是 undefined)
      return state
  }
}

通俗点讲,就是 reducer 返回啥,state 就被替换成啥

§ 总结

  • store 由 Redux 的 createStore(reducer) 生成
  • state 通过 store.getState() 获取,本质上一般是一个存储着整个应用状态的对象
  • action 本质上是一个包含 type 属性的普通对象,由 Action Creator (函数) 产生
  • 改变 state 必须 dispatch 一个 action
  • reducer 本质上是根据 action.type 来更新 state 并返回 nextState函数
  • reducer 必须返回值,否则 nextState 即为 undefined
  • 实际上,state 就是所有 reducer 返回值的汇总(本教程只有一个 reducer,主要是应用场景比较简单)

Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

⊙ Redux 与传统后端 MVC 的对照

Redux 传统后端 MVC
store 数据库实例
state 数据库中存储的数据
dispatch(action) 用户发起请求
action: { type, payload } type 表示请求的 URL,payload 表示请求的数据
reducer 路由 + 控制器(handler)
reducer 中的 switch-case 分支 路由,根据 action.type 路由到对应的控制器
reducer 内部对 state 的处理 控制器对数据库进行增删改操作
reducer 返回 nextState 将修改后的记录写回数据库

§ 最简单的例子 ( 在线演示 )

<!DOCTYPE html>
<html>
<head>
  <script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
</head>
<body>
<script>
/** Action Creators */
function inc() {
  return { type: 'INCREMENT' };
}
function dec() {
  return { type: 'DECREMENT' };
}

function reducer(state, action) {
  // 首次调用本函数时设置初始 state
  state = state || { counter: 0 };

  switch (action.type) {
    case 'INCREMENT':
      return { counter: state.counter + 1 };
    case 'DECREMENT':
      return { counter: state.counter - 1 };
    default:
      return state; // 无论如何都返回一个 state
  }
}

var store = Redux.createStore(reducer);

console.log( store.getState() ); // { counter: 0 }

store.dispatch(inc());
console.log( store.getState() ); // { counter: 1 }

store.dispatch(inc());
console.log( store.getState() ); // { counter: 2 }

store.dispatch(dec());
console.log( store.getState() ); // { counter: 1 }
</script>
</body>
</html>

由上可知,Redux 并不一定要搭配 React 使用。Redux 纯粹只是一个状态管理库,几乎可以搭配任何框架使用
(上述例子连 jQuery 都没用哦亲)

tip

redux-simple-tutorial's People

Contributors

b1f6c1c4 avatar dotcom900825 avatar kenberkeley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-simple-tutorial's Issues

关于文章中applyMiddleware(...middlewares)的问题

/* 降低逼格写法 */
function printStateMiddleware(middlewareAPI) { // 记为【锚点-1】,中间件内可用的 API
  return function (dispatch) {                 // 记为【锚点-2】,传入上级中间件处理逻辑(若无则为原 store.dispatch)

    // 下面记为【锚点-3】,整个函数将会被传到下级中间件(如果有的话)作为它的 dispatch 参数
    return function (action) { // <---------------------------------------------- 这货就叫做【中间件处理逻辑哥】吧
      console.log('state before dispatch', middlewareAPI.getState())
  
      var returnValue = dispatch(action) // 还记得吗,dispatch 的返回值其实还是 action
  
      console.log('state after dispatch', middlewareAPI.getState())

      return returnValue // 继续传给下一个中间件作为参数 action   我感觉你这句话解释有问题
    }
  }
}

下一个中间件的action在这里是通过dispatch(action)传递的吧,(也就是next(action)),感觉这里有问题。

lodash中的deepClone不存在

你好,根据你这个文档,我学着敲了下代码做为练习,却发现lodash中没有deepClone方法,而有cloneDeep方法,不知道是不是用的lodash版本不同,我用的是lodash4.15.0,redux3.5.2。

so dirty

用户您好,请珍惜您的gayhub账号

感谢信

在作者深入浅出的讲解下, 终于是理清楚了redux的**本质。真的十分感谢!!

这里是不是有点不妥

// 串联各个中间件,为各个中间件传入原 store.dispatch,见【降低逼格写法】的【锚点-2】
dispatch = compose(...chain)(store.dispatch)
应该是给最后的中间件传入原 store.dispatch。其它的中间件并没有使用原 store.dispatch。

createStore 部分有点儿小问题...

// 逐个触发回调函数。这里不缓存数组长度是明智的,原因见【悬念1·解疑】
for (var i = 0; i < listeners.length; i++) {
  listeners[i]()
}

1. 不缓存数组长度和触发回调函数没关系

这样的话,dispatch 后,在逐个执行回调函数的过程中
如果有新增订阅或取消订阅,都在 nextListeners 中操作
让 currentListeners 中的回调函数得以完整地执行

就像你之前说的那样,currentListeners 数组在调用回调函数时并不会变化,新增或取消都在 nextListeners 中操作。

倒是之前都是用 forEach 结果因为 forEach 会对空的子元素进行特殊处理,而改成了 for 循环

2. listeners[i]() 会导致 listeners 作为 this 暴露到外部

所以现在是这样...

for (var i = 0; i < listeners.length; i++) {
  var listener = listeners[i]
  listener()
}

你好,最简单例子里面有一个reduer的**不符合redux的规范,不改变state,而是创造一个新的state

https://github.com/kenberkeley/redux-simple-tutorial#-最简单的例子--在线演示-
`function reducer(state, action) {
// 首次调用本函数时设置初始 state
state = state || { counter: 0 };

switch (action.type) {
case 'INCREMENT':
return { counter: state.counter + 1 };
case 'DECREMENT':
return { counter: state.counter - 1 };
default:
return state; // 无论如何都返回一个 state
}
}`
counter:state.counter+1 应该 state{...state, counter : state.counter+1 }
虽然对于初级者直接修改来的直观些,但必要注明一下

关于combineReducers&compose

不好意思,本来想单独要下联系方式,可是按您说的还是开issue吧。
关于combineReducers&compose 的介绍部分没有看懂有几个疑惑
1.compose 的应用场景一般是什么?我理解的是,这个函数只是在调用方法的执行顺序上有所不同,像Jquery的链式调用,但是compose 是从右向左调用,具体在什么时候应该用这个函数?
2.combineReducers 这个实在没看懂,这个函数是将多个state整合吗?
方便的话说明下具体的在什么时候用到,谢谢

关于compose的一点疑问

现在官方的compose源码中关于reduceRight这段变成了
return funcs.reduce((a, b) => (...args) => a(b(...args)))
reduce不应该是从左到右执行的吗?
为什么这里换了reduce还可以做到compose(f, g, h)(...arg) => f(g(h(...args)))的效果?
感觉这个issue不应该在这提,但是找了一晚上也没弄明白,希望能有人解答.

写的太好了

这是我见过最好的 redux的文章啦,如果能有 和react配合的教程就好啦,

文末enhancer执行顺序图

原 createStore ————


return enhancer1(createStore)(reducer, preloadedState, enhancer2)
|
├———————→ createStore 增强版 1


return enhancer2(createStore1)(reducer, preloadedState, enhancer3)
|
├———————————→ createStore 增强版 1+2


return enhancer3(createStore1+2)(reducer, preloadedState, applyMiddleware(m1,m2,m3))
|
├————————————————————→ createStore 增强版 1+2+3


return appleMiddleware(m1,m2,m3)(createStore1+2+3)(reducer, preloadedState)
|
├——————————————————————————————————→ 生成最终增强版 store

感觉图中enhancer的第二次调用应该去掉吧。因为createStore调用的是compose返回的函数(称func),func被调用让enhancer被串联执行,在这个过程中,enhancer应该只有一次调用,func(createStore) => enhancer(createStore),后面的第二次调用应该是所有enhaner执行完成,返回最终版store,在原始createStore内,执行finalCreateStore(reducer, preloadedState)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.