解释 Vuex 源码中 `commit` 方法的实现,它如何触发 `mutations` 并更新 `state`。

各位同学,早上好!今天咱们来聊聊 Vuex 里面那个神秘又重要的家伙——commit。别看它名字平平无奇,实际上是 Vuex 状态管理的核心驱动力。想象一下,Vuex 就像一个精密的工厂,state 就是工厂里的原材料,mutations 则是生产线上的各种机器,而 commit 就是那个启动按钮,它按下之后,原材料才能被机器加工,最终变成我们想要的产品。

现在,咱们就深入到 Vuex 的源码里,看看 commit 这个按钮是怎么运作的。为了方便理解,我们一步一步来,先从一个简单的例子开始。

1. 从一个简单的例子开始

假设我们有一个 Vuex store,它有一个 state,一个 mutation,以及一个 action。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    incrementAsync ({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

export default store

在这个例子中,state.count 的初始值是 0,mutations.increment 的作用是将 state.count 加 1,actions.incrementAsync 的作用是在 1 秒后 commit increment mutation。

现在,我们可以在 Vue 组件中使用 commit 来触发 increment mutation,从而更新 state.count

// MyComponent.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapActions(['incrementAsync']),
    increment () {
      this.$store.commit('increment') // 这里就是 commit 的使用
    }
  }
}
</script>

在这个组件中,当点击 "Increment" 按钮时,会调用 this.$store.commit('increment'),这会触发 increment mutation,从而更新 state.count

2. commit 的调用栈

当我们调用 this.$store.commit('increment') 时,发生了什么呢?为了搞清楚这个问题,我们需要查看 Vuex 的源码。

commit 方法最终会调用到 store.js 中的 commit 函数。下面我们来逐步分析这个调用过程。

首先,我们需要找到 store.commit 的定义。

// vuex/src/store.js

commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = this._mutations[type]
  if (!mutation) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }

  try {
    this._committing = true
    mutation.forEach(handler => {
      handler(this.state, payload)
    })
  } finally {
    this._committing = false
  }

  if (this._subscribers.length > 0) {
    this._subscribers.forEach(sub => sub({ mutation: type, payload }, this.state))
  }
}

我们来逐行解读这段代码:

  • unifyObjectStyle(_type, _payload, _options): 这个函数的作用是统一 commit 的调用方式。commit 可以接受两种调用方式:

    • 字符串形式:commit('mutationName', payload)
    • 对象形式:commit({ type: 'mutationName', payload: payload })

    unifyObjectStyle 会将对象形式的调用转换为字符串形式,方便后续处理。

  • const mutation = this._mutations[type]: 这行代码从 this._mutations 对象中根据 type (mutation 的名字) 找到对应的 mutation 函数。 this._mutations 是一个对象,存储了所有定义的 mutations,key 是 mutation 的名字,value 是对应的函数。
  • if (!mutation) { ... }: 如果找不到对应的 mutation,会打印一个错误信息 (在非生产环境下),然后直接返回。
  • this._committing = true; ... finally { this._committing = false; }: 这是一个 try...finally 块。在 try 块中,将 this._committing 设置为 true,表示正在进行 mutation 操作。在 finally 块中,无论 try 块中的代码是否抛出异常,都会将 this._committing 设置为 falsethis._committing 是一个标志位,用于防止在 mutation 过程中直接修改 state。Vuex 规定只能通过 mutation 来修改 state。
  • mutation.forEach(handler => { handler(this.state, payload) }): 这行代码遍历找到的 mutation 函数 (因为同一个 mutation 名字可以注册多个处理函数),并依次调用它们。 handler 就是我们定义的 mutation 函数,它接收两个参数:this.state (当前的 state) 和 payload (传递给 mutation 的参数)。
  • if (this._subscribers.length > 0) { ... }: 如果存在订阅者 (通过 store.subscribe 注册的函数),则遍历所有订阅者,并调用它们。 订阅者可以监听 mutation 的触发,并在 mutation 发生后执行一些操作,例如记录日志、发送通知等。

3. _mutations 的构建

现在,我们知道了 commit 方法是如何触发 mutation 的,但是 this._mutations 这个对象是从哪里来的呢?

this._mutations 是在 Vuex.Store 构造函数中创建的。

// vuex/src/store.js

constructor (options = {}) {
  // ...

  // bind commit and dispatch to self.
  const store = this
  const { dispatch, commit } = this
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
  }
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
  }

  // strict mode
  this.strict = options.strict

  const state = options.state
  // install Modules
  installModule(this, state, [], options)

  // ...
}

可以看到,在 Vuex.Store 构造函数中调用了 installModule 函数。这个函数负责安装 module,并将 module 中的 mutations 注册到 this._mutations 对象中。

// vuex/src/module/module-collection.js

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

  // register in namespace map
  if (module.namespaced) {
    if (store._devtoolHook) {
      if (typeof module.namespaced !== 'boolean') {
        console.error(
          '[vuex] namespaced option should be boolean.'
        )
      }
      if (rootState && typeof rootState !== 'object') {
        console.error(
          '[vuex] root state must be a plain object.'
        )
      }
    }
  }

  // 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)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })

  // install plugins
  if (module.plugins) {
    module.plugins.forEach(plugin => plugin(store))
  }
}

installModule 函数中,module.forEachMutation 会遍历 module 中的所有 mutations,并调用 registerMutation 函数将它们注册到 store._mutations 对象中。

// vuex/src/store.js

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

registerMutation 函数将 mutation 函数包装成一个新的函数 wrappedMutationHandler,并将它添加到 store._mutations[type] 数组中。 这样,当我们调用 commit(type, payload) 时,实际上会调用 wrappedMutationHandler(payload),而 wrappedMutationHandler 会调用我们定义的 mutation 函数 handler,并将 statepayload 作为参数传递给它。

4. state 的更新

现在,我们知道了 commit 方法是如何触发 mutation 的,以及 mutation 函数是如何被调用的。 但是,mutation 函数又是如何更新 state 的呢?

答案就在 mutation 函数的定义中。 mutation 函数接收两个参数:statepayloadstate 是当前的 state 对象,我们可以直接修改它。

例如,在我们的例子中,increment mutation 的定义如下:

increment (state) {
  state.count++
}

这行代码直接修改了 state.count 的值,将其加 1。 由于 Vuex 使用了 Vue 的响应式系统,因此当 state.count 的值发生变化时,所有依赖于 state.count 的组件都会自动更新。

5. strict 模式

Vuex 提供了 strict 模式,用于强制通过 mutation 来修改 state。 当 strict 模式开启时,如果在 mutation 之外修改了 state,Vuex 会抛出一个错误。

// vuex/src/store.js

constructor (options = {}) {
  // ...

  // strict mode
  this.strict = options.strict

  // ...
}

// vuex/src/store.js

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

// vuex/src/store.js

enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (store._committing) {
      return
    }
    console.error('[vuex] Do not mutate vuex store state outside mutation handlers.')
  }, { deep: true, sync: true })
}

Vuex.Store 构造函数中,如果 strict 选项为 true,则会调用 enableStrictMode 函数。 enableStrictMode 函数会使用 vm.$watch 来监听 state 的变化。 如果 state 在 mutation 之外被修改,则会打印一个错误信息。

总结

我们来总结一下 commit 方法的实现:

  1. commit(type, payload) 方法接收 mutation 的名字 type 和 payload payload 作为参数。
  2. commit 方法首先从 this._mutations 对象中找到对应的 mutation 函数。
  3. 如果找不到对应的 mutation 函数,则打印一个错误信息并返回。
  4. commit 方法将 this._committing 设置为 true,表示正在进行 mutation 操作。
  5. commit 方法调用 mutation 函数,并将 statepayload 作为参数传递给它。
  6. mutation 函数直接修改 state 对象。
  7. commit 方法将 this._committing 设置为 false
  8. 如果存在订阅者,则遍历所有订阅者,并调用它们。

用表格来概括一下:

步骤 描述 相关代码
1 接收 mutation 类型和 payload commit (_type, _payload, _options)
2 查找对应的 mutation 函数 const mutation = this._mutations[type]
3 验证 mutation 是否存在 if (!mutation) { ... }
4 设置 _committing 标志位 this._committing = true; ... finally { this._committing = false; }
5 执行 mutation 函数 mutation.forEach(handler => { handler(this.state, payload) })
6 触发订阅者 if (this._subscribers.length > 0) { ... }
7 Mutation 注册到 _mutations registerMutation (store, type, handler, local), 构建 store._mutations[type] 数组。
8 state 的修改在 mutation 函数内部直接操作,Vue 的响应式系统会处理后续更新。 increment (state) { state.count++ }
9 strict 模式下,会监听 state 的变化,如果不是通过 mutation 修改的,会报错。 enableStrictMode (store)store._vm.$watch(function () { return this._data.$$state }, ...)

一点思考

Vuex 的设计思想非常简洁明了。 它将 state 的管理集中到一个地方,并通过 mutation 来控制 state 的修改。 这使得我们可以更好地理解和调试我们的应用程序。

当然,Vuex 也有一些缺点。 例如,当 state 非常大时,mutation 的触发可能会导致性能问题。 另外,Vuex 的学习曲线也比较陡峭,特别是对于初学者来说。

但是,总的来说,Vuex 仍然是一个非常优秀的 state 管理工具,值得我们学习和使用。

好了,今天的讲座就到这里。 希望大家通过今天的学习,对 Vuex 的 commit 方法有了更深入的了解。 记住,理解源码是提升编程能力的关键! 下课!

发表回复

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