Vuex高级面试题

176 阅读39分钟

Vuex的state如何实现响应式更新?当直接给state对象添加新属性时为什么不会触发视图更新?如何解决?

Vuex 的 state 实现响应式更新的原理

在 Vuex 里,state 能够实现响应式更新,主要是因为 Vuex 内部运用了 Vue 的响应式系统。当创建 Vuex 的 store 实例时,state 对象会被 Vue 处理成响应式对象。具体步骤如下:

  1. 初始化:在创建 store 时,state 会被传入并被 Vue 进行响应式转换。这一过程类似于在 Vue 组件里定义 data 属性,Vue 会使用 Object.defineProperty() 方法将 state 对象的属性转换为 getter/setter
  2. 依赖收集:当组件通过计算属性或者 mapState 辅助函数访问 state 中的属性时,Vue 会收集这些依赖。也就是说,Vue 会记住哪些组件依赖了哪些 state 属性。
  3. 更新触发:一旦 state 中的属性值发生变化,Vue 会检测到这些变化,然后通知所有依赖该属性的组件进行更新。组件更新后,视图也会随之更新。
直接给 state 对象添加新属性时不触发视图更新的原因

在 Vue 里,响应式系统是基于 Object.defineProperty() 实现的,它只能对已经存在的属性进行响应式处理。当直接给 state 对象添加新属性时,Vue 并不会自动将这个新属性转换为响应式属性,所以当新属性的值发生变化时,Vue 无法检测到这些变化,也就不会触发视图更新。

解决办法
  1. 使用 Vue.set() 

  2. store.commit 配合 mutation:在 mutation 中使用 Object.assign() 或者扩展运算符来创建一个新的对象,从而添加新属性。示例如下:

  3. 在 Vue 3 中使用 reactive 和 toRefs

为什么Vuex推荐通过Vue.set()或对象展开符(...)操作state?

在 Vuex 中推荐使用 Vue.set() 或对象展开符(...)操作 state,主要是为了确保状态的修改能够被 Vue 的响应式系统捕获,从而保证视图能够正确更新

解释Vuex的state -> view -> action -> mutation -> state循环流程,为什么强调只能通过mutation修改state?

循环流程解释
  • state 到 viewstate 是 Vuex 中存储应用状态的地方,当 state 中的数据发生变化时,Vue 的响应式系统会自动检测到这些变化,并通知依赖该数据的组件进行更新,从而使 view(视图)能够反映出 state 的最新状态。
  • view 到 action:用户在视图上的交互行为,例如点击按钮、输入文本等,会触发相应的事件处理函数。在这些事件处理函数中,通常会通过 dispatch 方法触发 actionaction 可以包含异步操作,如发送网络请求、处理复杂的业务逻辑等。
  • action 到 mutationaction 不能直接修改 state,而是通过 commit 方法来提交 mutationmutation 是一个同步函数,它接收 state 作为第一个参数,并根据 mutation 的类型和传入的参数来修改 state
  • mutation 到 statemutation 中的逻辑会直接修改 state 的值,一旦 state 被修改,Vue 的响应式系统会再次生效,将最新的 state 状态更新到 view 上,从而完成一次循环。
强调只能通过 mutation 修改 state 的原因
  • 可追踪性:通过将所有对 state 的修改都集中在 mutation 中,可以方便地追踪和记录 state 的变化。这对于调试应用程序非常有帮助,开发人员可以清楚地看到每个 mutation 是如何影响 state 的,以及在什么时间发生了哪些变化。
  • 可预测性mutation 是同步函数,其执行结果是可预测的。这意味着给定相同的输入,mutation 总是会产生相同的输出,不会受到异步操作或其他外部因素的影响。这种可预测性使得应用程序的状态管理更加稳定和可靠,易于理解和维护。
  • 便于代码组织和维护:将 state 的修改逻辑集中在 mutation 中,可以使代码更加清晰和有条理。开发人员可以在一个地方找到所有与 state 修改相关的代码,而不是在应用程序的各个角落分散地修改 state,从而提高了代码的可维护性和可读性。
  • 插件和开发工具支持:Vuex 的插件和开发工具(如 Vue DevTools)可以更好地与 mutation 集成。它们可以拦截 mutation 的调用,提供额外的功能,如日志记录、时间旅行调试等。如果允许直接在其他地方修改 state,这些插件和工具将无法有效地发挥作用。

为什么mutation必须是同步函数,而action可以是异步的?如果强行在mutation中写异步代码会导致什么问题?

为什么mutation必须是同步函数,而action可以是异步的
1. mutation 必须是同步函数的原因
  • 可追踪性:Vuex 的一个重要特性是能够追踪状态的变化,方便调试和维护。因为 mutation 是修改 state 的唯一途径,将其设计为同步函数,能让状态的变化可预测且易于追踪。每次调用 mutation,状态的变化是即时的,开发人员可以清晰地知道在某个时刻 state 是如何被修改的。例如,在使用 Vue DevTools 的时间旅行调试功能时,能精确看到每个 mutation 执行前后 state 的变化情况。
  • 数据一致性:同步操作能保证在 mutation 执行期间,state 的变化是连续且可控制的。如果 mutation 是异步的,在异步操作执行过程中,其他 mutation 可能会同时修改 state,这就会导致 state 的变化混乱,难以保证数据的一致性。
2. action 可以是异步的原因
  • 处理异步操作:在实际应用中,很多操作是异步的,比如网络请求、定时器等。action 就是为了处理这些异步操作而设计的。它可以在异步操作完成后,通过 commit 方法触发 mutation 来修改 state。例如,在发送一个 HTTP 请求获取数据后,等数据返回再提交 mutation 更新 state
  • 业务逻辑分离:将异步操作放在 action 中,能让 mutation 专注于单纯的状态修改逻辑,实现业务逻辑和状态修改逻辑的分离,使代码结构更清晰,便于维护和测试。
如果强行在mutation中写异步代码会导致什么问题?
  • 状态变化不可追踪:由于异步操作不会立即执行完毕,在 Vuex 的调试工具(如 Vue DevTools)中,无法准确追踪 state 是何时被修改的。比如,在一个异步的 mutation 里发起了一个网络请求,当请求返回并修改 state 时,调试工具记录的 mutation 执行时间和实际 state 变化的时间会不一致,导致难以调试和定位问题。
  • 数据不一致:异步操作的执行顺序是不确定的,可能会导致多个 mutation 同时修改 state,造成 state 的数据不一致。例如,有两个异步的 mutation 都对同一个 state 属性进行修改,由于异步操作的时序问题,最终 state 的值可能不符合预期。
  • 违反设计原则:违背了 Vuex 中 mutation 用于同步修改 stateaction 用于处理异步操作的设计原则,使代码的逻辑变得混乱,增加了维护和理解代码的难度。

如何在action中处理多个异步操作的串行/并行执行?

串行执行意味着多个异步操作按顺序依次执行,即前一个异步操作完成后,才会开始执行下一个。可以借助 async/await 或者 .then() 链式调用实现。

如何动态注册一个Vuex模块?在什么场景下需要动态注册?如何避免模块命名冲突?

如何动态注册一个 Vuex 模块

在 Vuex 中,可以使用 store.registerModule 方法来动态注册一个模块。以下是具体的步骤和示例代码:

1. 定义模块

首先,你需要定义一个 Vuex 模块,包含 statemutationsactions 和 getters 等部分。

// 定义一个动态模块
const dynamicModule = {
  state: {
    dynamicData: 'This is dynamic data'
  },
  mutations: {
    updateDynamicData(state, newData) {
      state.dynamicData = newData;
    }
  },
  actions: {
    updateData({ commit }, newData) {
      commit('updateDynamicData', newData);
    }
  },
  getters: {
    getDynamicData(state) {
      return state.dynamicData;
    }
  }
};
2. 动态注册模块

在需要注册模块的地方,调用 store.registerModule 方法。

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 创建 store 实例
const store = new Vuex.Store({});

// 动态注册模块
store.registerModule('dynamic', dynamicModule);

// 使用动态模块的数据
console.log(store.state.dynamic.dynamicData); // 输出: This is dynamic data

// 调用动态模块的 action
store.dispatch('dynamic/updateData', 'New dynamic data');

// 再次获取数据
console.log(store.state.dynamic.dynamicData); // 输出: New dynamic data

在什么场景下需要动态注册

1. 懒加载模块

当应用程序非常大时,一次性加载所有的 Vuex 模块可能会导致初始加载时间过长。动态注册模块可以实现懒加载,只有在需要使用某个模块时才进行加载和注册,从而提高应用的性能。

2. 插件系统

在开发插件系统时,插件可能需要向应用中注入自己的状态管理逻辑。通过动态注册模块,插件可以方便地将自己的模块添加到主应用的 Vuex 中,而不需要修改主应用的核心代码。

3. 路由切换

在单页面应用中,不同的路由可能需要不同的状态管理。可以根据路由的切换动态注册和卸载模块,以减少不必要的状态管理开销。

如何避免模块命名冲突

1. 使用唯一的模块名

在注册模块时,确保每个模块都有一个唯一的名称。可以使用命名空间或者前缀来区分不同的模块。例如:

// 模块 1
const module1 = {
  namespaced: true,
  state: { /* ... */ },
  mutations: { /* ... */ },
  actions: { /* ... */ },
  getters: { /* ... */ }
};

// 模块 2
const module2 = {
  namespaced: true,
  state: { /* ... */ },
  mutations: { /* ... */ },
  actions: { /* ... */ },
  getters: { /* ... */ }
};

// 注册模块
store.registerModule('module1', module1);
store.registerModule('module2', module2);
2. 使用命名空间

Vuex 支持模块的命名空间,通过设置 namespaced: true,可以将模块的 mutationsactions 和 getters 封装在一个命名空间中,避免与其他模块的命名冲突。

const moduleA = {
  namespaced: true,
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  }
};

// 注册模块
store.registerModule('moduleA', moduleA);

// 调用命名空间模块的 action
store.dispatch('moduleA/incrementAsync');

带命名空间(namespaced: true)的模块如何访问全局的state和getters?

通过 rootState 和 rootGetters 可以方便地访问全局的 state 和 getters

如何设计可复用的Vuex模块?如果多个组件需要独立实例化同一个模块,如何实现?(模块复用)

如何设计可复用的 Vuex 模块

1. 保持模块功能单一

可复用的 Vuex 模块应该专注于实现单一的功能,例如用户认证、数据缓存等。这样可以提高模块的内聚性,使其更容易被复用。

2. 使用命名空间

为模块添加 namespaced: true,可以避免模块之间的命名冲突,使模块更加独立。

3. 避免硬编码依赖

模块内部的代码应该尽量避免硬编码对其他模块或外部环境的依赖,以提高模块的灵活性。可以通过参数传递或注入依赖的方式来解决。

4. 提供清晰的接口

模块应该提供清晰的 actionsmutations 和 getters 接口,方便其他组件或模块调用。

示例代码
// 可复用的 Vuex 模块
const counterModule = {
  namespaced: true,
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  }
};

export default counterModule;

如果多个组件需要独立实例化同一个模块,如何实现

1. 动态注册模块

可以在组件的 created 钩子中动态注册模块,每个组件实例都可以独立拥有自己的模块实例。

import Vue from 'vue';
import Vuex from 'vuex';
import counterModule from './counterModule';

Vue.use(Vuex);

const store = new Vuex.Store({});

export default {
  created() {
    // 动态注册模块,使用唯一的模块名
    const moduleName = `counter-${this._uid}`;
    store.registerModule(moduleName, counterModule);
  },
  methods: {
    increment() {
      const moduleName = `counter-${this._uid}`;
      store.commit(`${moduleName}/increment`);
    },
    getCount() {
      const moduleName = `counter-${this._uid}`;
      return store.state[moduleName].count;
    }
  }
};
2. 手动创建模块实例

可以在组件内部手动创建模块的实例,并将其添加到 store 中。

import Vue from 'vue';
import Vuex from 'vuex';
import counterModule from './counterModule';

Vue.use(Vuex);

const store = new Vuex.Store({});

export default {
  data() {
    return {
      moduleName: `counter-${this._uid}`
    };
  },
  created() {
    // 手动创建模块实例
    const instance = {
      ...counterModule,
      state: { ...counterModule.state }
    };
    store.registerModule(this.moduleName, instance);
  },
  methods: {
    increment() {
      store.commit(`${this.moduleName}/increment`);
    },
    getCount() {
      return store.state[this.moduleName].count;
    }
  }
};

通过以上方法,可以实现多个组件独立实例化同一个 Vuex 模块,每个组件都可以独立管理自己的状态。

如何在两个独立的模块中监听对方的mutation或action?解释rootStaterootGetters在模块中的使用场景。

如何在两个独立的模块中监听对方的 mutation 或 action

监听对方的 mutation

在 Vuex 里,要在一个模块中监听另一个模块的 mutation,可以利用 store.subscribe 方法。这个方法会在每次 mutation 被提交时触发。以下是示例代码:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 模块 A
const moduleA = {
    namespaced: true,
    state: {
        valueA: 0
    },
    mutations: {
        incrementA(state) {
            state.valueA++;
        }
    }
};

// 模块 B
const moduleB = {
    namespaced: true,
    state: {
        valueB: 0
    },
    actions: {
        initWatcher({ rootState, dispatch }) {
            // 订阅所有 mutation
            const unsubscribe = this.subscribe((mutation) => {
                // 检查是否是模块 A 的 incrementA mutation
                if (mutation.type === 'moduleA/incrementA') {
                    // 在这里可以执行相应的操作
                    dispatch('doSomethingInB');
                }
            });
        },
        doSomethingInB() {
            console.log('Module B detected module A's mutation and is doing something.');
        }
    }
};

const store = new Vuex.Store({
    modules: {
        moduleA,
        moduleB
    }
});

// 在模块 B 中初始化监听器
store.dispatch('moduleB/initWatcher');

// 触发模块 A 的 mutation
store.commit('moduleA/incrementA');
监听对方的 action

要监听另一个模块的 action,可以使用 store.subscribeAction 方法,它会在每次 action 被分发时触发。示例如下:

// 模块 A
const moduleA = {
    namespaced: true,
    actions: {
        doSomethingInA() {
            console.log('Module A is doing something.');
        }
    }
};

// 模块 B
const moduleB = {
    namespaced: true,
    actions: {
        initActionWatcher({ rootState, dispatch }) {
            // 订阅所有 action
            const unsubscribe = this.subscribeAction((action) => {
                // 检查是否是模块 A 的 doSomethingInA action
                if (action.type === 'moduleA/doSomethingInA') {
                    // 在这里可以执行相应的操作
                    dispatch('doSomethingInBAfterA');
                }
            });
        },
        doSomethingInBAfterA() {
            console.log('Module B detected module A's action and is doing something.');
        }
    }
};

const store = new Vuex.Store({
    modules: {
        moduleA,
        moduleB
    }
});

// 在模块 B 中初始化 action 监听器
store.dispatch('moduleB/initActionWatcher');

// 触发模块 A 的 action
store.dispatch('moduleA/doSomethingInA');

rootState 和 rootGetters 在模块中的使用场景

rootState 的使用场景

rootState 可用于在模块中访问根状态。比如,在某个模块的 action 里,需要根据根状态的某个值来决定是否执行特定操作。示例如下:

const moduleC = {
    namespaced: true,
    actions: {
        someAction({ rootState }) {
            if (rootState.globalFlag) {
                // 根据根状态的 globalFlag 执行操作
                console.log('Performing action based on root state.');
            }
        }
    }
};

const store = new Vuex.Store({
    state: {
        globalFlag: true
    },
    modules: {
        moduleC
    }
});

store.dispatch('moduleC/someAction');
rootGetters 的使用场景

rootGetters 可用于在模块中访问根 getters。例如,在模块的 getter 里,需要结合根 getters 的返回值来计算新的值。示例如下:

const store = new Vuex.Store({
    state: {
        globalValue: 10
    },
    getters: {
        getGlobalValueDoubled: state => state.globalValue * 2
    },
    modules: {
        moduleD: {
            namespaced: true,
            getters: {
                combinedValue: (state, getters, rootState, rootGetters) => {
                    // 结合根 getters 的值计算新的值
                    return state.localValue + rootGetters.getGlobalValueDoubled;
                }
            },
            state: {
                localValue: 5
            }
        }
    }
});

console.log(store.getters['moduleD/combinedValue']);

通过上述方式,就能在模块间实现监听 mutation 或 action,并合理运用 rootState 和 rootGetters 了。

如何编写一个Vuex插件?举例说明插件在提交mutation前后的拦截逻辑(如日志记录)。

Vuex 插件是一个函数,它接收 store 作为唯一参数,可以在插件中监听 store 的各种事件,比如 mutation 提交、action 分发等。下面为你详细介绍如何编写一个 Vuex 插件,并举例说明在提交 mutation 前后的拦截逻辑(日志记录)。

编写 Vuex 插件的基本步骤

  1. 定义插件函数:插件函数接收 store 作为参数。
  2. 在插件函数内部实现拦截逻辑:可以监听 store 的事件,如 subscribe 方法可用于监听 mutation 提交,subscribeAction 方法可用于监听 action 分发。
  3. 返回插件函数:将定义好的插件函数返回。

示例代码

以下是一个在提交 mutation 前后进行日志记录的 Vuex 插件示例:

// 定义 Vuex 插件
const loggerPlugin = (store) => {
    // 在插件初始化时,可以进行一些初始化操作
    console.log('Vuex logger plugin initialized.');

    // 监听 mutation 提交
    store.subscribe((mutation, state) => {
        // 在 mutation 提交前记录日志
        console.group(`Before Mutation: ${mutation.type}`);
        console.log('Payload:', mutation.payload);
        console.log('Previous State:', state);
        console.groupEnd();

        // 这里可以添加在 mutation 提交前执行的其他逻辑

        // 在 mutation 提交后记录日志
        console.group(`After Mutation: ${mutation.type}`);
        console.log('New State:', store.state);
        console.groupEnd();

        // 这里可以添加在 mutation 提交后执行的其他逻辑
    });
};

// 使用 Vuex
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 创建 store 实例,并应用插件
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        },
        decrement(state) {
            state.count--;
        }
    },
    plugins: [loggerPlugin]
});

// 触发 mutation
store.commit('increment');

代码解释

  1. 插件函数定义loggerPlugin 是一个函数,接收 store 作为参数。
  2. 监听 mutation 提交:使用 store.subscribe 方法监听 mutation 提交事件。该方法接收一个回调函数,回调函数的第一个参数是 mutation 对象,包含 typemutation 类型)和 payloadmutation 携带的参数);第二个参数是 state,表示 mutation 提交前的状态。
  3. 日志记录:在 mutation 提交前后分别记录日志,包括 mutation 类型、payload、提交前的状态和提交后的状态。
  4. 应用插件:在创建 store 实例时,将 loggerPlugin 添加到 plugins 数组中。

如何实现状态持久化(如vuex-persistedstate)?需要考虑哪些边界情况?

通过 createPersistedState() 创建一个插件实例,并将其添加到 store 的 plugins 数组中,这样 vuex-persistedstate 会自动将 state 数据存储到 localStorage 中,并在页面加载时恢复数据。

需要考虑的边界情况

数据大小限制
  • 问题localStorage 和 sessionStorage 有数据大小限制(通常为 5MB 左右),如果 state 数据量过大,可能会超出这个限制,导致数据存储失败。
  • 解决方案:可以对数据进行压缩处理,或者只存储必要的数据。也可以考虑使用 IndexedDB 等其他存储方式,它们支持更大的数据存储。
数据格式兼容性
  • 问题:在将 state 存储到本地存储时,需要将其序列化为字符串(通常使用 JSON.stringify()),在恢复数据时再反序列化(使用 JSON.parse())。如果 state 中包含一些特殊的数据类型(如 Date 对象、RegExp 对象等),序列化和反序列化过程可能会丢失信息。
  • 解决方案:在序列化和反序列化时,对特殊数据类型进行处理。例如,可以自定义序列化和反序列化函数,将 Date 对象转换为字符串存储,恢复时再转换回 Date 对象。
安全问题
  • 问题localStorage 和 sessionStorage 中的数据是明文存储的,如果 state 中包含敏感信息(如用户密码、令牌等),可能会存在安全风险。
  • 解决方案:不要将敏感信息存储在 state 中,或者对存储的数据进行加密处理。
多标签页或窗口同步问题
  • 问题:如果应用在多个标签页或窗口中打开,当一个标签页中的 state 发生变化并存储到本地存储时,其他标签页可能无法及时更新。
  • 解决方案:可以使用 storage 事件监听本地存储的变化,当本地存储数据发生变化时,在其他标签页中更新 state
版本兼容性问题
  • 问题:如果应用进行了升级,state 的结构可能会发生变化,导致旧版本存储的数据无法正确恢复。
  • 解决方案:在存储数据时添加版本号,在恢复数据时检查版本号,如果版本不兼容,可以进行相应的处理(如清空旧数据、进行数据迁移等)。

严格模式(strict: true)的作用是什么?为什么生产环境要关闭它?

严格模式(strict: true)的作用

在 Vuex 里,开启严格模式(即设置 strict: true)时,Vuex 会对状态修改进行深度监测,以此保证所有的 state 修改都是通过 mutation 完成的。下面是严格模式的主要作用:

1. 保证状态修改的规范性

严格模式能强制开发者遵循 Vuex 的设计原则,也就是只能通过 mutation 来修改 state。若直接在 action 或者组件里修改 state,Vuex 会抛出错误。

2. 便于调试

严格模式可以帮助开发者在开发阶段快速定位状态修改的问题。当状态被非法修改时,Vuex 会立即给出错误提示,方便开发者及时发现并解决问题,从而提高开发效率。

为什么生产环境要关闭严格模式

虽然严格模式在开发阶段非常有用,但在生产环境中应该关闭,主要原因如下:

1. 性能开销大

严格模式会对 state 的修改进行深度监测,这需要消耗大量的计算资源。在生产环境中,应用的性能是至关重要的,开启严格模式会导致应用性能下降,尤其是在处理大量数据或者频繁更新状态的场景下,性能问题会更加明显。

2. 增加代码体积

严格模式的实现需要额外的代码来进行状态监测,这会增加应用的代码体积。在生产环境中,为了提高应用的加载速度,应该尽量减小代码体积,关闭严格模式可以避免引入不必要的代码。

3. 无实际意义

在生产环境中,代码已经经过了充分的测试和调试,开发者应该已经确保所有的状态修改都是通过 mutation 完成的。此时,严格模式的监测功能就没有实际意义了,反而会带来性能和代码体积方面的问题。

综上所述,严格模式适合在开发和测试阶段使用,帮助开发者规范代码和调试问题;而在生产环境中,为了保证应用的性能和加载速度,应该关闭严格模式。

Vuex的getter是否具有缓存机制?如何优化高计算成本的getter?

优化高计算成本的 getter

1. 拆分复杂计算

如果 getter 的计算逻辑非常复杂,可以将其拆分成多个简单的 getter,然后在需要的地方组合使用。这样每个 getter 的计算量会减少,也更容易进行缓存和维护。

const store = new Vuex.Store({
    state: {
        products: [
            { id: 1, name: 'Product 1', price: 10, quantity: 2 },
            { id: 2, name: 'Product 2', price: 20, quantity: 1 },
            { id: 3, name: 'Product 3', price: 15, quantity: 3 }
        ]
    },
    getters: {
        productPrices: state => state.products.map(product => product.price),
        productQuantities: state => state.products.map(product => product.quantity),
        totalPrices: (state, getters) => {
            return getters.productPrices.map((price, index) => price * getters.productQuantities[index]);
        },
        grandTotal: (state, getters) => {
            return getters.totalPrices.reduce((acc, total) => acc + total, 0);
        }
    }
});

在这个例子中,将计算总价的逻辑拆分成了多个 getter,每个 getter 负责一部分计算,并且利用了缓存机制,避免了重复计算。

2. 防抖和节流

如果 getter 的计算依赖于频繁变化的数据,可以考虑使用防抖(Debounce)或节流(Throttle)技术来限制计算的频率。例如,当用户在输入框中输入内容时,可能会频繁触发 getter 的计算,使用防抖可以确保在用户停止输入一段时间后再进行计算。

import { debounce } from 'lodash';

const store = new Vuex.Store({
    state: {
        searchQuery: ''
    },
    getters: {
        filteredData: (state, getters) => {
            const debouncedFilter = debounce(() => {
                // 执行复杂的过滤逻辑
                return state.data.filter(item => item.name.includes(state.searchQuery));
            }, 300);
            return debouncedFilter();
        }
    }
});

这里使用了 Lodash 库的 debounce 函数,确保在用户输入停止 300 毫秒后才进行过滤计算。

3. 手动缓存

如果 getter 的计算结果在某些情况下不会发生变化,可以手动进行缓存。例如,在某些数据初始化后,其计算结果是固定的,可以将结果存储在一个变量中,下次需要时直接返回缓存结果。

let cachedResult = null;
const store = new Vuex.Store({
    state: {
        staticData: [1, 2, 3, 4, 5]
    },
    getters: {
        processedData: state => {
            if (cachedResult === null) {
                // 执行复杂的处理逻辑
                cachedResult = state.staticData.map(item => item * 2);
            }
            return cachedResult;
        }
    }
});

在这个例子中,将 processedData 的计算结果存储在 cachedResult 变量中,下次访问时如果结果已经缓存,则直接返回,避免了重复计算。

在超大型单页应用中,Vuex可能出现哪些性能问题?如何通过模块分割、懒加载等方式优化?

在超大型单页应用中,Vuex 可能出现的性能问题

1. 初始加载时间过长
  • 原因:在超大型单页应用里,state 数据量通常很大,并且包含众多的 mutationsactions 和 getters。当应用启动时,Vuex 需要对这些数据和逻辑进行初始化,这会消耗大量时间,从而导致初始加载时间变长。
  • 表现:用户打开应用后,可能需要等待较长时间才能看到页面内容,影响用户体验。
2. 内存占用过高
  • 原因:大量的 state 数据会占用大量内存,尤其是在数据频繁更新时,内存占用会进一步增加。而且,由于 Vuex 的响应式系统会对 state 进行劫持,也会消耗一定的内存资源。
  • 表现:应用可能会变得卡顿,甚至出现内存溢出的错误。
3. 响应式更新缓慢
  • 原因:当 state 发生变化时,Vuex 会通知所有依赖该 state 的组件进行更新。在超大型应用中,依赖关系复杂,组件数量众多,这会导致响应式更新的过程变得缓慢。
  • 表现:用户操作后,页面更新不及时,出现延迟。
4. 调试困难
  • 原因:随着应用的不断发展,state 的结构会变得越来越复杂,mutations 和 actions 的数量也会不断增加。这使得在调试时很难定位问题,尤其是在处理异步操作和复杂的状态变化时。
  • 表现:开发人员需要花费大量时间来排查和解决问题。

通过模块分割、懒加载等方式优化

1. 模块分割
  • 原理:将一个大的 Vuex store 拆分成多个小的模块,每个模块负责管理一部分独立的状态和逻辑。这样可以使代码结构更加清晰,便于维护和管理。

  • 实现步骤

    • 定义模块:将相关的 statemutationsactions 和 getters 封装在一个模块中。
    • 注册模块:在创建 Vuex store 时,将各个模块注册到 store 中。
// 模块 A
const moduleA = {
    namespaced: true,
    state: {
        dataA: []
    },
    mutations: {
        updateDataA(state, newData) {
            state.dataA = newData;
        }
    },
    actions: {
        fetchDataA({ commit }) {
            // 模拟异步请求
            setTimeout(() => {
                const newData = [1, 2, 3];
                commit('updateDataA', newData);
            }, 1000);
        }
    },
    getters: {
        getDataA: state => state.dataA
    }
};

// 模块 B
const moduleB = {
    namespaced: true,
    state: {
        dataB: []
    },
    mutations: {
        updateDataB(state, newData) {
            state.dataB = newData;
        }
    },
    actions: {
        fetchDataB({ commit }) {
            // 模拟异步请求
            setTimeout(() => {
                const newData = [4, 5, 6];
                commit('updateDataB', newData);
            }, 1000);
        }
    },
    getters: {
        getDataB: state => state.dataB
    }
};

// 创建 store
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    modules: {
        moduleA,
        moduleB
    }
});

export default store;
2. 懒加载
  • 原理:懒加载是指在需要使用某个模块时才进行加载,而不是在应用启动时就加载所有模块。这样可以减少初始加载时间和内存占用。

  • 实现步骤

    • 动态注册模块:使用 store.registerModule 方法在需要时动态注册模块。
    • 异步加载模块:使用 ES6 的动态导入(import())来异步加载模块。
// 动态注册模块示例
const loadModule = async (moduleName) => {
    const module = await import(`./modules/${moduleName}`);
    store.registerModule(moduleName, module.default);
};

// 在需要时加载模块
loadModule('moduleC');
3. 状态持久化优化
  • 原理:对于一些不经常变化的状态,可以使用状态持久化技术将其存储在本地存储(如 localStorage 或 sessionStorage)中。这样在应用重新加载时,可以直接从本地存储中获取状态,减少数据请求和初始化的时间。

  • 实现步骤

    • 安装 vuex-persistedstate 插件:该插件可以帮助我们实现状态持久化。
    • 配置插件:在创建 Vuex store 时,使用 vuex-persistedstate 插件。
import createPersistedState from 'vuex-persistedstate';

const store = new Vuex.Store({
    state: {
        // ...
    },
    mutations: {
        // ...
    },
    actions: {
        // ...
    },
    plugins: [createPersistedState()]
});
4. 缓存机制
  • 原理:对于一些计算成本较高的 getters,可以使用缓存机制来避免重复计算。Vuex 的 getters 本身就具有一定的缓存功能,但在某些情况下,我们可以手动实现更复杂的缓存逻辑。

  • 实现步骤

    • 使用变量缓存结果:在 getters 中使用一个变量来存储计算结果,只有当依赖项发生变化时才重新计算。
let cachedResult = null;
const store = new Vuex.Store({
    state: {
        data: [1, 2, 3, 4, 5]
    },
    getters: {
        processedData: state => {
            if (cachedResult === null) {
                // 执行复杂的处理逻辑
                cachedResult = state.data.map(item => item * 2);
            }
            return cachedResult;
        }
    }
});

为什么需要避免在state中存储过多非响应式数据(如类实例)?如何安全地管理非序列化数据?

为什么需要避免在 state 中存储过多非响应式数据(如类实例)

1. 破坏响应式系统

Vuex 的 state 依赖于 Vue 的响应式系统,该系统通过 Object.defineProperty()(Vue 2)或 Proxy(Vue 3)来实现数据的劫持和响应式更新。当一个对象被加入到 state 中时,Vue 会将其属性转换为 getter/setter 或者使用 Proxy 进行代理,以便在属性值发生变化时通知相关的组件进行更新。

然而,类实例通常包含一些自定义的方法和属性,这些属性可能不会被 Vue 的响应式系统正确处理。如果将类实例存储在 state 中,当实例的属性发生变化时,Vue 可能无法检测到这些变化,从而导致视图无法及时更新。

2. 序列化和持久化问题

在某些情况下,我们可能需要对 state 进行序列化,例如将 state 存储到本地存储(如 localStorage)或通过网络传输。但类实例通常包含一些无法序列化的属性(如函数、Symbol 等),在序列化过程中这些属性会丢失,导致数据不完整。

3. 调试和维护困难

类实例的内部状态和行为可能比较复杂,将其存储在 state 中会增加调试和维护的难度。当出现问题时,很难追踪和定位是哪个实例的哪个属性导致了问题。

如何安全地管理非序列化数据

1. 分离非序列化数据和响应式数据

将非序列化数据(如类实例)和响应式数据分开存储。可以将响应式数据存储在 state 中,而将非序列化数据存储在其他地方,通过 state 中的引用或标识符来关联它们。

// 假设这是一个类实例
class MyClass {
    constructor() {
        this.value = 0;
    }
    increment() {
        this.value++;
    }
}

// 创建类实例
const myInstance = new MyClass();

// 创建 Vuex store
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        // 存储响应式数据和对类实例的引用
        counter: 0,
        myInstanceRef: null
    },
    mutations: {
        incrementCounter(state) {
            state.counter++;
            // 如果需要,可以在这里调用类实例的方法
            if (state.myInstanceRef) {
                state.myInstanceRef.increment();
            }
        },
        setMyInstanceRef(state, instance) {
            state.myInstanceRef = instance;
        }
    },
    actions: {
        initMyInstance({ commit }) {
            // 初始化类实例并存储引用
            commit('setMyInstanceRef', myInstance);
        }
    }
});

// 在组件中使用
export default {
    created() {
        this.$store.dispatch('initMyInstance');
    },
    methods: {
        increment() {
            this.$store.commit('incrementCounter');
        }
    }
};
2. 使用 getter 和 action 进行交互

通过 getter 和 action 来访问和操作非序列化数据,避免直接在组件中操作这些数据。这样可以保持代码的一致性和可维护性。

const store = new Vuex.Store({
    state: {
        myInstanceRef: null
    },
    getters: {
        getMyInstanceValue: state => {
            if (state.myInstanceRef) {
                return state.myInstanceRef.value;
            }
            return null;
        }
    },
    actions: {
        updateMyInstanceValue({ state }, newValue) {
            if (state.myInstanceRef) {
                state.myInstanceRef.value = newValue;
            }
        }
    }
});

// 在组件中使用
export default {
    computed: {
        myInstanceValue() {
            return this.$store.getters.getMyInstanceValue;
        }
    },
    methods: {
        updateValue(newValue) {
            this.$store.dispatch('updateMyInstanceValue', newValue);
        }
    }
};
3. 清理和销毁

在不需要使用非序列化数据时,要及时清理和销毁相关的资源,避免内存泄漏。可以在组件销毁时调用相应的清理方法。

export default {
    beforeDestroy() {
        // 清理非序列化数据
        this.$store.commit('setMyInstanceRef', null);
    }
};

通过以上方法,可以安全地管理非序列化数据,避免在 state 中存储过多非响应式数据带来的问题。

mapStatemapGetters等辅助函数的实现原理是什么?如何在Vue3的Composition API中替代它们?

mapStatemapGetters 等辅助函数的实现原理

1. mapState

mapState 是 Vuex 提供的一个辅助函数,用于将 store 中的 state 映射到组件的计算属性中。其实现原理主要是利用 JavaScript 的对象解构和计算属性的特性。

在 Vue 组件里,计算属性是基于响应式依赖进行缓存的函数。mapState 函数会返回一个对象,对象的每个属性都是一个计算属性的配置项,这些计算属性会从 store 中获取相应的 state 值。

以下是简化的 mapState 实现原理示例:

function mapState(mappings) {
    const result = {};
    // 遍历传入的映射配置
    Object.keys(mappings).forEach(key => {
        const value = mappings[key];
        result[key] = function () {
            // 如果 value 是字符串,直接从 store.state 中获取对应的值
            if (typeof value === 'string') {
                return this.$store.state[value];
            } 
            // 如果 value 是函数,调用该函数并传入 store.state 作为参数
            else if (typeof value === 'function') {
                return value.call(this, this.$store.state);
            }
        };
    });
    return result;
}

使用示例:

const store = new Vuex.Store({
    state: {
        count: 0
    }
});

const Component = {
    computed: {
        ...mapState({
            localCount: 'count'
        })
    }
};

在上述代码中,mapState 函数返回一个对象,对象的 localCount 属性是一个计算属性,它会从 store.state 中获取 count 的值。

2. mapGetters

mapGetters 函数的实现原理与 mapState 类似,它用于将 store 中的 getters 映射到组件的计算属性中。

简化的 mapGetters 实现原理示例:

function mapGetters(mappings) {
    const result = {};
    Object.keys(mappings).forEach(key => {
        const value = mappings[key];
        result[key] = function () {
            // 如果 value 是字符串,直接从 store.getters 中获取对应的值
            if (typeof value === 'string') {
                return this.$store.getters[value];
            } 
            // 如果 value 是函数,调用该函数并传入 store.getters 作为参数
            else if (typeof value === 'function') {
                return value.call(this, this.$store.getters);
            }
        };
    });
    return result;
}

使用示例:

const store = new Vuex.Store({
    state: {
        count: 0
    },
    getters: {
        doubleCount: state => state.count * 2
    }
});

const Component = {
    computed: {
        ...mapGetters({
            localDoubleCount: 'doubleCount'
        })
    }
};

在上述代码中,mapGetters 函数返回一个对象,对象的 localDoubleCount 属性是一个计算属性,它会从 store.getters 中获取 doubleCount 的值。

在 Vue 3 的 Composition API 中替代它们

1. 替代 mapState

在 Vue 3 的 Composition API 中,可以使用 computed 函数来手动创建计算属性,从而替代 mapState

import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
    setup() {
        const store = useStore();
        // 手动创建计算属性
        const localCount = computed(() => store.state.count);
        return {
            localCount
        };
    }
};

在上述代码中,使用 computed 函数创建了一个计算属性 localCount,它会从 store.state 中获取 count 的值。

2. 替代 mapGetters

同样地,在 Vue 3 的 Composition API 中,可以使用 computed 函数来替代 mapGetters

import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
    setup() {
        const store = useStore();
        // 手动创建计算属性
        const localDoubleCount = computed(() => store.getters.doubleCount);
        return {
            localDoubleCount
        };
    }
};

在上述代码中,使用 computed 函数创建了一个计算属性 localDoubleCount,它会从 store.getters 中获取 doubleCount 的值。

通过这种方式,在 Vue 3 的 Composition API 中可以灵活地替代 mapState 和 mapGetters 等辅助函数。

Vuex的store是如何被注入到Vue组件中的?解释Vue.mixinbeforeCreate钩子在此过程中的作用。

Vuex 的 store 注入到 Vue 组件的整体流程

在 Vue 应用中使用 Vuex 时,需要把 store 注入到每个 Vue 组件里,这样组件就能访问 store 中的 stategettersmutations 和 actions 等。其注入过程主要依赖于 Vue.mixin 和 beforeCreate 钩子。

Vue.mixin 和 beforeCreate 钩子的作用

1. Vue.mixin

Vue.mixin 是一个全局混入方法,它会把传入的选项(如生命周期钩子、方法等)混入到每个 Vue 实例中。也就是说,当使用 Vue.mixin 时,所有的 Vue 组件都会继承这些混入的选项。

在 Vuex 里,借助 Vue.mixin 可以把一些公共的逻辑添加到每个组件中,这样就能确保每个组件都能访问到 store

2. beforeCreate 钩子

beforeCreate 是 Vue 实例生命周期中的一个钩子函数,它会在实例初始化之后、数据观测和 event/watcher 事件配置之前被调用。在 beforeCreate 钩子中,可以进行一些初始化操作,比如注入 store

具体实现原理

下面是简化后的代码,用于说明 store 是如何通过 Vue.mixin 和 beforeCreate 钩子注入到组件中的:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 创建 store 实例
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        }
    }
});

// 简化的 Vuex 注入逻辑
Vue.mixin({
    beforeCreate() {
        const options = this.$options;
        if (options.store) {
            // 如果当前实例的选项中有 store,说明是根实例,将 store 挂载到 this.$store 上
            this.$store = options.store;
        } else if (options.parent && options.parent.$store) {
            // 如果当前实例不是根实例,从父实例中获取 store 并挂载到 this.$store 上
            this.$store = options.parent.$store;
        }
    }
});

// 创建 Vue 实例
new Vue({
    store,
    render: h => h('div', 'Hello Vuex'),
}).$mount('#app');

代码解释

  1. Vue.mixin 调用:使用 Vue.mixin 全局混入一个对象,该对象包含 beforeCreate 钩子函数。

  2. beforeCreate 钩子逻辑

    • 根实例处理:在 beforeCreate 钩子中,首先检查当前实例的 $options 中是否有 store。如果有,说明当前实例是根实例,将 store 挂载到 this.$store 上。
    • 子实例处理:如果当前实例不是根实例,就从父实例中获取 store 并挂载到 this.$store 上。这样,所有的子组件都能通过 this.$store 访问到同一个 store 实例。

通过这种方式,store 就被注入到了每个 Vue 组件中,组件可以使用 this.$store 来访问 store 中的数据和方法。

从源码角度解释,为什么mutation通过commit调用而action通过dispatch调用?两者的执行上下文有何不同?

从 Vuex 的源码设计来看,mutation 通过 commit 调用而 action 通过 dispatch 调用的原因,主要源于两者的职责分离和执行机制的不同。以下是源码层面的详细解释:

一、commitdispatch 的源码实现差异

1. commit 方法的源码逻辑
  • 源码位置vuex/src/store.js
  • 核心流程
    commit (_type, _payload, _options) {
      // 标准化参数(类型、载荷、配置)
      const { type, payload, options } = unifyObjectStyle(_type, _payload, _options);
    
      const entry = this._mutations[type];
      if (!entry) {
        return;
      }
    
      // 遍历所有同名 mutation(支持多次注册)
      entry.forEach(function (handler) {
        handler(payload);
      });
    }
    
  • 关键点
    • commit 直接调用 _mutations 中注册的函数。
    • 同步执行handler(payload) 是同步调用的,无异步处理。
    • 参数传递:仅传递 payload,无上下文注入。
2. dispatch 方法的源码逻辑
  • 源码位置vuex/src/store.js
  • 核心流程
    dispatch (_type, _payload) {
      // 标准化参数(类型、载荷)
      const { type, payload } = unifyObjectStyle(_type, _payload);
    
      const entry = this._actions[type];
      if (!entry) {
        return Promise.reject(new Error(`unknown action type: ${type}`));
      }
    
      // 包装 action 执行结果为一个 Promise
      const result = entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload);
    
      return result.then(res => res[0]);
    }
    
  • 关键点
    • dispatch 调用 _actions 中的函数,并返回一个 Promise。
    • 异步支持:允许 action 返回 Promise,支持 async/await
    • 参数传递:传递 payload,并注入上下文对象(见下文)。

二、执行上下文的差异

1. Mutation 的执行上下文
  • 源码注入方式
    Mutation 的 handler 被调用时,第一个参数是当前模块的 state
    // 注册 mutation 时的源码片段(vuex/src/store.js)
    const entry = this._mutations[type] || (this._mutations[type] = []);
    entry.push(function wrappedMutationHandler (payload) {
      handler.call(store, local.state, payload);
    });
    
  • 上下文内容
    • 只有 statepayload,没有 commitdispatch 等 API。
    • 原因:Mutation 的职责是直接修改状态,无需其他操作。
2. Action 的执行上下文
  • 源码注入方式
    Action 的 handler 被调用时,第一个参数是包含模块局部上下文的对象:
    // 注册 action 时的源码片段(vuex/src/store.js)
    const entry = this._actions[type] || (this._actions[type] = []);
    entry.push(function wrappedActionHandler (payload) {
      const ctx = {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state
      };
      return handler.call(store, ctx, payload);
    });
    
  • 上下文内容
    • 包含 stategetters(局部和全局)、commitdispatch 等。
    • 原因:Action 需要处理异步逻辑,可能需要调用其他 action 或提交 mutation。

三、设计哲学与差异总结

特性MutationAction
调用方式commit('mutationName', payload)dispatch('actionName', payload)
职责同步修改状态处理异步逻辑、业务操作
执行机制同步直接调用可能异步,返回 Promise
执行上下文state + payload完整上下文(statecommitdispatch 等)
源码实现目标确保状态变更的原子性和可追踪性支持复杂逻辑和异步流程

四、为什么强制区分 commitdispatch

  1. 职责隔离
    • Mutation 专注于状态变更,Action 处理业务逻辑,符合单一职责原则。
  2. Devtools 追踪
    • Mutation 的同步特性使得 Vue Devtools 能准确记录状态变化快照。
  3. 可预测性
    • 强制通过 commit 修改状态,避免在 Action 中直接操作 state,确保状态变更路径清晰。

五、示例验证

1. Mutation 的同步性验证
mutations: {
  increment(state) {
    state.count++;
  }
},
actions: {
  asyncIncrement({ commit }) {
    setTimeout(() => {
      commit('increment'); // 正确:在异步回调中提交 mutation
    }, 1000);
  }
}
  • 结果:状态变更仍被 Devtools 记录,但 mutation 本身是同步执行的。
2. Action 的上下文访问
actions: {
  fetchData({ commit, dispatch, state }) {
    api.getData().then(data => {
      commit('SET_DATA', data); // 使用 commit
      dispatch('logAction');     // 使用 dispatch
    });
  }
}
  • 上下文:Action 可自由调用其他模块的 API,支持复杂协作。

六、总结

从源码角度看,commitdispatch 的设计差异体现了 Vuex 对 同步状态变更异步业务逻辑 的严格分离。

  • commit:直接操作状态,保证变更可追踪,无上下文注入。
  • dispatch:处理异步逻辑,提供完整上下文,返回 Promise。

这种设计确保了状态管理的可预测性和可维护性,是 Vuex 实现单向数据流的核心机制。

Vuex如何支持模块的热更新?写出一个热重载配置示例。

Vuex 支持模块热更新的原理

在开发环境中,为了提高开发效率,我们希望在修改 Vuex 模块代码时,能在不刷新整个页面的情况下更新模块内容。Vuex 支持模块的热更新,其原理基于 Webpack 的热模块替换(Hot Module Replacement,HMR)功能。当模块代码发生变化时,Webpack 会检测到这些变化,并通知 Vuex 重新加载相应的模块。

热重载配置示例

下面是一个完整的热重载配置示例,包含了 Vuex 模块和 Webpack 配置。

1. 项目结构

假设项目结构如下:

src/
├── main.js
├── store/
│   ├── index.js
│   └── modules/
│       ├── moduleA.js
│       └── moduleB.js
└── webpack.config.js
2. 编写 Vuex 模块

store/modules/moduleA.js

const moduleA = {
    namespaced: true,
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        }
    },
    actions: {
        incrementAsync({ commit }) {
            setTimeout(() => {
                commit('increment');
            }, 1000);
        }
    },
    getters: {
        doubleCount: state => state.count * 2
    }
};

export default moduleA;

store/modules/moduleB.js

const moduleB = {
    namespaced: true,
    state: {
        message: 'Hello from Module B'
    },
    mutations: {
        updateMessage(state, newMessage) {
            state.message = newMessage;
        }
    },
    actions: {
        changeMessage({ commit }, newMessage) {
            commit('updateMessage', newMessage);
        }
    },
    getters: {
        getMessage: state => state.message
    }
};

export default moduleB;
3. 编写 Vuex store

store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import moduleA from './modules/moduleA';
import moduleB from './modules/moduleB';

Vue.use(Vuex);

const store = new Vuex.Store({
    modules: {
        moduleA,
        moduleB
    }
});

// 支持热更新
if (module.hot) {
    module.hot.accept(['./modules/moduleA', './modules/moduleB'], () => {
        const newModuleA = require('./modules/moduleA').default;
        const newModuleB = require('./modules/moduleB').default;
        store.hotUpdate({
            modules: {
                moduleA: newModuleA,
                moduleB: newModuleB
            }
        });
    });
}

export default store;
4. 编写入口文件

src/main.js

import Vue from 'vue';
import App from './App.vue';
import store from './store';

new Vue({
    store,
    render: h => h(App)
}).$mount('#app');
5. 配置 Webpack

webpack.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode: 'development',
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        hot: true
    }
};

代码解释

  1. store/index.js 中的热更新逻辑

    • if (module.hot) 用于检查当前环境是否支持热更新。
    • module.hot.accept 方法监听 moduleA 和 moduleB 模块的变化。
    • 当模块发生变化时,使用 require 重新加载模块,并通过 store.hotUpdate 方法更新 store 中的模块。
  2. webpack.config.js 中的配置

    • webpack.HotModuleReplacementPlugin 插件用于启用热模块替换功能。
    • devServer.hot: true 配置开发服务器支持热更新。

通过以上配置,当修改 moduleA 或 moduleB 模块的代码时,Webpack 会检测到变化并通知 Vuex 重新加载相应的模块,从而实现模块的热更新。

如何实现“撤销/重做”(Undo/Redo)功能?设计一个基于Vuex的方案。

以下是基于Vuex实现撤销/重做(Undo/Redo)功能的完整方案:

一、核心设计思路

  1. 状态快照:每次状态变更时保存深拷贝的「状态快照」。
  2. 双栈管理:通过 undoStack(撤销栈)和 redoStack(重做栈)记录历史状态。
  3. 操作隔离:通过专用Mutation处理撤销/重做逻辑,避免循环记录。

二、Vuex Store 实现

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import _cloneDeep from 'lodash.clonedeep' // 使用深拷贝工具

Vue.use(Vuex)

const MAX_HISTORY = 50 // 最大历史记录数

export default new Vuex.Store({
  state: {
    // 应用主状态(示例)
    content: '初始文本',
    // 历史记录
    undoStack: [],
    redoStack: [],
  },
  mutations: {
    // 常规Mutation(业务操作)
    UPDATE_CONTENT(state, newContent) {
      state.content = newContent
    },

    // 专用Mutation:记录历史
    SAVE_SNAPSHOT(state) {
      // 限制栈长度
      if (state.undoStack.length >= MAX_HISTORY) {
        state.undoStack.shift()
      }
      state.undoStack.push(_cloneDeep(state.content))
      state.redoStack = [] // 新操作清空重做栈
    },

    // 专用Mutation:执行撤销
    UNDO(state) {
      if (state.undoStack.length === 0) return
      const currentState = _cloneDeep(state.content)
      state.redoStack.push(currentState)
      state.content = state.undoStack.pop()
    },

    // 专用Mutation:执行重做
    REDO(state) {
      if (state.redoStack.length === 0) return
      const currentState = _cloneDeep(state.content)
      state.undoStack.push(currentState)
      state.content = state.redoStack.pop()
    }
  },
  actions: {
    // 业务Action需手动触发快照
    updateContent({ commit }, newContent) {
      commit('UPDATE_CONTENT', newContent)
      commit('SAVE_SNAPSHOT')
    },

    // 撤销/重做Action
    undo({ commit }) {
      commit('UNDO')
    },
    redo({ commit }) {
      commit('REDO')
    }
  }
})

三、自动记录快照的插件实现(进阶)

// vuex-undo-redo-plugin.js
export default function createUndoRedoPlugin() {
  return store => {
    // 监听所有非历史操作的Mutation
    store.subscribe((mutation, state) => {
      if (!['SAVE_SNAPSHOT', 'UNDO', 'REDO'].includes(mutation.type)) {
        store.commit('SAVE_SNAPSHOT')
      }
    })
  }
}

// store.js中引入
import undoRedoPlugin from './vuex-undo-redo-plugin'
export default new Vuex.Store({
  // ...
  plugins: [undoRedoPlugin()]
})

四、组件中使用

<template>
  <div>
    <textarea v-model="content"></textarea>
    <button 
      @click="undo" 
      :disabled="!canUndo"
    >撤销</button>
    <button 
      @click="redo" 
      :disabled="!canRedo"
    >重做</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['content', 'undoStack', 'redoStack']),
    canUndo() {
      return this.undoStack.length > 0
    },
    canRedo() {
      return this.redoStack.length > 0
    }
  },
  methods: {
    ...mapActions(['updateContent', 'undo', 'redo']),
    // 输入框双向绑定
    content: {
      get() {
        return this.$store.state.content
      },
      set(value) {
        this.updateContent(value)
      }
    }
  }
}
</script>

五、关键优化点

  1. 性能优化

    • 使用 lodash.clonedeep 替代 JSON.parse(JSON.stringify()),正确处理复杂对象
    • 限制历史记录长度,避免内存溢出
  2. 状态隔离

    • 通过插件自动记录非历史操作Mutation
    • 专用Mutation避免循环触发快照
  3. 用户体验

    • 根据栈状态禁用按钮
    • 输入防抖(示例未展示,可结合Lodash)

六、扩展场景

1. 部分状态撤销
// 仅跟踪特定模块的状态
SAVE_SNAPSHOT(state) {
  const snapshot = {
    content: _cloneDeep(state.content),
    // 其他需要跟踪的state字段
  }
  state.undoStack.push(snapshot)
}
2. 操作合并
// 合并连续输入操作(通过时间阈值)
let lastSaveTime = 0
store.subscribe((mutation, state) => {
  if (Date.now() - lastSaveTime > 1000) { // 1秒内的操作合并
    store.commit('SAVE_SNAPSHOT')
    lastSaveTime = Date.now()
  }
})

七、方案对比

方案优点缺点
全量快照实现简单,状态恢复准确内存占用高,不适合大型状态树
差异记录内存优化,适合复杂应用实现复杂,需处理状态合并逻辑
操作命令模式精准控制可撤销粒度需为每个操作实现反向命令

通过此方案,可实现企业级的撤销/重做功能,适用于文档编辑、图形绘制、表单操作等场景,平衡性能与开发成本。

在搜索场景中,如何保证后发起的网络请求结果不会覆盖先发起的请求?(基于Vuex的action取消机制)

请求竞态处理

在搜索场景中,保证后发请求结果不覆盖先发请求的核心在于 “竞态处理”(Race Condition Handling)。通过 取消过时请求忽略过期响应 来实现。以下是基于 Vuex Action 和 Axios 的完整方案:

一、实现思路

  1. 为每个请求标记唯一标识(如时间戳或序列号)。
  2. 在发起新请求前,取消未完成的旧请求(利用 Axios 的 CancelToken)。
  3. 响应处理时,仅接受最新请求的结果(通过标识比对)。

二、代码实现

1. Vuex Store 配置
// store.js
import axios from 'axios';
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    searchResults: [],
    currentRequestId: 0,      // 当前请求的标识
    cancelRequest: null,      // 取消请求的函数
  },
  mutations: {
    SET_RESULTS(state, results) {
      state.searchResults = results;
    },
    SET_REQUEST_ID(state, id) {
      state.currentRequestId = id;
    },
    SET_CANCEL_FN(state, cancel) {
      state.cancelRequest = cancel;
    },
  },
  actions: {
    async fetchResults({ commit, state }, keyword) {
      // 1. 取消未完成的旧请求
      if (state.cancelRequest) {
        state.cancelRequest('请求被取消');
        commit('SET_CANCEL_FN', null);
      }

      // 2. 生成唯一请求标识
      const requestId = Date.now();
      commit('SET_REQUEST_ID', requestId);

      // 3. 创建 CancelToken
      const CancelToken = axios.CancelToken;
      const source = CancelToken.source();
      commit('SET_CANCEL_FN', source.cancel);

      try {
        const response = await axios.get('/api/search', {
          params: { q: keyword },
          cancelToken: source.token,
        });

        // 4. 仅处理最新请求的响应
        if (requestId === state.currentRequestId) {
          commit('SET_RESULTS', response.data);
        }
      } catch (error) {
        if (!axios.isCancel(error)) {
          console.error('搜索失败:', error);
        }
      }
    },
  },
});
2. 在组件中触发搜索
<template>
  <div>
    <input @input="handleSearch" placeholder="输入关键词搜索">
    <ul>
      <li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['searchResults']),
  },
  methods: {
    ...mapActions(['fetchResults']),
    // 防抖处理(可选)
    handleSearch: _.debounce(function(e) {
      this.fetchResults(e.target.value);
    }, 300),
  },
};
</script>

三、关键机制解析

1. 请求标识(requestId
  • 每个请求生成唯一标识(如时间戳),确保能区分新旧请求。
  • 响应处理时,仅当标识匹配当前最新请求时才更新数据。
2. 取消旧请求
  • 通过 Axios 的 CancelToken 取消未完成的请求。
  • 将取消函数存储在 Vuex 中,供下次请求前调用。
3. 竞态处理
  • 即使旧请求的响应晚于新请求到达,也会因标识不匹配而被忽略。

四、优化方案

1. 防抖(Debounce)
  • 使用 Lodash 的 _.debounce 减少高频触发请求的次数。
  • 防抖时间(如 300ms)根据实际场景调整。
2. 请求缓存
  • 对相同关键词的请求结果缓存,避免重复请求(需结合业务场景)。
3. 错误友好处理
  • 捕获取消请求的错误(axios.isCancel),避免污染日志。
  • 可添加加载状态提示(如 isLoading)。

五、方案优势

  1. 精准控制请求生命周期:确保只有最新请求影响视图。
  2. 低侵入性:基于现有技术栈(Vuex + Axios)实现,无需复杂改造。
  3. 高可靠性:通过标识比对,彻底解决竞态问题。

六、扩展场景

1. 并行请求竞争

若需要处理并行请求(如分页加载),可为每个请求单独管理标识和取消函数。

2. 组合式API(Vue3)

在 Vue3 的 setup() 中,通过 useStorewatchEffect 实现类似逻辑:

import { useStore } from 'vuex';
import { watchEffect } from 'vue';

export default {
  setup() {
    const store = useStore();
    let currentRequestId = 0;

    const handleSearch = (keyword) => {
      currentRequestId++;
      const requestId = currentRequestId;

      store.dispatch('fetchResults', keyword).then(() => {
        if (requestId === currentRequestId) {
          // 处理有效响应
        }
      });
    };

    return { handleSearch };
  },
};

通过此方案,可彻底解决搜索场景中的请求竞态问题,确保用户体验的一致性和数据准确性。

如何为Vuex模块提供完整的类型推断?写出一个类型安全的模块定义示例。

以下是为 Vuex 模块提供完整类型推断的步骤和示例,确保每个部分(State、Mutations、Actions、Getters)的类型安全:

1. 定义模块类型

State 类型

定义模块状态的接口:

// types/user.ts
export interface UserState {
  name: string;
  age: number | null;
  email: string;
}

export interface RootState {
  // 其他模块的 state 可在此定义(若有根模块)
}
Mutations 类型

使用 MutationTree<State> 定义 mutations 的类型:

import { MutationTree } from 'vuex';
import { UserState } from './types/user';

export type UserMutations = MutationTree<UserState> & {
  SET_NAME(state: UserState, payload: string): void;
  SET_AGE(state: UserState, payload: number): void;
};
Actions 类型

使用 ActionTree<State, RootState> 定义 actions 的类型:

import { ActionTree } from 'vuex';
import { UserState, RootState } from './types/user';

export type UserActions = ActionTree<UserState, RootState> & {
  fetchUser(context: ActionContext<UserState, RootState>, userId: number): Promise<void>;
  updateEmail(context: ActionContext<UserState, RootState>, newEmail: string): void;
};
Getters 类型

使用 GetterTree<State, RootState> 定义 getters 的类型:

import { GetterTree } from 'vuex';
import { UserState, RootState } from './types/user';

export type UserGetters = GetterTree<UserState, RootState> & {
  getUserName(state: UserState): string;
  isAdult(state: UserState): boolean;
};

2. 实现类型安全的模块

将类型应用到模块定义中:

// store/modules/user.ts
import { Module } from 'vuex';
import { UserState, RootState } from '../types/user';
import { UserMutations, UserActions, UserGetters } from '../types/user';

const state: UserState = {
  name: '',
  age: null,
  email: '',
};

const mutations: UserMutations = {
  SET_NAME(state, payload) { // payload 自动推断为 string
    state.name = payload;
  },
  SET_AGE(state, payload) { // payload 自动推断为 number
    state.age = payload;
  },
};

const actions: UserActions = {
  async fetchUser({ commit }, userId) { // userId 自动推断为 number
    const user = await fetchUserApi(userId);
    commit('SET_NAME', user.name);
    commit('SET_AGE', user.age);
  },
  updateEmail({ state }, newEmail) { // newEmail 自动推断为 string
    if (state.age! >= 18) {
      state.email = newEmail; // 直接修改需启用 strict mode
    }
  },
};

const getters: UserGetters = {
  getUserName: (state) => state.name.toUpperCase(), // 返回类型推断为 string
  isAdult: (state) => state.age !== null && state.age >= 18, // 返回类型推断为 boolean
};

const userModule: Module<UserState, RootState> = {
  namespaced: true,
  state,
  mutations,
  actions,
  getters,
};

export default userModule;

3. 创建全局 Store 并集成类型

定义根 Store 类型并初始化:

// store/index.ts
import { createStore, Store } from 'vuex';
import userModule, { UserState } from './modules/user';

// 定义根 State 类型(包含所有模块)
export interface RootState {
  user: UserState;
  // 其他模块...
}

const store = createStore<RootState>({
  modules: {
    user: userModule,
  },
});

export default store;

4. 在组件中使用类型安全的 Store

直接访问 Store

通过 useStore 和类型增强获得类型推断:

import { useStore } from 'vuex';
import { RootState } from '@/store';

export default {
  setup() {
    const store = useStore<RootState>();
    
    // 访问 State(自动推断类型)
    const userName = store.state.user.name; // string
    const userAge = store.state.user.age; // number | null

    // 调用 Action(需指定命名空间路径)
    store.dispatch('user/fetchUser', 123); // 参数 123 必须是 number

    // 调用 Mutation
    store.commit('user/SET_NAME', 'Alice'); // 参数 'Alice' 必须是 string

    return { userName };
  },
};
使用辅助函数(mapXXX)

通过自定义类型包装辅助函数:

import { mapState, mapActions } from 'vuex';
import { defineComponent } from 'vue';

export default defineComponent({
  computed: {
    // 映射 State(明确指定类型)
    ...mapState('user', {
      userName: (state: UserState) => state.name,
      userAge: (state: UserState) => state.age,
    }),
  },
  methods: {
    // 映射 Actions(指定命名空间)
    ...mapActions('user', ['fetchUser', 'updateEmail']),
    
    handleClick() {
      this.fetchUser(123); // 参数自动校验为 number
      this.updateEmail('alice@example.com'); // 参数自动校验为 string
    },
  },
});

5. 类型增强(可选)

若需更简洁的调用方式,可扩展 Vuex 类型声明:

// src/vuex.d.ts
import { Store } from 'vuex';
import { RootState } from '@/store';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: Store<RootState>;
  }
}

总结

通过上述步骤,Vuex 模块的每个部分(State、Mutations、Actions、Getters)均具备完整类型推断:

  1. State:明确定义接口,确保状态属性类型安全。
  2. Mutations/Actions:使用 MutationTreeActionTree 约束参数类型。
  3. Getters:通过 GetterTree 确保返回值类型正确。
  4. Store 集成:根 Store 类型聚合所有模块,组件中通过泛型或辅助函数获得类型提示。

优点

  • 减少手动类型声明的冗余代码。
  • 开发时自动补全和类型校验,避免低级错误。
  • 提升代码可维护性,尤其在大型项目中。

注意事项

  • 启用 Vuex 的 strict 模式,避免直接修改 State。
  • 若使用命名空间,需在调用时明确路径(如 'user/fetchUser')。

Pinia作为Vuex的替代方案,解决了哪些痛点?举例说明两者的API差异。

Pinia 作为 Vuex 的替代方案,主要解决了以下痛点,并通过更简洁的 API 设计提升了开发体验:

Pinia 解决的痛点

  1. 简化代码结构
    Vuex 需要定义 statemutationsactionsgetters 等多个概念,而 Pinia 合并了 mutationsactions,允许直接在 actions 中修改状态,减少模板代码。

  2. TypeScript 友好
    Pinia 的 API 设计对 TypeScript 支持更自然,无需复杂类型声明即可实现完整的类型推断。Vuex 需要额外配置或装饰器才能实现类似效果。

  3. 模块化更灵活
    Vuex 的模块需要嵌套在根模块中,且开启命名空间后需通过路径访问。Pinia 的每个 Store 都是独立的,天然支持扁平化模块管理,无需命名空间。

  4. 更轻量与直观
    Pinia 的 API 更符合 Vue 3 的组合式思想(Composition API),移除了 mutations 这类冗余概念,直接通过方法修改状态。

  5. DevTools 支持改进
    Pinia 默认集成 Vue DevTools,调试更直观,而 Vuex 需要额外配置。

API 差异示例

1. Store 定义
  • Vuex
    需要定义 statemutationsactions,并通过 new Vuex.Store() 创建实例:

    const store = new Vuex.Store({
      state: { count: 0 },
      mutations: {
        increment(state) { state.count++ }
      },
      actions: {
        asyncIncrement({ commit }) { commit('increment') }
      },
      getters: { double: (state) => state.count * 2 }
    });
    
  • Pinia
    使用 defineStore 定义 Store,合并 mutationsactions,直接通过方法修改状态:

    const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      actions: {
        increment() { this.count++ },       // 同步操作
        async asyncIncrement() { this.count++ } // 异步操作
      },
      getters: { double: (state) => state.count * 2 }
    });
    
2. 状态修改
  • Vuex
    必须通过 commit 触发 mutations,异步操作需在 actions 中处理:

    // 组件中
    this.$store.commit('increment');
    this.$store.dispatch('asyncIncrement');
    
  • Pinia
    直接调用 actions 或通过 $patch 修改状态,更符合直觉:

    const counterStore = useCounterStore();
    counterStore.increment();       // 直接调用 action
    counterStore.$patch({ count: 1 }); // 批量修改
    
3. 模块化
  • Vuex
    需要嵌套模块并手动启用命名空间:

    const userModule = {
      namespaced: true,
      state: { name: 'Alice' },
      mutations: { setName(state, name) { state.name = name } }
    };
    
    const store = new Vuex.Store({ modules: { user: userModule } });
    // 组件中使用
    this.$store.state.user.name;
    this.$store.commit('user/setName', 'Bob');
    
  • Pinia
    每个 Store 独立存在,按需引入:

    // stores/user.js
    const useUserStore = defineStore('user', {
      state: () => ({ name: 'Alice' }),
      actions: { setName(name) { this.name = name } }
    });
    
    // 组件中使用
    const userStore = useUserStore();
    userStore.name = 'Bob'; // 直接修改或调用 action
    
4. 组合式 API 集成
  • Vuex
    需通过 mapStatemapActions 等辅助函数映射到组件:
    import { mapState, mapActions } from 'vuex';
    
    export default {
      computed: { ...mapState(['count']) },
      methods: { ...mapActions(['asyncIncrement']) }
    };
    
    
  • Pinia
    直接通过函数调用和响应式解构:
    import { useCounterStore } from '@/stores/counter';
    
    export default {
      setup() {
        const counterStore = useCounterStore();
        const { count, double } = storeToRefs(counterStore);
        return { count, double, asyncIncrement: counterStore.asyncIncrement };
      }
    };
    

总结

Pinia 通过简化概念(如移除 mutations)、优化 TypeScript 支持、扁平化模块管理,以及更贴合 Vue 3 的组合式 API 设计,显著提升了状态管理的开发体验。其 API 设计更现代且易于维护,适合新项目或 Vuex 升级场景。

在什么场景下应该选择Vuex,而不是Event Bus或Provide/Inject?各方案的优缺点是什么?

在 Vue 应用中,选择 VuexEvent BusProvide/Inject 取决于具体场景和需求。以下是各方案的适用场景及优缺点分析:

1. Vuex

适用场景
  • 复杂状态共享:多个组件需要共享同一状态(如用户信息、全局配置)。
  • 状态持久化:需要跨路由或页面保持状态(如购物车数据)。
  • 可预测的状态变更:需要严格追踪状态变化的来源和流程(通过 mutationsactions)。
  • 调试需求:需要利用 Vue DevTools 追踪状态变更历史和时间旅行功能。
优点
  • 集中化管理:所有状态存储在单一 Store 中,便于维护和调试。
  • 数据流清晰:通过 mutations(同步)和 actions(异步)约束状态修改,避免直接操作。
  • 响应式支持:自动触发组件更新,无需手动处理依赖。
  • 模块化:支持分模块管理大型应用状态。
缺点
  • 复杂度高:小型项目可能引入不必要的模板代码(如定义 mutations)。
  • 学习成本:需要理解 statemutationsactionsgetters 等概念。
  • 与 Composition API 集成稍显繁琐:需要结合 useStore 或辅助函数使用。

2. Event Bus

适用场景
  • 简单跨组件通信:非父子关系的组件间传递一次性事件(如通知弹窗关闭)。
  • 临时通信需求:无需持久化状态的简单场景。
优点
  • 轻量级:无需引入额外库,直接使用 Vue 实例即可实现。
  • 快速实现:适合快速原型开发或简单场景。
缺点
  • 不可维护:事件名硬编码,容易导致全局命名冲突。
  • 调试困难:事件流难以追踪,容易引发“意大利面条式代码”。
  • 内存泄漏风险:未及时解绑事件监听会导致内存泄漏。
  • 无状态管理能力:仅传递事件,不存储数据。
示例代码
// 创建 Event Bus
const eventBus = new Vue();

// 组件 A 发送事件
eventBus.$emit('close-modal');

// 组件 B 监听事件
eventBus.$on('close-modal', () => { /* 处理逻辑 */ });

3. Provide/Inject

适用场景
  • 深层嵌套组件传值:父组件向深层子组件传递数据(如主题配置、用户权限)。
  • 局部状态共享:某个子树范围内的组件共享数据,无需全局状态。
优点
  • 避免逐层传递 Props:简化祖先组件与深层子组件的通信。
  • 响应式支持:若提供的是响应式对象(如 reactiveref),子组件可自动更新。
缺点
  • 单向数据流:父组件通过 provide 提供数据,子组件只能被动接收,难以反向修改。
  • 缺乏集中管理:数据分散在各层级组件中,难以维护和调试。
  • 非全局性:仅限同一组件树内使用,无法跨不同子树共享状态。
示例代码
// 父组件
export default {
  provide() {
    return { theme: reactive({ color: 'dark' }) };
  }
}

// 深层子组件
export default {
  inject: ['theme'],
  methods: {
    changeTheme() {
      this.theme.color = 'light'; // 若 theme 是响应式对象,可触发更新
    }
  }
}

选择方案对比表

场景VuexEvent BusProvide/Inject
状态共享范围全局任意组件间父子或深层嵌套组件
状态持久化✅ 支持❌ 不支持⚠️ 依赖组件生命周期
数据流可预测性✅ 高❌ 低⚠️ 中等(需手动约束)
调试支持✅ 完善❌ 困难⚠️ 一般(依赖响应式)
适用项目规模中大型项目小型临时需求局部数据传递
代码侵入性较高

何时选择 Vuex?

  1. 跨组件、跨页面的全局状态共享(如用户登录状态、多步骤表单数据)。
  2. 需要严格追踪状态变更历史(如撤销/重做功能)。
  3. 多人协作的大型项目,需要明确的代码规范和集中管理。

何时选择 Event Bus?

  1. 简单的一次性事件通知(如关闭弹窗、触发动画)。
  2. 临时需求或小型工具类项目,无需长期维护。

何时选择 Provide/Inject?

  1. 深层嵌套组件传递配置或主题(如国际化语言、UI 主题)。
  2. 避免 Props 逐层传递,提升代码简洁性。

总结

  • Vuex:适合复杂、全局的状态管理,优势在于可维护性和可预测性,但会引入一定复杂度。
  • Event Bus:轻量级解决简单通信需求,但缺乏状态管理和长期维护能力。
  • Provide/Inject:优雅解决深层组件传值问题,但不适合全局或频繁变更的状态。

推荐原则:优先使用最简单方案(如 Provide/Inject 或组件通信),仅在状态共享复杂到难以维护时引入 Vuex(或更现代的 Pinia)。

考察点总结

  • 对Vuex核心机制(响应式、单向数据流)的深度理解
  • 模块化设计能力与复杂状态架构经验
  • 插件开发、性能优化等高级技巧
  • 对Vue生态发展趋势(如Pinia)的认知
  • 解决实际问题的设计思维(如撤销重做、请求竞态)