Geeks_Z の Blog Geeks_Z の Blog
首页
  • 学习笔记

    • 《HTML》
    • 《CSS》
    • 《JavaWeb》
    • 《Vue》
  • 后端文章

    • Linux
    • Maven
    • 汇编语言
    • 软件工程
    • 计算机网络概述
    • Conda
    • Pip
    • Shell
    • SSH
    • Mac快捷键
    • Zotero
  • 学习笔记

    • 《数据结构与算法》
    • 《算法设计与分析》
    • 《Spring》
    • 《SpringMVC》
    • 《SpringBoot》
    • 《SpringCloud》
    • 《Nginx》
  • 深度学习文章
  • 学习笔记

    • 《PyTorch》
    • 《ReinforementLearning》
    • 《MetaLearning》
  • 学习笔记

    • 《高等数学》
    • 《线性代数》
    • 《概率论与数理统计》
  • 增量学习
  • 哈希学习
GitHub (opens new window)

Geeks_Z

AI小学生
首页
  • 学习笔记

    • 《HTML》
    • 《CSS》
    • 《JavaWeb》
    • 《Vue》
  • 后端文章

    • Linux
    • Maven
    • 汇编语言
    • 软件工程
    • 计算机网络概述
    • Conda
    • Pip
    • Shell
    • SSH
    • Mac快捷键
    • Zotero
  • 学习笔记

    • 《数据结构与算法》
    • 《算法设计与分析》
    • 《Spring》
    • 《SpringMVC》
    • 《SpringBoot》
    • 《SpringCloud》
    • 《Nginx》
  • 深度学习文章
  • 学习笔记

    • 《PyTorch》
    • 《ReinforementLearning》
    • 《MetaLearning》
  • 学习笔记

    • 《高等数学》
    • 《线性代数》
    • 《概率论与数理统计》
  • 增量学习
  • 哈希学习
GitHub (opens new window)
  • Linux

  • Java

  • 微服务笔记

  • MySQL

  • Nginx

  • HTML

  • CSS

  • JavaWeb

  • Vue

    • Start
    • Node和NPM
    • 快速入门
    • Vue实例
    • 组件化
    • 模块化
    • webpack
    • vue-cli
    • Promies
    • Vuex
      • 什么是 Vuex
        • 使用场景
        • 数据流层
        • 注意事项
      • 核心概念
        • state
        • 在 Vue 组件中获得 Vuex 状态
        • mapState 辅助函数
        • Getter
        • 通过属性访问
        • 通过方法访问
        • mapGetters 辅助函数
        • Mutation
        • 提交载荷(Payload)
        • 对象风格的提交方式
        • Mutation 需遵守 Vue 的响应规则
        • 使用常量替代 Mutation 事件类型
        • Mutation 必须是同步函数
        • 在组件中提交 Mutation
        • Action
        • 分发 Action
        • 在组件中分发 Action
        • 组合 Action
        • Module
        • 模块的局部状态
        • 命名空间
      • Vuex 项目开发中常见的文件布局
        • 项目结构
        • 文件的说明
      • Vuex 的简单案例
        • 目录结构
        • 新建 store 存储于 vuex 相关
        • state.js
        • getters.js
        • mutations-types.js
        • mutations.js
        • index.js
        • 在 main.js 中注册 store
        • 在 App.vue 中使用
        • 结果
      • Vuex 工作原理详解
        • 理解 computed
        • vue 中 data 属性和 computed 相关的源代码
        • initData
        • initComputed
        • defineComputed所代理属性的 get 方法
        • 获取依赖并更新的过程
        • vuex 插件
    • Axios
    • vue-router
    • ref $refs(元素、组件引用 )
    • 生命周期
    • Mix(混入)配置项
    • 安装插件 use()
    • 功能样式
    • 项目开发配置
    • 样式绑定
    • 事件
  • Git

  • 开发规范

  • SpringCloud微服务权限系统

  • bug

  • Software

  • ProgramNotes
  • Vue
Geeks_Z
2022-10-24
目录

Vuex

Vuex

什么是 Vuex

Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

其实最简单理解为,在我们写 Vue 组件中,一个页面多个组件之间想要通信数据,那你可以使用Vuex

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
  • Vuex 状态管理 管理组件数据流动 局数据管理
  • Vue 的全局数据池,在这里它存放着大量的复用或者公有的数据,然后可以分发给组件
  • Vue 双向数据绑定的 MV 框架,数据驱动(区别节点驱动),模块化和组件化,所以管理各组件和模块之间数据的流向至关重要
  • Vuex 是一个前端非持久化的数据库中心,Vuex 其实是 Vue 的重要选配,一般小型不怎么用,大型项目运用比较多,所以页面刷新,Vuex 数据池会重置

路由-》管理的是组件流动

Vuex-》管理的是数据流动

没有Vuex之前,组件数据来源

  • ajax 请求后端
  • 组件自身定义默认数据
  • 继承其他组件的数据
  • (从 vuex 拿)

使用场景

  • 多个视图使用于同一状态
传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力

1
2
  • 不同视图需要变更同一状态:
采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝,通常会导致无法维护的代码

1
2

数据流层

注意事项

  1. 数据流都是单向的
  2. 组件能够调用 action
  3. action 用来派发 mutation
  4. 只有 mutation 可以改变状态
  5. store 是响应式的,无论 state 什么时候更新,组件都将同步更新

核心概念

state

Vuex 使用单一状态树,用一个对象就包含了全部的应用层次状态。至此它便作为一个唯一的数据源而存在。这也意味着,每个应用将仅仅包含一个 store 实例。

单状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

在 Vue 组件中获得 Vuex 状态

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

// 创建一个 Counter 组件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count() {
      return store.state.count
    },
  },
}

//每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
1
2
3
4
5
6
7
8
9
10
11

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):

const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `,
})
1
2
3
4
5
6
7
8
9
10
11

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。让我们更新下 Counter 组件 的实现:

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count() {
      return this.$store.state.count
    },
  },
}
1
2
3
4
5
6
7
8

mapState 辅助函数

当一个组件需要获取多个状态时,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState(state) {
      return state.count + this.localCount
    },
  }),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count',
])
1
2
3
4

由于 mapState 函数返回的是一个对象,在 ES6 的写法中,我们可以通过对象展开运算符,可以极大的简化写法:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

//相当于将 state的属性,都添加到computed,而且指向state中的数据

1
2
3
4
5
6
7
8
9
10

Getter

用来从 store 获取 Vue 组件数据,类似于 computed。

Getter 接受 state 作为其第一个参数:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false },
    ],
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13

通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
1

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
1
2
3
4
5
6

在其他组件中使用 getter:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

1
2
3
4
5
6

注意: getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

通过方法访问

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

getters: {
  // ...
  getTodoById: state => id => {
    return state.todos.find(todo => todo.id === id)
  }
}

store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
1
2
3
4
5
6
7
8

注意: getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
    // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ]),
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果你想将一个 getter 属性另取一个名字,使用对象形式:

mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount',
})
1
2
3
4

Mutation

事件处理器用来驱动状态的变化,类似于 methods,同步操作。

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。

每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1,
  },
  mutations: {
    increment(state, value) {
      // 变更状态
      state.count++
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11

当外界需要通过 mutation 的 handler 来修改 state 的数据时,不能直接调用 mutation 的 handler,而是要通过 commit 方法传入类型。

store.mutations.increment,这种方式是错误的,必须使用 store.commit('increment',value) ,value 可作为要传递进入 store 的数据

提交载荷(Payload)

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):

// ...
mutations: {
  increment (state, value) {
  //第一个参数是state,value可以作为传递进来数据的参数
    state.count += value
  }
}

1
2
3
4
5
6
7
8

使用方式:

store.commit('increment', 10)
1

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

...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

1
2
3
4
5
6
7
// 以载荷形式分发
store.commit('increment', {
  amount: 10,
})
1
2
3
4

对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

// 以对象形式分发
store.commit({
  type: 'increment',
  amount: 10,
})
1
2
3
4
5

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

1
2
3
4
5
6

Mutation 需遵守 Vue 的响应规则

既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。

  1. 最好提前在你的 store 中初始化好所有所需属性
  2. 使用 Vue.set(obj, 'newProp', 123)
  3. 以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:state.obj = { ...state.obj, newProp: 123 }

使用常量替代 Mutation 事件类型

  1. 新建 mutation-types.js 文件,定义常量来管理 mutation 中的类型:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
1
2

或者直接导出对象

export default {
  SOME_MUTATION: 'SOME_MUTATION',
}
1
2
3
  1. 在 store.js 中引入 mutation-types.js,引入类型常量使用
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14

引入类型对象使用:

...
import  MutationType from './mutation-type'
 mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [MutationType.SOME_MUTATION] (state) {
      // mutate state
    }
  }

1
2
3
4
5
6
7
8
9
  1. 在外部使用时,需要局部先引入或者在main.js全局引入mutation-types.js:
import MutationType from './mutation-type'

this.$store.commit(MutationType.SOME_MUTATION, '传入内容')
1
2
3

Mutation 必须是同步函数

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

1
2
3
4
5
6
7
8

假设现在正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。 每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。 然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:

因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

在组件中提交 Mutation

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

方式一:

this.$store.commit('increment', '参数')
1

方式二:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy', // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment', // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    }),
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Action

可以给组件使用的函数,以此用来驱动事件处理器 mutations,异步操作。

Action 类似于 mutation,不同在于:

  1. Action 提交的是 mutation,而不是直接变更状态。
  2. Action 可以包含任意异步操作。

例子:

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++
    },
  },
  actions: {
    increment(context) {
      //context 执行的上下文,作为第一个参数
      context.commit('increment')
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

需要调用 commit 很多次的时候,可以简写成:

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

1
2
3
4
5
6

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')
1

Action 就不受约束!在 Mutation 无法执行的异步操作,可以在 action 内部进行使用:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

1
2
3
4
5
6
7
8

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10,
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10,
})
1
2
3
4
5
6
7
8
9
10

调用异步 API 和分发多重 mutation:

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求,然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在组件中分发 Action

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy', // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment', // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    }),
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

1
2
3
4
5
6
7
8
9
10
11

现在可以直接使用:

store.dispatch('actionA').then(() => {
  // ...
})
1
2
3

在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

1
2
3
4
5
6
7
8
9

最后,如果我们利用 async / await,我们可以如下组合 action:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

1
2
3
4
5
6
7
8
9
10
11
12

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

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

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

假设模块A state 中 有 ‘city’,在外界访问时,则用 store.state.a.city

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment(state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    },
  },

  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    },
  },
}
1
2
3
4
5
6
7
8
9
10

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount(state, getters, rootState) {
      return state.count + rootState.count
    },
  },
}
1
2
3
4
5
6
7
8

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

  1. 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。
  2. 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced属性后不需要修改模块内的代码。

Vuex 项目开发中常见的文件布局

项目结构

https://gitee.com/geeks_z/MacImages/raw/master/202204071013511.png

文件的说明

1、一般会在 vue 的项目下 src 文件中创建一个 store 存放项目中使用的 vuex 相关的文件 2、 actions 存放全部的异步的或者多个 mutations 的方法 3、getters 存放全部的 getter 方法 4、index 对外暴露的文件 5、mutations-type 存放一些常量 6、mutations 存放全部修改 state 的方法 7、state 项目中全部的状态

Vuex 的简单案例

目录结构

https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200521115616.png

新建 store 存储于 vuex 相关

state.js

/**
 * 定义项目中state状态的文件
 */
const state = {
  count: 0,
  show: '',
}

export default state
1
2
3
4
5
6
7
8
9

getters.js

/**
 * 定义项目中的getters,这个里面设置的是获取store中的状态
 * 其实都是些函数,从state状态中返回数据,
 * 然后在一般的组件中使用mapGetters就可以获取到数据,
 * 里面可以对state进行操作,然后返回出去
 */

export const counts = state => state.count
export const show = state => state.show
1
2
3
4
5
6
7
8
9

mutations-types.js

/**
 * 定义项目中mutations-types的常量
 */
// 增加
export const INCREMENT = 'INCREMENT'
// 减少
export const DECREMENT = 'DECREMENT'
// 改变文本
export const CHANGE_TEXT = 'CHANGE_TEXT'
1
2
3
4
5
6
7
8
9

mutations.js

import * as types from './mutations-types'

const mutations = {
  [types.INCREMENT](state) {
    state.count++
  },
  [types.DECREMENT](state) {
    state.count--
  },
  [types.CHANGE_TEXT](state, v) {
    state.show = v
  },
}

export default mutations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

index.js

import Vue from 'vue'
import Vuex from 'vuex'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'

//使用插件vuex
Vue.use(Vuex)

export default new Vuex.Store({
  getters,
  state,
  mutations,
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在 main.js 中注册 store

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

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10

在 App.vue 中使用

<template>
  <div id="app">
    <div class="store">
      <p>
        {{counts}}
      </p>
      <button @click="handleIncrement"><strong>+</strong></button>
      <button @click="handleDecrement"><strong>-</strong></button>
      <hr>
      <h3>{{show}}</h3>
      <input
              placeholder="请输入内容"
              v-model="obj"
              @change="changObj"
              clearable>
      </input>
    </div>
  </div>
</template>
<script>
  // 获取状态
  import {mapGetters,mapMutations} from 'vuex';
  import * as types from './store/mutations-types';
  export default {
    name: 'app',
    data(){
      return {
        obj: ''
      }
    },
    computed:{
      ...mapGetters([
        'counts',
        'show'
      ])
    },
    methods:{
      handleIncrement(){
        this.setIncrement()
      },
      handleDecrement(){
        this.setDecrement()
      },
      changObj(){
        this.setChangeText(this.obj)
      },
      ...mapMutations({
        setIncrement: types.INCREMENT,
        setDecrement: types.DECREMENT,
        setChangeText: types.CHANGE_TEXT,
      })
    }
  }
</script>
<style>
  .store{
    text-align: center;
  }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

结果

https://gitee.com/geeks_z/MacImages/raw/master/202204071013954.png

Vuex 工作原理详解

理解 computed

Computed 计算属性是 Vue 中常用的一个功能,但你理解它是怎么工作的吗?

拿官网简单的例子来看一下:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
1
2
3
4
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello',
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join()
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13

vue 的 computed 是如何更新的,为什么当 vm.message 发生变化时,vm.reversedMessage 也会自动发生变化?

vue 中 data 属性和 computed 相关的源代码

// src/core/instance/state.js
// 初始化组件的state
export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  // 当组件存在data属性
  if (opts.data) {
    initData(vm)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
  // 当组件存在 computed属性
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

initState方法当组件实例化时会自动触发,该方法主要完成了初始化 data,methods,props,computed,watch 这些我们常用的属性,我们来看看我们需要关注的initData和initComputed

initData

// src/core/instance/state.js
function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  // .....省略无关代码

  // 将vue的data传入observe方法
  observe(data, true /* asRootData */)
}

// src/core/observer/index.js
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // ...省略无关代码
  ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在初始化的时候 observe 方法本质上是实例化了一个 Observer 对象,这个对象的类是这样的

// src/core/observer/index.js
export class Observer {
  value: any
  dep: Dep
  vmCount: number // number of vms that has this object as root $data

  constructor(value: any) {
    this.value = value
    // 关键代码 new Dep对象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // ...省略无关代码
    this.walk(value)
  }

  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 给data的所有属性调用defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在对象的构造函数中,最后调用了walk方法,该方法即遍历 data 中的所有属性,并调用defineReactive方法,defineReactive方法是vue实现 MDV(Model-Driven-View)的基础,本质上就是代理了数据的 set,get 方法,当数据修改或获取的时候,能够感知。我们具体看看defineReactive的源代码

// src/core/observer/index.js
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重点,在给具体属性调用该方法时,都会为该属性生成唯一的dep对象
  const dep = new Dep()

  // 获取该属性的描述对象
  // 该方法会返回对象中某个属性的具体描述
  // api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果该描述不能被更改,直接返回,因为不能更改,那么就无法代理set和get方法,无法做到响应式
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  // 重新定义data当中的属性,对get和set进行代理。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // 收集依赖, reversedMessage为什么会跟着message变化的原因
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知依赖进行更新
      dep.notify()
    },
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

我们可以看到,在所代理的属性的get方法中,当 dep.Target 存在的时候会调用dep.depend()方法,这个方法非常的简单,不过在说这个方法之前,我们要认识一个新的类Dep

Dep 是 vue 实现的一个处理依赖关系的对象, 主要起到一个纽带的作用,就是连接 reactive data 与 watcher,代码非常的简单

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher
  id: number
  subs: Array<Watcher>

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 更新 watcher 的值,与 watcher.evaluate() 类似,
      // 但 update 是给依赖变化时使用的,包含对 watch 的处理
      subs[i].update()
    }
  }
}

// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []

export function pushTarget(_target: Watcher) {
  // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
  // 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget() {
  // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
  Dep.target = targetStack.pop()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

代码非常的简单,回到调用dep.depend()方法的时候,当Dep.Target存在,就会调用,而depend方法则是将该 dep 加入watcher的newDeps中,同时,将所访问当前属性的dep对象中的subs插入当前 Dep.target 的 watcher.看起来有点绕,不过没关系,我们一会跟着例子讲解一下就清楚了。

讲完了代理的 get,方法,我们讲一下代理的 set 方法,set 方法的最后调用了dep.notify(),当设置 data 中具体属性值的时候,就会调用该属性下面的dep.notify()方法,通过class Dep了解到,notify 方法即将加入该 dep 的 watcher 全部更新,也就是说,当你修改data中某个属性值时,会同时调用dep.notify()来更新依赖该值的所有watcher。

initComputed

initComputed这条线,这条线主要解决了什么时候去设置Dep.target的问题

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
  // 初始化watchers列表
  const watchers = (vm._computedWatchers = Object.create(null))
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 关注点1,给所有属性生成自己的watcher, 可以在this._computedWatchers下看到
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    }

    if (!(key in vm)) {
      // 关注点2
      defineComputed(vm, key, userDef)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在初始化 computed 时,有 2 个地方需要去关注

  1. 对每一个属性都生成了一个属于自己的 Watcher 实例,并将 **{ lazy: true }**作为 options 传入
  2. 对每一个属性调用了 defineComputed 方法(本质和 data 一样,代理了自己的 set 和 get 方法,我们重点关注代理的get方法)

我们看看Watcher的构造函数

// src/core/observer/watcher.js
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // 如果初始化lazy=true时(暗示是computed属性),那么dirty也是true,需要等待更新
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 在computed实例化时,将具体的属性值放入this.getter中
    // 省略不相关的代码
    this.value = this.lazy
      ? undefined
      : this.get()
  }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

除了日常的初始化外,还有 2 行重要的代码

this.dirty = this.lazy this.getter = expOrFn

1
2

在computed生成的watcher,会将 watcher 的 lazy 设置为 true,以减少计算量。因此,实例化时,this.dirty也是 true,标明数据需要更新操作。我们先记住现在computed 中初始化对各个属性生成的 watcher 的 dirty 和 lazy 都设置为了 true。同时,将 computed 传入的属性值**(一般为funtion),放入watcher的getter**中保存起来。

defineComputed所代理属性的 get 方法

// src/core/instance/state.js
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果找到了该属性的watcher
    if (watcher) {
      // 和上文对应,初始化时,该dirty为true,也就是说,当第一次访问computed中的属性的时候,会调用 watcher.evaluate()方法;
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当第一次访问 computed 中的值时,会因为初始化watcher.dirty = watcher.lazy的原因,从而调用evalute()方法,evalute()方法很简单,就是调用了 watcher 实例中的get方法以及设置dirty = false,我们将这两个方法放在一起

// src/core/instance/state.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}

get () {
// 重点1,将当前watcher放入Dep.target对象
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 重点2,当调用用户传入的方法时,会触发什么?
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相关代码
  }
  return value
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在 get 方法中中,第一行就调用了pushTarget方法,其作用就是将Dep.target设置为所传入的 watcher,即所访问的computed中属性的watcher, 然后调用了value = this.getter.call(vm, vm)方法,想一想,调用这个方法会发生什么?

this.getter 在 Watcher 构建函数中提到,本质就是用户传入的方法,也就是说,this.getter.call(vm, vm)就会调用用户自己声明的方法,那么如果方法里面用到了 this.data中的值或者其他被用 defineReactive 包装过的对象,那么,访问 this.data.或者其他被defineReactive包装过的属性,是不是就会访问被代理的该属性的 get 方法。我们在回头看看 get方法是什么样子的。

注意:我讲了其他被用 defineReactive,这个和后面的 vuex 有关系,我们后面在提

get: function reactiveGetter() {
  const value = getter ? getter.call(obj) : val
  // 这个时候,有值了
  if (Dep.target) {
    // computed的watcher依赖了this.data的dep
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
    }
    if (Array.isArray(value)) {
      dependArray(value)
    }
  }
  return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代码注释已经写明了,就不在解释了,这个时候我们走完了一个依赖收集流程,知道了 computed 是如何知道依赖了谁。最后根据this.data所代理的set方法中调用的notify,就可以改变this.data的值,去更新所有依赖this.data值的 computed 属性 value 了。

获取依赖并更新的过程

那么,我们根据下面的代码,来简易拆解获取依赖并更新的过程

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello',
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join()
    },
  },
})
vm.reversedMessage // =>  olleH
vm.message = 'World' //
vm.reversedMessage // =>  dlroW
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 初始化 data 和 computed,分别代理其 set 以及 get 方法, 对 data 中的所有属性生成唯一的 dep 实例。
  2. 对 computed 中的 reversedMessage 生成唯一 watcher,并保存找 vm._computedWatchers 中
  3. 访问 reversedMessage,设置 Dep.target 指向 reversedMessage 的 watcher,调用该属性具体方法reversedMessage。
  4. 方法中访问 this.message,即会调用 this.message 代理的 get 方法,将 this.message 的dep加入输入 reversedMessage 的watcher,同时该 dep 中的subs添加这个watcher
  5. 设置vm.message = 'World',调用 message 代理的 set 方法触发dep 的 notify方法
  6. 因为是 computed 属性,只是将watcher中的dirty设置为 true
  7. 最后一步vm.reversedMessage,访问其 get 方法时,得知reversedMessage的watcher.dirty为 true,调用**watcher.evaluate()**方法获取新的值。

这样,也可以解释了为什么有些时候当 computed 没有被访问(或者没有被模板依赖),当修改了this.data值后,通过 vue-tools 发现其computed中的值没有变化的原因,因为没有触发到其get方法。

vuex 插件

我们知道,vuex 仅仅是作为 vue 的一个插件而存在,不像 Redux,MobX 等库可以应用于所有框架,vuex 只能使用在 vue 上,很大的程度是因为其高度依赖于 vue 的 computed 依赖检测系统以及其插件系统,

通过官方文档 (opens new window)我们知道,每一个 vue 插件都需要有一个公开的 install 方法,vuex 也不例外。其代码比较简单,调用了一下 applyMixin 方法,该方法主要作用就是在所有组件的beforeCreate生命周期注入了设置this.$store这样一个对象。

// src/store.js
export function install(_Vue) {
  if (Vue && _Vue === Vue) {
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
1
2
3
4
5
6
7
8
// src/mixins.js
// 对应applyMixin方法
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init ? [vuexInit].concat(options.init) : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit() {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function' ? options.store() : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

我们在业务中使用 vuex 需要类似以下的写法

const store = new Vuex.Store({
  state,
  mutations,
  actions,
  modules,
})
1
2
3
4
5
6

那么 Vuex.Store到底是什么样的东西呢?我们先看看他的构造函数

// src/store.js
constructor (options = {}) {
  const {
    plugins = [],
    strict = false
  } = options

  // store internal state
  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  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()

  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 = strict

  const state = this._modules.root.state

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)

  // 重点方法 ,重置VM
  resetStoreVM(this, state)

  // apply plugins
  plugins.forEach(plugin => plugin(this))

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

除了一堆初始化外,我们注意到了这样一行代码resetStoreVM(this, state) 他就是整个 vuex 的关键

// src/store.js
function resetStoreVM(store, state, hot) {
  // 省略无关代码
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed,
  })
}
1
2
3
4
5
6
7
8
9
10
11

去除了一些无关代码后我们发现,其本质就是将我们传入的 state 作为一个隐藏的 vue 组件的 data,也就是说,我们的 commit 操作,本质上其实是修改这个组件的 data 值,结合上文的 computed,修改被defineReactive代理的对象值后,会将其收集到的依赖的watcher中的dirty设置为 true,等到下一次访问该 watcher 中的值后重新获取最新值。

这样就能解释了为什么 vuex 中的 state 的对象属性必须提前定义好,如果该state中途增加一个属性,因为该属性没有被defineReactive,所以其依赖系统没有检测到,自然不能更新。

由上所说,我们可以得知store._vm.$data.$$state === store.state, 我们可以在任何含有 vuex 框架的工程得到这一点

https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200521124912.png

vuex 整体思想诞生于flux,可其的实现方式完完全全的使用了 vue 自身的响应式设计,依赖监听、依赖收集都属于 vue 对对象 Property set get 方法的代理劫持。最后一句话结束 vuex 工作原理,vuex中的store本质就是没有template的隐藏着的vue组件;

#Vue
上次更新: 2025/02/26, 08:57:57
Promies
Axios

← Promies Axios→

最近更新
01
RAIL
02-26
02
IOCTF
02-25
03
DGM
02-25
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Geeks_Z | MIT License
京公网安备 11010802040735号 | 京ICP备2022029989号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式