zp1112 / blog

地址
http://issue.suzper.com/
36 stars 3 forks source link

终于理解了react-redux和redux的核心原则!以及存在的意义。。。 #10

Open zp1112 opened 6 years ago

zp1112 commented 6 years ago

redux核心思想

redux是通用的一套用于项目中统一管理全局状态的框架,这个状态state贯穿整个应用,全局共享,那么就少不了需要使用约定的统一的规范的动作actions来进行state的更新操作reducers。

三大组成

State

全局唯一的共享的state,其实就是一个对象,这个对象里面的属性将来需要在应用中的多个甚至每个页面都要用到并进行更新操作。

const initialState = {
  count: 0
}
Action

当多个页面多个组件都需要在各处进行共享state的操作的时候,如果各自随意修改state,就会变得很混乱,你不知道那个组件在何时何地修改了state,因此,需要一套约定的规范来进行更新state操作,redux提出使用action动作来触发,action相当于一个个钥匙,分发给各个组件,当你需要修改state的时候,只需要dispatch对应的action,这些action收集起来最终去触发对应的reducer执行最终的state更新操作。传给reducer的action始终只能是一个对象,对象中必须要有一个能描述操作的type用于让reducer针对type做对应的处理。

Reducer

reducer就是最终要更新state的函数,参数为传入的state和action,会对不同action.type做出不同操作的函数,在没有任何操作情况下,我们返回初始的initialState。

export default function counter(state = initialState, action) {
  const count = state.count
  switch (action.type) {
    case 'increase':
      return { count: count + 1 }
    default:
      return state
  }
}

以上的这个reducer处理increase的时候返回了一个新state,而不是在原来的state上直接修改,为什么要这么做呢?有三点原因,但是让我们先上一大大大大大。。。段示例代码:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware, compose } from 'redux';
import App from './App';
import reducers from './reducers/index';

let store;
if(!(window.__REDUX_DEVTOOLS_EXTENSION__ || window.__REDUX_DEVTOOLS_EXTENSION__)){
  store = createStore(
    reducers,
    applyMiddleware(thunk)
  );
}else{
  store = createStore(
    reducers,
    compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) //插件调试,未安装会报错
  );
}

if (process.env.NODE_ENV !== 'production') {
  const { whyDidYouUpdate } = require('why-did-you-update');
  whyDidYouUpdate(React);
}
// react和redux的结合产生了react-redux
ReactDOM.render(
  <Provider store={store}>
      <App />
  </Provider>, document.getElementById('root')
);

./reducers/index

import { combineReducers } from 'redux';
// 两个分开的reducer,将来可以根据项目需求进行state状态的细分,比如user相关的state,和app相关的state等分开使得state也组件化, 最终通过combineReducers进行组合成最终的大state,这里只是简单的示例。
function counter(state = { count: 0 }, action) {
  const count = state.count;
  switch (action.type) {
    case 'increase':
      state.count += 1;
      return state;
    default:
      return state;
  }
}

user(state = { username: 'candy' }, action) {
  switch (action.type) {
    case 'changename':
      return { username: action.name }
    default:
      return state
  }
}

export default combineReducers({
  todoApp, 
  user
});

App.js

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';

class App extends PureComponent {
  render() {
    const { count, onIncreaseClick, onChangeUserClick, username } = this.props
    return (
      <div>
        <span>{count}</span>
        <button onClick={onIncreaseClick}>Increase</button>
        <span>{username}</span>
        <button onClick={onChangeUserClick}>ChangeUser</button>
      </div>
    )
  }
}
function mapStateToProps(state) {
  return {
    count: state.todoApp.count,
    username: state.user.username
  }
}
const increaseAction = { type: 'increase' };
const changeUserClickAction = { type: 'changename', name: 'zp' };
function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction),
    onChangeUserClick: () => dispatch(changeUserClickAction)
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

image

针对这个例子我们来看一下不修改state的三大理由:

  1. 我们知道对象是堆内存,如果不创建副本,那么redux的所有操作都指向了内存中的同一个state,那么redux-devtools就很难追踪到state前后的变化,redux-devtools列表里所有的state都将被最后一次操作的结果所取代。打开控制台,可以看到每次的state更新状态。

image

  1. 点击Increase按钮,可以看到count并没有递增,点击ChangeUser,可以看到username正常更新,此时count才发生变化。为什么呢,因为react-redux先比较state的引用,引用没变化说明dispatch 前后的 state 没有改变,所以此处的counter这个reducer在type未increase的时候并没有返回新的state,而是返回旧的state,那么react-redux即认为状态没有改变,然后再点击ChangeUser的时候返回了新的state副本,引用改变了。这时组件才执行render渲染,那么刚才的count现在才渲染出来。关于组件的render时机可以参考这里

正确的是直接返回一个新的对象{ count: state.count += 1 }

  1. 创建副本也是为了保证向下传入的this.props与nextProps能得到正确的值,以便我们能够利用前后props的改变情况以决定如何render组件,其实和上面那点一样。。

如何正确更新state

  1. 拆分reducer

比如一个复杂state为如下所示,那么我们可以将其reducer拆分成分别处理count和user的两个,最后通过combineReducers合并。这样在返回副本的时候比较简单,可以直接{count: 2}这种单层的对象。

const state = {
  count: {
    sum: 0
  },
  user: {
    username: 'candy'
  }
}
  1. Object.assign()

简单的数据往往一层两层的对象,对于一层对象的改变,也可以使用

object.assign({}, state, {
  count: {sum: 2}
})
  1. 深拷贝

但是程序往往是复杂类型的,单层的对象无法满足业务需求的时候,就会出现嵌套很深的对象,这时候Object.assign()就不起作用了,因为Object.assign()只是浅拷贝。。。对于全是可枚举的属性的对象,可以使用如下方法

const newState = JSON.parse(JSON.stringify(state)); // 深拷贝
newState.count.sum = 3;
return newState;

或者使用lodash的cloneDeep

const newState = cloneDeep(state);
newState.visibilityFilter.b.d = action.filter;
return newState;
  1. immutable.js

依然参考这个

每种方案各有优劣,需要视项目情况而定,选择最适合最清晰的方式,才能高效高性能的进行开发!!!boom!!!

marknihao commented 6 months ago

if(!(window.REDUX_DEVTOOLS_EXTENSION || window.REDUX_DEVTOOLS_EXTENSION))是我眼花了吗