深入分析 Vuex 源码中 `strict` 模式的实现原理,它如何检测非 `mutation` 修改 `state` 的情况。

好的,各位靓仔靓女,今天咱们来扒一扒 Vuex 源码里 strict 模式的底裤,看看它到底是怎么揪出那些偷偷摸摸修改 state 的坏蛋的!

开场白:strict 模式是个啥?

想象一下,Vuex 的 state 是你的银行账户,而 mutation 就像是银行柜台。你只能通过柜台(mutation)来存钱、取钱,一切操作都有记录,安全透明。

但是,总有些家伙想绕过柜台,直接用黑客手段修改你的账户余额。strict 模式就像银行的安全系统,一旦发现有人非法修改账户,立刻拉响警报!

简单来说,strict 模式就是 Vuex 提供的一种严格模式,当开启时,它会强制你只能通过 mutation 来修改 state。如果直接修改 state,就会抛出一个错误,让你知道哪里出了问题。

strict 模式的源码实现:一层层的监控

strict 模式的核心在于对 state 的深度监控。 Vuex 并没有使用什么黑魔法,而是巧妙地利用了 Vue 的响应式系统。

让我们一步步深入源码,揭开它的神秘面纱。

  1. createStore 阶段:埋下监控的种子

createStore 函数中,如果 strict 选项为 true,Vuex 就会设置 store._committing 标志为 false。 这个标志是一个开关,用来控制是否允许直接修改 state

function createStore (options = {}) {
  // ... 其他代码

  const strict = options.strict

  // ... 其他代码

  // store internal state
  this._committing = false  // 初始化为 false
  this._actions = Object.create(null)
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  this._watcherVM = new Vue() // 用于观察 state 的变化
  this._makeLocalGettersCache = Object.create(null)

  // bind commit and dispatch to self
  const local = this.dispatch = bind(this.dispatch, this)
  const commit = this.commit = bind(this.commit, this)

  // strict mode
  if (strict) {
    this.strict = true
  }

  // install module system
  installModule(this, state, [], this._modules.root, true)

  // initialize the store vm, which is the root of the whole tree.
  // the store vm is used to trigger mutations and actions.
  resetStoreVM(this, state)

  // enable devtools plugin
  if (devtools) {
    devtoolHook(this)
  }
}
  1. installModule 阶段:递归安装模块,并设置 state 的响应式

installModule 函数负责递归地安装所有模块,并将模块的 state 设置为响应式。 这就意味着 Vue 会监听 state 的任何变化。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state) //关键代码: 使用 Vue.set 使 state 变成响应式
    })
  }

  // ... 其他代码
}

关键代码是 Vue.set(parentState, moduleName, module.state)Vue.set 方法会确保 state 变成响应式对象,这样 Vue 才能监听它的变化。

  1. _withCommit 函数:控制 _committing 标志

_withCommit 函数是 strict 模式的核心。它负责在 mutation 执行前后设置 store._committing 标志。

_withCommit (fn) {
  const committing = this._committing
  this._committing = true // 设置为 true,表示允许修改 state
  fn()
  this._committing = committing // 恢复原来的值
}

mutation 执行前,_withCommit 会将 store._committing 设置为 true,表示允许修改 statemutation 执行完毕后,它会将 store._committing 恢复为原来的值(通常是 false)。

  1. commit 函数:触发 mutation,并使用 _withCommit

commit 函数负责触发 mutation。 在触发 mutation 之前,它会调用 _withCommit 函数,确保 store._committing 标志被正确设置。

commit (_type, _payload, _options) {
  // ... 其他代码

  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  // ... 其他代码
}
  1. resetStoreVM 函数:设置 watcher,监控 state 的变化

resetStoreVM 函数创建了一个 Vue 实例(store._watcherVM),并使用它来观察 state 的变化。

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public APIs
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving entire scope.
    // can be optimized via bind/call but it's not likely to be a bottleneck.
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  // using Vue.config.silent = true
  // Vue.config.silent = true  // 这行代码已经被注释掉了,所以不会抑制警告。
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store) //  关键代码:开启严格模式
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

如果开启了 strict 模式,resetStoreVM 还会调用 enableStrictMode 函数。

  1. enableStrictMode 函数:设置 watcher 的回调函数,检测非法修改

enableStrictMode 函数才是真正发挥作用的地方。它使用 Vue 的 watch 选项来监听 state 的变化,并在回调函数中检测是否允许修改 state

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (!store._committing) {
      warn(`Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

这段代码做了这些事情:

  • store._vm.$watch(function () { return this._data.$$state }, ...): 使用 Vue 实例的 $watch 方法来监听 state 的变化。 this._data.$$state 引用的是 Vuex 的 state 对象。
  • { deep: true, sync: true }: deep: true 表示深度监听,可以监听到 state 对象内部任何属性的变化。 sync: true 表示同步执行回调函数,这非常重要,因为我们需要立即检测到非法修改。
  • () => { if (!store._committing) { warn(...) } }: 这是回调函数。 它会检查 store._committing 标志。 如果 store._committingfalse,说明当前不是在 mutation 中修改 state,那么就抛出一个警告,告诉你不要在 mutation 之外修改 state

流程总结:strict 模式的工作原理

用一张表格来总结一下 strict 模式的工作流程:

步骤 描述 关键代码
1. createStore 创建 Vuex 实例,如果 stricttrue,则初始化 _committingfalse this._committing = false
2. installModule 递归安装模块,将 state 设置为响应式。 Vue.set(parentState, moduleName, module.state)
3. _withCommit 控制 _committing 标志,在 mutation 执行前后切换其值。 this._committing = true; ...; this._committing = committing
4. commit 触发 mutation,并使用 _withCommit 函数。 this._withCommit(() => { entry.forEach(...) })
5. resetStoreVM 创建 Vue 实例,并将 state 绑定到该实例。 this._vm = new Vue({ data: { $$state: state }, computed })
6. enableStrictMode 开启严格模式,使用 watcher 监听 state 的变化,检测非法修改。 store._vm.$watch(function () { return this._data.$$state }, () => { ... }, { deep: true, sync: true })

举个栗子:模拟 strict 模式

为了更好地理解 strict 模式,我们可以自己模拟一个简单的实现。

class MyVuex {
  constructor(options) {
    this.state = options.state;
    this.mutations = options.mutations;
    this._committing = false;
    this.strict = options.strict || false;

    // 使 state 变成响应式
    this.vm = new Vue({
      data: {
        $$state: this.state
      }
    });

    if (this.strict) {
      this.enableStrictMode();
    }
  }

  commit(mutationName, payload) {
    this._committing = true;
    this.mutations[mutationName](this.vm.$$state, payload);
    this._committing = false;
  }

  enableStrictMode() {
    this.vm.$watch(
      () => this.vm.$$state,
      () => {
        if (!this._committing) {
          console.warn("Do not mutate vuex store state outside mutation handlers.");
        }
      },
      { deep: true, sync: true }
    );
  }
}

// 使用示例
const store = new MyVuex({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  },
  strict: true // 开启 strict 模式
});

// 正确的修改方式
store.commit('increment', 1);
console.log(store.vm.$$state.count); // 输出 1

// 错误的修改方式 (会触发警告)
try {
  store.vm.$$state.count = 10; // 直接修改 state
} catch (e) {
    //Vue 在 strict 模式下会直接抛出异常,而不是警告
  console.error(e)
}
console.log(store.vm.$$state.count); // 输出 1 (因为Vuex strict模式会抛出异常,阻止了修改)

这个例子虽然简单,但它展示了 strict 模式的核心思想:

  1. 使用一个标志(_committing)来控制是否允许修改 state
  2. 使用 watcher 监听 state 的变化。
  3. watcher 的回调函数中检查是否允许修改 state,如果不允许,则抛出一个警告。

为什么 strict 模式使用 sync: true

sync: truestrict 模式的关键。 如果不使用 sync: truewatcher 的回调函数会在 Vue 的异步更新队列中执行。 这就意味着,当你在 mutation 之外修改 state 时,watcher 的回调函数可能不会立即执行,而是会在下一个事件循环中执行。

这样会导致一个问题:你在修改 state 之后,可能已经执行了其他的操作,而 watcher 的回调函数才抛出警告。 这会让调试变得非常困难,因为你可能不知道是哪个操作导致了 state 的非法修改。

使用 sync: true 可以确保 watcher 的回调函数立即执行,这样你就可以立即发现 state 的非法修改,并及时进行修复。

strict 模式的性能影响

strict 模式会增加一些性能开销,因为它需要使用 watcher 监听 state 的变化。 特别是在大型应用中,state 的数据量很大,strict 模式的性能开销可能会比较明显。

因此,通常只建议在开发环境中使用 strict 模式,而在生产环境中关闭它。

总结:strict 模式的意义

strict 模式是 Vuex 提供的一个非常有用的工具。 它可以帮助你:

  • 保持 state 的可预测性,避免意外的修改。
  • 提高代码的可维护性,更容易追踪 state 的变化。
  • 尽早发现问题,减少调试的时间。

虽然 strict 模式会增加一些性能开销,但在开发环境中开启它是非常有价值的。它可以帮助你编写更健壮、更可靠的 Vuex 应用。

好啦,今天的讲座就到这里。希望通过这次深入源码的探索,你对 Vuex 的 strict 模式有了更深刻的理解。 记住,安全第一,规范操作,才能构建出稳如老狗的 Vuex 应用! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注