zoukankan      html  css  js  c++  java
  • Redux 状态管理

    1,Redux 

    单一对象,单一store状态树的形式存储数据。

    多个reducer来编辑action 通过action对象修改 store,共同维护一个根store。

    redux就是纯函数,纯函数,纯函数,重要的事情说三遍。

    纯函数作为reducer也就是action返回新的state,更新state,这个是约定。

    中间件的嵌套要保证最后一个中间件是返回的state对象,然后用这些函数处理并返回对象。

    2,Redux适应的场景

    单页应用过于复杂,需要管理state,

    包括服务区响应、本地数据、待提交数据,UI状态,路由状态,标签状态等。

    如果单纯用state开发复杂度会增大。。

    在加上 更新调优、服务端渲染、路由跳转前调用接口等等。。复杂。。

    复杂主要的两个原因是:变化和异步,需要redux优化一下。

    redux的三大原则试图让state的变化可预测

    redux实践举例 

    import { createStore } from 'redux';
    
    /*
     * 这是一个 reducer,形式为 (state, action) => state 的纯函数。
     * 描述了 action 如何把 state 转变成下一个 state。
     */
    function counter (state=0, action) {
        switch (action.type) {
            case 'INCREMENT':
                return state + 1;
            default:
                return state;
        }
    }
    
    // 创建store数据存储对象,提供3个api: getState  subScribe dispatch
    let store = createStore(counter);
    
    // 手动监听 订阅更新,绑定在视图层
    store.subscribe(() => {
        console.log(store.getState());
    });
    
    // 改变内部state 的唯一方法是触发dispatch一个action
    // action 可以被序列化,或者存储下来方便追溯
    
    store.dispatch({ type: 'INCREMENT' });
    // 要做的修改变成了一个对象,这个对象就是action
    // 写函数编辑这个action,得到想要的store,这个函数就是reducer !
    
    // 只有 一个store 可以有多个reducer,想react有一个root根组件和其他子组件
    
    
    export default store;

    3,核心概念

    举例,可能的state对象是这样,一个对象,包含一个对象数组的数据。

    {
      todos: [{
        text: 'Eat food',
        completed: true
      }, {
        text: 'Exercise',
        completed: false
      }],
      visibilityFilter: 'SHOW_COMPLETED'
    }

    规定:发起action来修改state值,有了规定就。

    action就是普通的js对象,举例说明:

    { type: 'ADD_TODO', text: 'Go to swimming pool' }
    { type: 'TOGGLE_TODO', index: 1 }
    { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

    一定有一个type,然后还有对应的值

    用 reducer函数联系 action 和 state,并返回新是state。

    这样的 reducer 一般可能有多个。

    // 过滤,传参是 state action
    function visibilityFilter(state = 'SHOW_ALL', action) {
      if (action.type === 'SET_VISIBILITY_FILTER') {
        return action.filter;
      } else {
        return state;
      }
    }
    
    // 可以修改state并返回
    // action上获取条件,改 state并返回
    function todos(state = [], action) {
      switch (action.type) {
      case 'ADD_TODO':
        return state.concat([{ text: action.text, completed: false }]);
      case 'TOGGLE_TODO':
        return state.map((todo, index) =>
          // 用action中的条件,改state
          action.index === index ?
            { text: todo.text, completed: !todo.completed } :
            todo
       )
      default:
        return state;
      }
    }

    上面这两个就是 reducer。

    再开发一个reducer,像下面这样:

    function todoApp(state = {}, action) {
      return {
        todos: todos(state.todos, action),
        visibilityFilter: visibilityFilter(state.visibilityFilter, action)
      };
    }

    其实是一个函数,返回的是对象,包含了之前声明的reducer。

    4,3大原则 (继续重复这3大原则)

    4.1 单一的数据源

    整个应用的state保持在一个 object tree 中,并且整个 object tree 只存在于一个 store 中。(这样同构应用开发会容易?为啥?)。

    来自客户端的数据可以很容易的序列化到客户端。加快开发速度。 

    单一的state tree ,像撤销和重做的操作更容易

    4.2 State是只读

    只能通过触发 action 的方式改变state值。

    这样视图和网络都不能直接修改state,所有修改都被集中处理,且按照one  by one 的顺序执行。

    因此不会出现 race condition竞争条件。

    action是普通对象,所有可以被打印、序列化、存储、后期调试或测试时放出来。

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

    4.3  使用纯函数进行修改

    reducer 是一些纯函数,它接收之前的state和action,随着应用变大会有很多 reducer。可以这样写。

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

    5,先前技术

    先不急猥琐发育。对比其他框架。

    看下redux之前的数据管理技术方案。

    Flux

    redux的灵感来源Flux的几个特性。redux规定将模型的更新集中于一个特定的层。

    redux和flux都不允许直接修改数据。

    (state, action) => state  ,这点两个框架一样。

    支持用纯函数,如果是不纯的函数,时间旅行、记录/回放或热加载不可实现。

    Elm

    是一种编程语言,受到Haskell语言启发,遵循 (state, action) => state, 是 modal-view-update 的架构。

    Elm 语言具有更好的 执行纯度、静态类型、不可变性、action、匹配模式等方面更具优势。

    Immutable

    这是一个不可变数据结构的库文件。看过react不可变数据的都知道。

    和 redux对接的也很好

    和backbone不同,backbone是可变数据

    Baobab

    Baobab是另一个优秀的库,实现了数据的不可变特性的api,用于更新纯js对象但方式和redux不同

    Rx

    Reactive Extensions 一种重写的现代化方案,管理复杂异步应用。

    Rx 提供构建将人机交互转变为 相互依赖可观测变量的库。

    专家建议可以同时使用它和redux库。

    组合得到一个新的action,在提交给 store.dispatch () 

    扩展阅览

    redux开发工具-> https://github.com/zalmoxisus/redux-devtools-extension/releases

    redux项目引入开发工具 -> https://github.com/zalmoxisus/redux-devtools-extension#usage

    redux 源码阅读 -> https://cdn.bootcss.com/redux/4.0.4/redux.js

    6,生态系统

    redux 生态系统体小精湛,还有很与之相匹配的库,https://github.com/xgrommx/awesome-redux

    包括:中间件、工具库、样板示例、代码魔板等。https://www.redux.org.cn/docs/introduction/Ecosystem.html

    然后很多示例,暂时不看。

    7,基础

    7.1  Action

    const action = {
      type: ADD_TODO,
      text: 'Build my first Redux app',
      index: 1,
      filter: SHOW_COMPLETED
    }
    import { ADD_TODO, REMOVE_TODO, SHOW_COMPLETED } from '../actionTypes' // 不是超大文件可以不用这样写。

    7.2 Reducer 

    指定了状态如何发生变化和响应的store的,action表示有什么事发生,reducer是具体操作。

    处理reducer关系时,注意应该尽量state范式化,不存在嵌套,放在一个对象,不同实体或列表用id引用。

    (previousState, action) => newState
    

    ---------------------------

    永远不要在reducer函数中做这些事:

    a, 修改传参。

    b, 执行副作用操作。

    c, 调用非纯函数,random() now() 等。

    --------------------------------

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

    const todoApp = combineReducers({
      visibilityFilter,
      todos
    });
    
    const reducer = combineReducers({
      a: doSomethingWithA,
      b: processB,
      c: c
    });
    
    function reducer(state = {}, action) {
      return {
        a: doSomethingWithA(state.a, action),
        b: processB(state.b, action),
        c: c(state.c, action)
      }
    };

    es6简介写法。

    import { combineReducers } from 'redux'
    import * as reducers from './reducers'
    
    const todoApp = combineReducers(reducers)

    8,Store

    提供:

    1. dispatch(action)       更新state
    2. getState()               获取state
    3. subscribe(listener)    注册事件
    4. subscribe(listener)    返回值解除注册事件
    unsubscribe()  //停止监听更新

    9,实现容器组件

    把展示组件和redux 关联起来,原理上就是 store.subscribe 从 redux.state 中读取数据作为props。

    然后渲染展示组件。

    connect方法做了性能优化,类似componentShouldUpdate。

    需要先定义 mapStateToProps 这个函数来指定如何把state值映射到组件的props。

    这个函数返回props需要的对象。

    const mapStateToProps = state => {
      return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
      }
    }

    还可以分发action,定义 mapDispatchToProps方法返回 dispatch 传给展示组件props的回调方法。

    传入一个dispatch 并返回一个有事件的对象。

    const mapDispatchToProps = dispatch => {
      return {
        onTodoClick: id => {
          dispatch(toggleTodo(id))
        }
      }
    }

    现在就有了展示组件需要的 props和执行函数,还需要一步把这些集成到组件上。

    import { connect } from 'react-redux';
    import { TodoList } from './TodoList.jsx';
    
    const VisibleTodoList = connect(
      mapStateToProps,
      mapDispatchToProps
    )(TodoList);
    
    export default VisibleTodoList;

    这样就可以和react结合使用。将视图和逻辑混合使用。

    import React from 'react'
    import { connect } from 'react-redux'
    import { addTodo } from '../actions'
    
    let AddTodo = ({ dispatch }) => {
      let input
    
      return (
        <div>
          <form
            onSubmit={e => {
              e.preventDefault()
              if (!input.value.trim()) {
                return
              }
              // 提交的是 dispatch,去更新state模拟提交数据。
              dispatch(addTodo(input.value))
              input.value = ''
            }}
          >
            <input
              ref={node => {
                input = node
              }}
            />
            <button type="submit">
              Add Todo
            </button>
          </form>
        </div>
      )
    }
    // connect() 返回一个函数,函数的传参绑定了store 的api。
    AddTodo = connect()(AddTodo)
    
    export default AddTodo

    用react-redux 的 Provider 对根组件封装,传入store

    import React from 'react'
    import { render } from 'react-dom'
    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import todoApp from './reducers'
    import App from './components/App'
    
    // 创建一个store
    let store = createStore(todoApp)
    
    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );

    ---------------------------------------------- 

    高级部分

    1,异步action

    当调用异步action时,需要注意两个注意的时间点:发起和响应。一般要dispatch三种action

    1. 发起请求  isFetching  标记开始
    2. reducer  请求成功的action ,合并state,更新 isFetching
    3. reducer  请求势必的action ,更新 isFetching 记录错误信息并展示

    这个过程可以使用这种啰嗦的方式定义:

    不同的 status,可以使用https://github.com/redux-utilities/redux-actions 辅助库返回reducer。

    { type: 'FETCH_POSTS' }
    { type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
    { type: 'FETCH_POSTS', status: 'success', response: { ... } }

    异步action就是在异步完成或结束之后执行dispatch。

    redux结合使用的异步请求库一般用 cross_fetch。一点都不好用,还要引入补丁  babel-polyfill。

    2,中间件

    引入中间件的store可以支持异步流,比如redux-promise,redux-thunk可以异步处理action

    需要注意的是,applyMiddleware 增强createStore中,拦截promise,最后一个middleware必须是返回对象。

    在action发起后,到达reducer之前的扩展点。

    中间件可以记录日志,创建报告,异步接口或路由等。

    import thunkMiddleware from 'redux-thunk'
    import { createLogger } from 'redux-logger'
    import { createStore, applyMiddleware } from 'redux'
    import { selectSubreddit, fetchPosts } from './actions'
    import rootReducer from './reducers'
    
    const loggerMiddleware = createLogger()
    
    const store = createStore(
      rootReducer,
      applyMiddleware(
        thunkMiddleware, // 允许我们 dispatch() 函数
        loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
      )
    )
     

    3,语法尽量符合es6规范

    4,服务端渲染

    两个部分:__INITIAL_STATE__renderToString

    // 引入服务端渲染接口
    import { renderToString } from 'react-dom/server'
    
    function handleRender(req, res) {
      // 创建Redux store 实例
      const store = createStore(counterApp);
    
      // 把组件渲染 HTML 字符串
      const html = renderToString(
        <Provider store={store}>
          <App />
        </Provider>
      )
    
      // 从 store 中获得初始 state
      const preloadedState = store.getState();
    
      // 把渲染后的页面内容发送给客户端
      res.send(renderFullPage(html, preloadedState));
    }
     
    
    客户端可以通过 window.__INITIAL_STATE_  获取state值。
    
    function renderFullPage(html, preloadedState) {
      return `
        <!doctype html>
        <html>
          <head>
            <title>Redux Universal Example</title>
          </head>
          <body>
            <div id="root">${html}</div>
            <script>
              window.__INITIAL_STATE__ = ${JSON.stringify(preloadedState)}
            </script>
            <script src="/static/bundle.js"></script>
          </body>
        </html>
        `
    }

    5,jest 编写测试

    因为redux代码大部分都是纯函数,纯函数比较好测。

    和其他jest mocha 测试一样,需要编写测试用例,断言查看结果。

    import configureMockStore from 'redux-mock-store'
    import thunk from 'redux-thunk'
    import * as actions from '../../actions/TodoActions'
    import * as types from '../../constants/ActionTypes'
    import nock from 'nock'
    import mocha from 'mocha' // 你可以使用任何测试库
    const middlewares = [ thunk ]
    const mockStore = configureMockStore(middlewares)
    describe('async actions', () => {
      afterEach(() => {
        nock.cleanAll()
      })
      it('创建对 action: FETCH_TODOS_SUCCESS 是否会完成 fetch 的判断', () => {
        nock('http://example.com/')
          .get('/todos')
          .reply(200, { body: { todos: ['do something'] }})
        const expectedActions = [
          { type: types.FETCH_TODOS_REQUEST },
          { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something']  } }
        ]
        const store = mockStore({ todos: [] })
        return store.dispatch(actions.fetchTodos())
          .then(() => { // 异步 actions 的返回
            expect(store.getActions()).toEqual(expectedActions)
          })
      })
    })

    6,实现数据撤销和可追溯

     期望的数据结构

    {
        past: [0,1,2,3],
        preset: 4,
        feature: [5,6]
    }

    引入插件 redux-undo 保留数据支持撤销

    npm install --save redux-undo

    使用undoable()

    import undoable, { distinctState } from 'redux-undo'
    const todos = (state = [], action) => {
    }
    
    const undoableTodos = undoable(todos, {
      filter: distinctState()
    });
    export default undoableTodos;
  • 相关阅读:
    正则表达式分类
    数据思维二三事
    关于编程语言的一些趣史
    重构后端模板文件的一种实践
    为什么程序员需要知道互联网行业发展史
    探秘JS的异步单线程
    Nerd的畅销产品
    Nerd的套现ATM机
    网络传输与加密 (2)
    网络传输与加密
  • 原文地址:https://www.cnblogs.com/the-last/p/11610676.html
Copyright © 2011-2022 走看看