第十二章:状态管理的深渊——Redux与Vuex的对决

“状态管理,是前端开发的巴别塔。”

林小凡盯着屏幕上并排打开的两个文档:

  • Redux单一不可变状态树纯函数reducerdispatch(action)
  • Vuex集中式存储mutation同步action异步

他揉了揉太阳穴,感觉自己的大脑正在被两种截然不同的哲学撕裂。

“为什么一个简单的‘登录状态’需要这么多概念?!”他对着空气咆哮,手指悬在键盘上,像站在悬崖边犹豫的旅人。

墨尘的消息适时弹出:

“欢迎来到状态管理的深渊——这里埋葬着无数前端工程师的理智。”


第一节:Redux的仪式感

新接手的React项目采用经典Redux架构,目录结构令人望而生畏:

复制代码
/src  
  /store  
    /actions  
      - userActions.js  
    /reducers  
      - userReducer.js  
    /types  
      - actionTypes.js  
    - configureStore.js  

一个简单的“用户登录”需要穿越四个文件:

  1. 定义action类型(actionTypes.js)

    javascript 复制代码
    export const LOGIN_REQUEST = 'USER/LOGIN_REQUEST';  
    export const LOGIN_SUCCESS = 'USER/LOGIN_SUCCESS';  
  2. 编写action创建函数(userActions.js)

    javascript 复制代码
    export const login = (credentials) => (dispatch) => {  
      dispatch({ type: LOGIN_REQUEST });  
      return api.login(credentials)  
        .then(user => dispatch({ type: LOGIN_SUCCESS, payload: user }));  
    };  
  3. 处理reducer(userReducer.js)

    javascript 复制代码
    const initialState = { loading: false, data: null };  
    
    export default (state = initialState, action) => {  
      switch (action.type) {  
        case LOGIN_REQUEST:  
          return { ...state, loading: true };  
        case LOGIN_SUCCESS:  
          return { ...state, data: action.payload, loading: false };  
        default:  
          return state;  
      }  
    };  
  4. 组件中调用

    jsx 复制代码
    const mapDispatch = { login };  
    export default connect(null, mapDispatch)(LoginForm);  

“就为了改个登录状态?!”林小凡数了数键盘磨损的键帽,“这代码比银行转账流程还复杂!”


第二节:Vuex的甜蜜陷阱

转战Vue项目时,Vuex看起来像救世主:

javascript 复制代码
// store.js  
export default new Vuex.Store({  
  state: {  
    user: null  
  },  
  mutations: {  
    SET_USER(state, user) {  
      state.user = user; // 直接修改!  
    }  
  },  
  actions: {  
    async login({ commit }, credentials) {  
      const user = await api.login(credentials);  
      commit('SET_USER', user);  
    }  
  }  
});  

// 组件中使用  
methods: {  
  ...mapActions(['login'])  
}  

“这才叫人性化!”林小凡刚赞叹完,就发现项目里出现了:

  • 嵌套五层的module
  • mutation和action的命名冲突
  • 神秘失踪的state更新(后来发现是对象引用问题)

最可怕的是这个:

javascript 复制代码
// 某个深奥的plugin  
store.subscribeAction((action, state) => {  
  if (action.type === 'login') {  
    localStorage.setItem('lastLogin', new Date());  
  }  
});  

“所以Vuex的简洁只是表象……”他对着store.watch的文档陷入沉思。


第三节:Redux Toolkit的救赎

当林小凡准备放弃Redux时,@reduxjs/toolkit出现了:

javascript 复制代码
// 用createSlice同时定义reducer和action  
const userSlice = createSlice({  
  name: 'user',  
  initialState: { loading: false, data: null },  
  reducers: {},  
  extraReducers: (builder) => {  
    builder.addCase(login.pending, (state) => {  
      state.loading = true;  
    });  
    builder.addCase(login.fulfilled, (state, action) => {  
      state.data = action.payload;  
      state.loading = false;  
    });  
  }  
});  

// 用createAsyncThunk自动生成action  
export const login = createAsyncThunk('user/login', (credentials) => {  
  return api.login(credentials);  
});  

“等等!”他瞪大眼睛,“Redux现在允许直接修改state了?还有自动生成的action?”

墨尘发来一个笑哭的表情:

“Redux团队终于承认:人类不应该手写action type。”


第四节:Pinia的突袭

就在林小凡刚适应Vuex时,Vue 3推出了Pinia

javascript 复制代码
// stores/user.js  
export const useUserStore = defineStore('user', {  
  state: () => ({  
    data: null  
  }),  
  actions: {  
    async login(credentials) {  
      this.data = await api.login(credentials);  
    }  
  }  
});  

// 组件中使用  
const store = useUserStore();  
store.login();  

没有mutation,没有module,甚至没有this.$store

“所以Vuex被放弃了?!”林小凡三观碎裂。

GitHub上的issue给出了答案:

“Pinia is Vuex 5, but we didn't want to do a major version bump.”


终节:状态管理的本质

深夜,林小凡在项目遗留代码里发现一段2017年的注释:

javascript 复制代码
/*  
 * 状态管理的终极方案:  
 * 1. 把数据存在组件内(简单)  
 * 2. 发现需要跨组件共享 → 提升到父组件  
 * 3. 发现层级太深 → 尝试Context/Provide  
 * 4. 还是太乱 → 上Redux/Vuex  
 * 5. 最后发现:还不如直接存在localStorage  
 */  

窗外,月光照在并排打开的四个标签页上:

  • Redux
  • Vuex
  • @reduxjs/toolkit
  • Pinia

像四个不同时代的墓碑,又像同一座山峰的四面。

第十二章 完


下一章预告:
第十三章:npm与yarn的依赖迷宫
“当你运行npm install时,永远不知道是下载代码还是引爆地雷。”——某被node_modules折磨的开发者