一篇文章让你彻底了解vuex的使用及原理(上)

这篇具有很好参考价值的文章主要介绍了一篇文章让你彻底了解vuex的使用及原理(上)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

vuex详解

文章讲解的Vuex的版本为4.1.0,会根据一些api来深入源码讲解,帮助大家更快掌握vuex的使用。

vuex的使用

使用Vue实例的use方法把Vuex实例注入到Vue实例中。

const store = createStore({...})
createApp(App).use(store)

use方法执行的是插件的中的install方法

src/store.js

export class Store {
  // ...
  // app 是vue实例
  install(app, injectKey) {
    app.provide(injectKey || storeKey, this)
    app.config.globalProperties.$store = this
    ...
  }
}

从上面可以看到Vue实例通过 provide方法把 store 实例 provide 到了根实例中。同时添加了一个全局变量$store,在每个组件中都可以通过this.$store来访问store实例。

app.provide 是给 Composition API 方式编写的组件用的,因为一旦使用了 Composition API ,我们在组件中想访问 store 的话会在 setup 函数中通过 useStore API 拿到.

import { useStore } from 'vuex'
export default {
  setup() {
    const store = useStore()
  }
}

useStore的实现也在src/injectKey.js文件中:

import { inject } from 'vue'
export const storeKey = 'store'
export function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

Vuex 就是利用了 provide/inject 依赖注入的 API 实现了在组件中访问到 store,由于是通过 app.provide 方法把 store 实例 provide 到根实例中,所以在 app 内部的任意组件中都可以 inject store 实例并访问了。

除了 Composition APIVue3.x 依然支持 Options API 的方式去编写组件,在 Options API 组件中我们就可以通过 this.$store 访问到 store 实例,因为实例的查找最终会找到全局 globalProperties 中的属性(globalProperties添加一个可以在应用的任何组件实例中访问的全局 property。组件的 property 在命名冲突具有优先权)。

provide/injectVuex 中的作用就是让组件可以访问到 store 实例。

响应式状态

state

state中设置的属性是响应式的

使用:

// 设置state
const state = createStore({
  state: {
    value: 1
  }
})

// 在组件上使用
const Com = {
  template: `<div>{{ $store.state.value }}</div>`,
}

之后会在Store类中调用一个resetStoreState方法,将传入的state通过reactive方法设置成响应式。

export function resetStoreState (store, state, hot) {
  ...
  store._state = reactive({
    data: state
  })
  ...
}

getters

可以从 store 中的 state 中派生出一些状态。

// 设置state和getters
const store = new Vuex.Store({
  state: {
    value: 1,
  },
  getters: {
    getterVal(state, getters) {
      return state.value + 1;
    }
  }
})

// 在组件上使用
const Com = {
  template: `<div>{{$store.getters.getterVal}}</div>`
}

也可以通过让getter返回一个函数来实现给getter传递参数。

// 设置state和getters
const store = new Vuex.Store({
  state: {
    value: 1,
  },
  getters: {
    getterVal(state, getters) {
      return function(num) {
        return state.val + num;
      };
    }
  }
})

// 在组件上使用
const Com = {
  template: `<div>{{$store.getters.getterVal(3)}}</div>`
}

同时vuex内部对getter有进行缓存处理,只有当依赖的state发生改变后才会重新收集。

来看看vuex内部是如何实现getter的:

  1. Store中会调用installModule会对传入的模块进行处理(详情请看下面的模块化部分,里边会对模块里的getters处理)
// src/store-util.js
// 对所有模块中的getters进行处理
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

function registerGetter (store, type, rawGetter, local) {
  // 去重处理
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 往store实例的_wrappedGetters存储getters,这样就可以通过store.getters['xx/xxx']来访问
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 调用模块中设置的getter方法,可以看到这里是传入了4个参数
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
  1. 在类Store中调用resetStoreState方法中对使用computed方法对getter进行计算后缓存。
export function resetStoreState (store, state, hot) {
  ...
  const wrappedGetters = store._wrappedGetters
  const scope = effectScope(true)
  scope.run(() => {
    forEachValue(wrappedGetters, (fn, key) => {
      computedObj[key] = partial(fn, store)
      computedCache[key] = computed(() => computedObj[key]())
      Object.defineProperty(store.getters, key, {
        get: () => computedCache[key].value,
        enumerable: true
      })
    })
  })
  ...
}

Mutations

更改 Vuexstore 中的状态的唯一方法是提交 mutationVuex 中的 mutation 非常类似于事件:每个 mutation 都有一个事件类型 (type)和一个回调函数 (handler)。这个回调函数就是实际进行状态更改的地方。

const store = createStore({
  state: {
    value: 1
  },
  mutations: {
    increment (state, payload) {
      state.value++
    }
  }
})

installModule方法中,对模块中的mutations进行注册,

// src/store-util.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)
  })
}

把模块中的mutations全部添加到Store实例中的_mutations对象中,将mutations中的this指向Store实例,并传入两个参数:

  • state: 当前模块的state(具体详解请看模块化部分)
  • payload: 通过commit中传入的参数。

像上面的demo_mutations值为{increment: [fn]}

commit触发mutation

语法:
commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)

通过使用commit方法来触发对应的mutation

store.commit('increment');

commit可以接受额外的参数(payload)来为mutation传入参数。

const store = createStore({
  state: {
    value: 1
  },
  mutations: {
    increment (state, num) {
      state.value += num;
    }
  }
})

store.commit('increment', 2);

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读

在类Store中定义了commit方法,因为commit方法内部需要使用到Store实例中的方法,因此需要使用call方法把this指向Store实例。

// src/store.js
export class Store {
  constructor (options = {}) {
    this._mutations = Object.create(null)
    // 改变this
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
  }
  ...
  commit (_type, _payload, _options) {
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    ...

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

// src/store-util.js
export function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  return { type, payload, options }
}

可以看到在处理传入commit的参数时Vuex进行了处理。可以往commit传入对象形式的配置。

const store = createStore({
  state: {
    value: 1
  },
  mutations: {
    increment (state, payload) {
      state.value += payload.value;
    }
  }
})
store.commit({
  type: 'increment',
  value: 1
})

mutation中必须是同步函数,如果mutation是一个异步函数,异步修改状态,虽然也会使状态正常更新,但是会导致开发者工具有时无法追踪到状态的变化,调试起来就会很困难。

订阅mutations

Vuex提供了subscribe方法用来订阅 storemutation,当mutations执行完后就会触发订阅回调。

语法:subscribe(handler, options)

  • handler: 处理函数
  • options: 配置
    • prepend: Boolean值,把处理函数添加到订阅函数链的最开始。
const store = createStore(...)

// subscribe方法的返回值是一个取消订阅的函数
const unsubscribe = store.subscribe((mutation, state) => {
  console.log(mutation)
})
// 你可以调用 unsubscribe 来停止订阅。
// unsubscribe()

const unsubscribe2 = store.subscribe((mutation, state) => {
  console.log('在订阅函数链头部,最先执行')
}, {prepend: true})

订阅的源码实现非常简单,使用一个数组来维护订阅函数。在触发commit方法内部添加执行订阅方法即可。

// src/store.js
export class Store {
  constructor (options = {}) {
    this._subscribers = [];
    ...
  }
  commit() {
    ...
    const mutation = { type, payload }
    // 执行mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 在mutation执行完后执行订阅
    this._subscribers
      .slice()
      .forEach(sub => sub(mutation, this.state))
  }
  subscribe (fn, options) {
    return genericSubscribe(fn, this._subscribers, options)
  }
  ...
}

// src/store-util.js
export function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    // 如果传入了prepend: true 把处理函数添加到订阅函数链的最开始
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  // 返回一个取消订阅的方法
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

Actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
const store = createStore({
  state: {
    value: 1
  },
  mutations: {
    increment (state, payload) {
      state.value += payload.value
    }
  },
  actions: {
    emitIncrement(context, payload) {
      context.commit('increment', payload);
      // action同样可以改变state中的数据,但是最好不要这样使用,在严格模式下控制台会抛异常且action是异步的,不方便DevTool 调试
      // context.state.value++;
    }
  },
})

installModule方法中,对模块中的actions进行注册。跟mutations一样的是也是也把所有的action方法放在一个变量_actions下,同时this也指向Store实例,并传入两个参数:

  • context: 当前模块的上下文(具体详解请看模块化部分)
  • payload: 通过dispatch中传入的参数。
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 强行转化为promise,在dispatch就可以统一处理
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    ...
    return res
  })
}

值得注意的是在action执行完后是返回一个Promise格式的值。详细看下面的dispatch部分。

dipatch分发actions

语法:
dispatch(type: string, payload?: any, options?: Object): Promise<any>
dispatch(action: Object, options?: Object): Promise<any>

store.dispatch('emitIncrement', {value: 1})
// 对象形式
store.dispatch({
  type: 'emitIncrement',
  value: 1
})

通过使用dispatch方法来触发对应的action

// src/store.js
export class Store {
  dispatch (_type, _payload) {
    // 跟mutations一样处理传入对象的形式
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    ...

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
    ...
  }
}

可以看到在执行action时如果对应的action数组中存在多个,用了Promise.all来处理,这是为了当type对应的所有的action都执行完后才继续执行这样才能保证订阅是在action执行之后触发。

订阅actions

mutation一样,action也可以添加订阅

语法:subscribeAction(handler: Function, options?: Object): Function

返回值也是一个取消订阅方法。

  • handler: 处理函数
  • options: 配置
    • before: 在action执行前触发(默认)
    • after: 在action执行后触发
    • error: 捕获分发 action 的时候被抛出的错误
    • prepend: Boolean值,把处理函数添加到订阅函数链的最开始。

相比起mutation的订阅,action的订阅较为复杂一点。

store.subscribeAction((action, state) => {
  console.log(action)
})

store.subscribeAction((action, state) => {
  console.log('在订阅函数链头部,最先执行')
}, {prepend: true})

store.subscribeAction({
  before: (action, state) => {
    console.log(`before action`)
  },
  after: (action, state) => {
    console.log(`after action`)
  },
  error: (action, state, error) => {
    console.log(`error action`)
    console.error(error)
  }
})

来看看订阅action的添加和触发相关的实现原理:

export class Store {
  subscribeAction (fn, options) {
    // 当传入的第一个参数为函数时,封装成{before:fn}的形式
    const subs = typeof fn === 'function' ? { before: fn } : fn
    // 把订阅方法放入_actionSubscribers数组中
    return genericSubscribe(subs, this._actionSubscribers, options)
  }
  dispatch (_type, _payload) {
    const action = { type, payload }
    const entry = this._actions[type]
    ...

    // 执行before相关订阅
    try {
      this._actionSubscribers
        .slice()
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    }
    ...

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    // 返回一个promise
    return new Promise((resolve, reject) => {
      // 对于同步函数而已,因此在注册阶段就直接使用Promise.resolve处理能异步函数了,所以统一使用then获取执行结果
      result.then(res => {
        try {
          // 执行after订阅者
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        }
        ...
        resolve(res)
      }, error => {
        try {
          // 执行error订阅者
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        }
        ...
        reject(error)
      })
    })
  }
}

模块化

如果只使用单一状态树,应用的所有状态就会集中到一个比较大的对象,store 对象就有可能变得相当臃肿。Vuex 可以允许将 store 分割成模块(module)。每个模块甚至是嵌套子模块拥有 stategetters等。

例如

const nestedModule = {
  namespaced: true,
  state: {
    a: 1
  },
  getters: {},
  mutations: {},
  actions: {},
}
const module = {
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    nested: nestedModule
  }
}
const store = createStore(module);
store.state.nested  // {a: 1} 获取嵌套module的状态

模块解析

Vuex实例创建时,会对传入的模块进行解析

export class Store {
  constructor(options) {
    this._modules = new ModuleCollection(options)
    installModule(this, state, [], this._modules.root)
  }
}

初始化ModuleCollection时会对模块进行注册:

// src/module/module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }
  register (path, rawModule, runtime = true) {
    // 将每个模块使用一个module对象来展示
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 将嵌套模块添加到父模块的_children属性中
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 注册嵌套模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

最后形成一个便于Vuex处理的对象:

this._modules = {
  root: {
    runtime: Boolean,
    state: {},
    _children: [],
    _rawModule: {},
    namespaced: Boolean
  }
}

installModule方法非常重要,里边会对嵌套module进行解析,使得嵌套内部可以在调用dispatchcommit的时候能找到对应的actionmutation

// src/store-utils.js installModule
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)

if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    ...
    // 将子模块的state添加到父模块的state中
    parentState[moduleName] = module.state
  })
}

namespaced的作用:
在子模块中添加namespaced: true,根模块的会根据模块的modules字段中的key作为索引将子模块中mutationsactionsgetters存储在根模块中。例如:

mutations: {'childModule/module1', [fn]}

在调用commit或者dispatch方法的时候就可以根据这个key找到对应子模块中的mutationaction
如果子模块中没有设置namespaced: true,那么在根模块中的_actions等字段的key就为对应的子模块中的actions里定义的key,如果模块非常多的时候,容易造成命名冲突。而vuex也考虑到这种情况,因此key对应的值的类型为数组,这样即使冲突了,也会执行数组中所有的方法,不过这样就会导致触发其他模块中非必要的actionmutation等。

// 如两个子模块都存在一个increment的mutation,同时都没有设置 namespaced: true,那么在根模块的_mutations就为
_mutations: { 'increment', [fn1, fn2] }
// 调用 store.commit('increment'),fn1, fn2都会执行

将子模块的state添加到父模块的state中,这样就可以通过store.state.childModule.state.xxx进行访问。

为模块创建了一个执行上下文,当触发commit或者dispatch时就会根据namespaced找到对应的模块中的mutations或者actions中的方法。文章来源地址https://www.toymoban.com/news/detail-422278.html

// src/store-utils.js installModule
// 为当前模块添加一个上下文
const local = module.context = makeLocalContext(store, namespace, path)

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

// src/store-utils.js
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''
  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      ...

      if (!options || !options.root) {
        type = namespace + type
        ...
      }
      return store.dispatch(type, payload)
    },
    // commit 类似
  }
  ...

  return local
}

到了这里,关于一篇文章让你彻底了解vuex的使用及原理(上)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 这篇文章,让你了解ERC-1155 多代币标准协议

    用于多种代币管理的合约标准接口。 单个部署的合约可以包括同质化代币、非同质化代币或其他配置(如半同质化代币)的任何组合。 ERC1155 的显着特点是它使用单个智能合约一次代表多个代币。这就是为什么它的balanceOf功能不同于 ERC20 和 ERC777 的原因:它有一个额外的id参

    2024年02月01日
    浏览(25)
  • 通过一篇文章让你了解Linux的重要性

    Linux是一种自由和开放源代码的操作系统,由林纳斯·托瓦兹于1991年首次发布。它基于Unix,具有模块化设计,支持多任务和多用户,能在多种硬件平台上运行。Linux系统在全球范围内得到广泛应用,包括服务器、移动设备、嵌入式系统等领域。其强大的功能、稳定性和安全性

    2024年04月15日
    浏览(24)
  • 一篇文章让你了解ADAS-HIL测试方案

    ADA S (Advanced Driber Assistant System),高级驾驶辅助系统, 先进驾驶辅 助系统,作用于辅助汽车驾驶,通过感知、决策和执行,帮助驾驶员察觉可能发生的危险,是提高安全性的主动安全技术,保障行驶安全,已成当前汽车装载必备系统;并普遍认为是实现自动驾驶AD的过程性

    2023年04月08日
    浏览(23)
  • 一篇文章让你了解nginx和lua脚本(Nginx详解)

    静态资源部署 Rewrite地址重写 正则表达式 反向代理 负载均衡 轮询、加权轮询、ip_hash、url_hash、fair Web缓存 环境部署 高可用的环境 用户认证模块… nginx二进制可执行文件 nginx.conf配置文件 error.log错误的日志记录 access.log访问日志记录 首先我们来学习下,我们的配置文件,n

    2024年02月10日
    浏览(23)
  • 一篇文章彻底了解网络字节序和主机字节序,初学者进来,不走弯路

    目录 1.什么是字节序? 2.大端字节序和小端字节序 3.主机字节序和网络字节序 4.不同类型数据传输处理流程对比 5.设计一个小程序来判断当前机器的字节序? 6.大小端转换方法? 字节序,字节在内存中排列顺序 计算机存储数据方式是从内存增长方向存储 图 1 计算机存储方式 网

    2024年02月03日
    浏览(28)
  • 你真的学懂if语句了嘛,看完这篇文章你一定会让你有所收获,彻底玩转if语句!

    🎬 鸽芷咕 :个人主页  🔥 个人专栏 :《C语言初阶篇》 《C语言进阶篇》 ⛺️生活的理想,就是为了理想的生活!    🌈 hello! 各位宝子们大家好啊,相信大家都多多少少了解过if语句吧,但是你真的有了解过,所有if语句的细节吗?学完这篇文章你将知道if语句的所有知识

    2024年02月11日
    浏览(25)
  • 【Spring框架】一篇文章带你彻底搞懂Spring解决循环依赖的底层原理

    目录 一、前言 二、什么是循环依赖 三、Spring Bean 的循环依赖问题 3.1 Bean 的创建步骤 3.2 为什么 Spring Bean 会产生循环依赖问题? 3.3 什么情况下循环依赖可以被处理? 四、Spring 如何解决循环依赖问题? 4.0 什么是三级缓存 4.1 简单的循环依赖(没有AOP) 4.1.0 创建Bean的前期流

    2024年04月17日
    浏览(33)
  • 一篇文章带你了解-selenium工作原理详解

    前言 Selenium是一个用于Web应用程序自动化测试工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。 主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得

    2024年02月10日
    浏览(28)
  • C++初阶之一篇文章让你掌握vector(理解和使用)

    在C++中,std::vector是标准模板库(STL)中的一种动态数组容器,它可以存储任意类型的元素,并且能够自动调整大小。std::vector提供了许多方便的成员函数,使得对数组的操作更加简单和高效。 vector声明 : template class T, class Alloc = allocatorT ; 这是 std::vector 的一般模板定义。它

    2024年02月14日
    浏览(21)
  • 一篇文章让你完全掌握使用Git推送代码到新版GitCode

    GitCode是一款基于Git的在线代码托管和协作工具,提供代码版本控制、代码托管、代码评审、项目管理等功能。它支持多种编程语言,包括 Java 、 Python 、 C++ 等,可帮助开发者高效协作,提高代码质量和开发效率。GitCode还提供丰富的 API 接口,支持与其他系统集成,方便开发

    2024年03月25日
    浏览(22)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包