Redux和Mobx和Vuex

195 阅读8分钟

1、Redux

什么是单项数据流?

Action -> Store -> View

当用户调用stroe.dispatch的时候,会派发一个action。reducer根据action类型和payload更新store里的state。最后是视图更新。(可以理解为,视图更新是subscribe包裹的回调函数,每当数据更新,自动调用subscribe里的回调函数)

使用案例

1.首先创建 store 对象


import { createStore } from 'redux';

const reducer = (prevState= {/*初始状态*/},action) => {
  let newState = prevState;
  switch(action.type){
    case "情况一":
      newState.show = false;
      //这里是逻辑处理
    case "情况二":
      newState.show = false;
      //这里是逻辑处理
  }
}

const store = createStore(reducer);

export default store;
  1. 每次 state 更新, subcribe里的回调函数会执行:
  store.subscribe(()=>{
    console.log(store.getState().show)
  })

3.调用 dispatch 更新 state。dispatch 内部的对象就是 action

store.dispatch({
    type:"我是type",
    payload:"我是参数",
})

为什么reducer必须返回新对象

1.redux会比较新state和旧state,如果不同,才会触发订阅者,更新页面。如果直接修改原对象,那么就无法比较新旧state。

2.违背纯函数的理念,不能改变参数(也不能改变全局变量)。

// 纯函数
function add(a, b) {
  return a + b;
}

console.log(add(2, 3)); // 输出: 5
console.log(add(2, 3)); // 输出: 5

// 非纯函数
let result = 0;

function addToResult(value) {
  result += value;
  return result;
}

console.log(addToResult(5)); // 输出: 5
console.log(addToResult(5)); // 输出: 10

3.reduecer设计成纯函数的好处是:让action变得可追踪。因为它保留了历史中的每一个state。

segmentfault.com/q/101000003…

如何实现redux?

发布订阅模式。

订阅者调用subscribe(回调函数)。因此在store内部有一个callbacks数组用于存放所有的回调。

当触发某些事件,调用store.dispatch(action)更新state。因此dispatch内部会执行reducer,获取根据case获取最新state。在更新完state之后,紧接着遍历callbacks数组,依次执行回调。

在回调内部,可以通过store.getState()获取所有状态,因此只需封装一个getState()函数,返回对象的state。

//自定义简单的redux
function createStore(reducer){
    var list = [];
    var state = reducer()
    function subscribe(callback){
        list.push(callback);
    }
    
    function dispatch(action){
        for(var i in list){
            state = reducer(state,action);
            list[i] && list[i]();
        }
    }
    
    function getState(){
        return state;
    }
    
    return {
    createStore,
    dispatch,
    }
}

//reducer样例
const reducer = (preState={
    show:true,
    //...
    },action)=>{
    
    let newState = {...preState}
    switch(action.type){
        case "第一个case":
            newState.show = false;
            return newState;
        case
        // ......
    }

combineReducer

Reducer按照业务需求拆分,然后合并为大reducer。

image.png

取值的时候也需要改变。原本从store.getState().show返回。现在需要指定store.getState().TabbarReducer.show

image.png

dispatch异步action(thunk)

使用步骤

  1. store.js中重点在于,创建store的时候,第二个参数传入中间件。
// store.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import messageReducer from './reducers/message';

const rootReducer = combineReducers({
  message: messageReducer,
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;

  1. 导出action有重大差别,普通情况下,导出的action是一个对象。如果是异步操作,导出的atcion是一个函数
// actions/message.js
export const setMessage = (message) => ({
  type: 'SET_MESSAGE',
  payload: message,
});

// 异步 action creator
export const fetchMessage = () => async (dispatch) => {
  // 模拟异步请求
  const fetchData = async () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('Fetched message');
      }, 1000);
    });
  };

  const message = await fetchData();
  dispatch(setMessage(message));
};

  1. dispatch时有差别。普通情况下,dispatch接受一个对象。但如果dispatch的数据是通过网络请求获取的,则dispatch的参数是一个函数(可以看出fetchMessage()返回值是一个函数)。
// FetchMessageButton.js
import React from 'react';
import { useDispatch } from 'react-redux';
import { fetchMessage } from './actions/message';

const FetchMessageButton = () => {
  const dispatch = useDispatch();

  const handleClick = () => {
    dispatch(fetchMessage());
  };

  return <button onClick={handleClick}>Fetch Message</button>;
};

export default FetchMessageButton;

以下是草稿(可忽略)

当我们要修改store里的值,一般会用store.dispatch(action对象)。默认情况下,action是一个对象,结构如下:

{
    type:'我是type',
    payload:'我是payload',
}

如果action里的值需要通过网络请求获取,该怎么办?store.dispatch(getAction())

const getAction = () => {
    //dispatch可以随便命名,等到异步请求返回后,再调用dispatch
    return (dispatch)=>{
    axios.get(www.baidu.com).then((data)=>{
        dispatch({
            type:'我是type',
            payload:data,
        });
        })
    }
} 

上述方法解决了dispatch的action的payload需要通过网络请求获取的问题。但是直接运行上述代码会报错,因为得先配置中间件redux-thunk。

const store = createStore(reducer,applyMiddleWare(reduxThunk);

调试工具

左边是dispatch(action)action的名字,右侧是修改后的state。

image.png

如何使用React-Redux

如果不用React-Redux

若想要获取store的最新值,需要使用store.subscribe()。一旦store的值被更新,回调函数就会执行。

image.png

connect函数的作用

如图下所示,经过高阶组件的封装,你的组件props里就会多出a和b两个属性。高阶组件:本质上是一个函数,接受一个组件,返回一个新组件。

const newComponent = connet(()=>{
 return {
     a:11,
     b:22,
})(你的组件)

理解了connect的用法之后,可以读懂以下代码。

count是store里的变量,它直接从props获取而非从store.getState()中获取。

incrementdecrement内部调用了dispatch。不再需要通过store.dispatch(你的action对象)来修改state,直接调用props里的incrementdecrement

import { createStore } from "redux";
import { Provider, connect } from "react-redux";

// 定义 reducer
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

// 创建 Redux store
const store = createStore(counterReducer);

// 定义组件
function Counter(props) {
  return (
    <div>
      <p>Count: {props.count}</p>
      <button onClick={props.increment}>Increment</button>
      <button onClick={props.decrement}>Decrement</button>
    </div>
  );
}

// 定义 mapStateToProps 函数
function mapStateToProps(state) {
  return { count: state.count };
}

// 定义 mapDispatchToProps 函数
function mapDispatchToProps(dispatch) {
  return {
    myfunction : fetchMessage(),
    increment: () => dispatch({ type: "INCREMENT" }),
    decrement: () => dispatch({ type: "DECREMENT" })
  };
}

// 将 Counter 组件与 Redux store 连接
const ConnectedCounter = connect(mapStateToProps, mapDispatchToProps)(Counter);

// 将 Provider 组件包裹在 App 组件中,将 Redux store 传递给应用
function App() {
  return (
    <Provider store={store}>
      <ConnectedCounter />
    </Provider>
  );
}

export default App;


2. Mobx

基本用法

action用于标记方法,所有修改可观测属性的方法,应该用atcion标记。

computed是计算属性,当计算属性依赖的属性发生变化,会重新计算。

import { action } from 'mobx';

class TodoStore {
  @observable todos = [];
  
  @action
  addTodo(text) {
    this.todos.push({ text, completed: false });
  }
  
  @action
  toggleTodo(index) {
    this.todos[index].completed = !this.todos[index].completed;
  }
}

  @computed
  get unfinishedTodos() {
    return this.todos.filter(todo => !todo.completed);
  }
}

以下是使用方法,每当autorun回调内的依赖发生变化,会重新执行回调。


// 创建TodoStore实例
const todoStore = new TodoStore();

// 使用autorun监听unfinishedTodos的变化并在控制台输出
const disposer = autorun(() => {
  console.log('Unfinished todos:', todoStore.unfinishedTodos);
});

// 添加新的待办事项
todoStore.addTodo('Learn MobX');
todoStore.addTodo('Learn React');

// 当你不再需要autorun时,可以调用返回的disposer函数取消监听
disposer();

在React组件中如何使用store

通过@inject和@observe装饰器,可以在this.props获取store对象

@inject("store")
@observer
class APP extends component{
    componentDidMount(){
        console.log(this.props.store.属性);
    }
}

异步操作

以下action属于同步操作,这没有任何问题。

  @action
  addTodo(text) {
    this.todos.push({ text, completed: false });
  }

如果action内含有异步操作,那就会报错。普通action内不应该有异步操作。

  @action
  addTodo(text) {
    axio.get(...).then((data)=>{
        this.todos.push({ data, completed: false });
    })
  }

如果有异步操作,请把修改store属性的代码包裹在runInAction里面。

  @action
  addTodo(text) {
    axio.get(...).then((data)=>{
        runInAction(this.todos.push({ data, completed: false }));
    })
  }

或者这样修改,用await代替then。但无论如何,修改store属性的操作都应该被包裹在runInAction内。

response = await axios.get(/* ... */);
const data = response.data;
runInAction(() => {
this.todos.push({ data, completed: false });

在React中如何使用

1.最外层包裹Provider,提供store

<Provider store = {store} >
    <App />
</Provider>

2.在类组件中如何使用store

    @inject("store")
    @observer //装饰器 @observer等同意Redux中的connect
    class App extends Component {
        console.log(this.props.store);//@observer将store对象注入到props中
    }

3.在函数式组件中如何使用store

import { Observer } from 'mobx-react' //Observer负责在state改变时,重新执行内部回调
import Store from 'store' //函数时组件不会自动把store注入props,需要自己引入

export default function Cinemas(props) {
    return (
    <Observer>
       {
           ()=>{
               store.list.map(item => <div>{item}</div>);
           }
       }
    </Observer>
    )
}

如何实现Mobx中的observable和autorun

let tempFunction = null;

function observable(obj) {
  let map = new Map();
  return new Proxy(obj, {
    get(target, key) {
      if (tempFunction) {
        let observerArray = map.get(key) || [];
        if (!observerArray.includes(tempFunction)) {
          observerArray.push(tempFunction);
          map.set(key, observerArray);
        }
      }
      return target[key];
    },
    set(target, key, newValue) {
      target[key] = newValue; // 更新对象的属性值
      let callbackArr = map.get(key);
      if (callbackArr) {
        for (let i = 0; i < callbackArr.length; i++) {
          callbackArr[i]();
        }
      }
      return true;
    },
  });
}

function autorun(callback) {
  tempFunction = callback;
  callback();
  tempFunction = null;
}


3.Redux和Mobx的不同之处

1.原理差异: Redux 是基于不可变状态纯函数的,其核心概念是通过使用单一的状态树和纯粹的 reducer 函数来管理应用状态。每当状态发生变化时,都会创建一个新的状态对象,而不是直接修改原有状态。这有助于更容易地追踪状态变化和调试。

MobX 是基于可观察对象和响应式原则的。其核心概念是将应用状态表示为可观察对象,当这些对象发生变化时,MobX 会自动触发相关的副作用。这允许您以更自然的方式管理状态,更接近面向对象编程。

2.使用方式差异: Redux 要求您遵循严格的模式,包括 action、reducer 和 store。这导致了更多的样板代码和较高的学习曲线。但这也使得 Redux 更具可预测性和可维护性。

相比之下,MobX 提供了更简洁的 API 和更灵活的架构。您可以使用装饰器或函数来定义可观察状态和计算值,以及当状态发生变化时要执行的副作用。这使得 MobX 更易于上手和使用。

3.每当store更新,redux对导致子组件更新。因为子组件接受的props更新了。而mobx只有在observe函数包裹内的依赖更新才会导致子组件更新。

VueX

初始准备

在new Vue实例的时候,需要将store对象传给Vue。

image.png

使用方法

触发action:

通过 commit 触发对应的 mutation

image.png

比如 store.commit('addN',3),就会触发 addN 这个 mutation

image.png

action的作用

因为我们不能在 mutation 中执行异步操作,所以以下写法是错误的:

image.png

因为这样写的话,Vue 调试工具无法追踪它的状态:

image.png

异步操作应该放到 action 里面:

image.png

getters是什么

类似于计算属性,它本质就是一个函数

image.png