vuex 理解

简单解释


简单的理解就是你在state中定义了一个数据之后,你可以在所在项目中的任何一个组件里进行获取、进行修改,并且你的修改可以得到全局的响应变更。

核心思想


Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应 的组件也会相应地得到高效更新。
  • 你不能直接改变 store
    中的状态。改变 store
    中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具如vue-devtool帮助我们更好地了解我们的应用。另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码会将变得更结构化且易维护。

vuex工作流程


image.png

vuex文件目录


src├── 
    ├── index.html
    ├── main.js
    ├── components
    └── store
        ├── index.js          # 我们组装模块并导出 store 的地方
        ├── state.js          # 根级别的 state
        ├── getters.js        # 根级别的 getter
        ├── mutation-types.js # 根级别的mutations名称(官方推荐mutions方法名使用大写)
        ├── mutations.js      # 根级别的 mutation
        ├── actions.js        # 根级别的 action
        └── modules
            ├── m1.js         # 模块1
            └── m2.js         # 模块2

Vuex包含的五个基本对象


  • State:单一状态树。
  • Getter:计算属性。
  • Mutation: 用于提交更改store中的状态(mutation是更改store中状态的唯一方法)。
  • Action:用于提交mutation,可以包含异步操作。
  • Module:当应用程序很大时,需要管理的状态很多时,需要将state进行拆分,分割成模块(modules),最后统一管理。

项目安装与引入


首先在项目中安装vuex

npm install vuex --save
注意:这里一定要加上–save因为这个包我们在生产环境中也要使用。

然后 在src文件目录下新建一个名为store的文件夹,为方便引入并在store文件夹里新建一个index.js,里面的内容如下:

import Vue from "vue";
import Vuex from "vuex";
import state from "./state";
import mutations from "./mutations";
import getters from "./getters";
import actions from "./actions";
import userinfo from "./module/userinfo";
Vue.use(Vuex);

const store = new Vuex.Store({
  state, // state:state  的简写
  getters,
  mutations,
  actions,
  modules: {
    userinfo
  }
});

export default store;

接下来,在 main.js里面引入store,然后再全局注入一下,这样一来就可以在任何一个组件里面使用this.$store了:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/index"; // 引入store
Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

说了上面的前奏之后,接下来就是纳入正题了,就是开篇说的state的玩法。

State


简单理解:在 store 中的 state 对象,可以理解为 Vue 实例中的 data 对象,它用来保存最基本的数据。

声明
export default {
  nums: 0, // 数量
  price: 100, // 金额
  count: 0, // 总计
  obj: {} // 定义一个空对象
}

在Vue中获取store中的状态:

<template>
  <div class="hello">
    <div>{{price}}</div>
  </div>
</template>
<script>
import store from '../store/index.js';   // 1. 对象字面量方法需引入store
export default {
  computed: {
      // 1. 对象字面量方法获取
      // price () {
      //   return store.state.price
      // },
      // 2.通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 			this.$store 访问到。
        price () {
          return this.$store.state.price
         }
    }
}
</script>

实际上做完上面的步骤你已经可以用this.$store.state.price在任何一个组件里面获取price定义的值了,但这不是理想的获取方式;当数据多时这种方法明显效率过低,所以 Vuex 中提供了 mapState 方法用于批量映射 store 中的状态。

MapState映射

<template>
  <div class="hello">
    <div>{{a}}{{b}}</div>
  </div>
</template>
<script>
import { mapState} from 'vuex';
export default {
  computed: {
      // 3.辅助函数
      ...mapState({
        a: state => state.nums,
        b: 'price'
      }),
    }
}
</script>

上例中a. 可以通过 ES6 中的箭头函数进行数据的映射,b. 当计算属性的名称与 state 的属性名一致时可能直接通过字符串赋值。

如果所有计算属性的名称都与 state 一致,可以在 mapState 中以数组的方式进行映射。如果 Vue 中已经存在计算属性,可以通过 ES6 的对象展开运算符 (…) 进行组合。

<template>
  <div class="hello">
    <div>{{nums}}{{price}}</div>
  </div>
</template>
<script>
import { mapState} from 'vuex';
export default {
  computed: {
    // 3.辅助函数
    ...mapState(['nums', 'price'])
  }
}
</script>

在 Vuex 模块化中,state 是唯一会根据组合时模块的别名来添加层级的,后面的 getters、mutations 以及 actions 都是直接合并在 store 下**。**

Mutations


简单理解:在vuex中,更改state 的方式只有提交mutation.大家可以把他就想象成vue中methods 中的一个方法。

状态提交
// mutation.js
import * as types from './mutation-types'
export default {
  [types.SET_NUMS] (state, nums) {
    // 必写state
    state.nums = nums
  },
  [types.SET_PRICE] (state, price) {
    state.price = price
  },
  // 改变state状态
  increments (state) {
    state.count = state.count + 1
  },
  // 提交载荷
  incrementsTwo (state, payload) {
    state.count += payload
  },
  // 提交载荷
  incrementsThree (state, payload) {
    state.count = state.count + payload.num1
  },
  // 对象风格的传参
  incrementsFour (state, payload) {
    state.count = state.count + payload.num1 + payload.num2
  }
}
// HelloWorld.vue文件
<template>
  <div class="hello">
     <div @click="increments">mutation提交</div>
     <div>mutation提交的值:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  methods() {
    increments () {
      this.$store.commit('increments')
    }
  }
}
</script>

想要改变状态的时候都是用this.$store.commit的方式

传参方式

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

第一种方式:提交载荷(Payload)
你可以向 this.$store.commit 传入额外的参数,即 mutation 的 载荷(payload):

// HelloWorld.vue文件
<template>
  <div class="hello">
     <div @click="incrementsTwo">mutation提交载荷按钮</div>
     <div>mutation提交载荷的状态:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  methods() {
    incrementsTwo () {
      this.$store.commit('incrementsTwo', 10)
    }
  }
}
</script>

官方推荐,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

// xxx.vue文件
<template>
  <div class="hello">
     <div @click="incrementsThree">mutation对象方式提交载荷按钮</div>
     <div>mutation对象方式提交载荷的状态:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  methods() {
    incrementsThree () {
      this.$store.commit('incrementsThree', {num1: 30})
    }
  }
}
</script>

**第二种方式:对象风格的传参方式
**提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

// xxx.vue文件
<template>
  <div class="hello">
     <div @click="incrementsFour">4)mutation----对象风格传参按钮</div>
     <div>mutation对象风格传参状态:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  methods() {
    incrementsFour () {
      this.$store.commit({
        type: 'incrementsFour', // 事件名
        num1: 30, // 参数1
        num2: 20 // 参数2
      })
    }
  }
}
</script>

Mutation遵守Vue的响应原则
  • 最好提前在你的 store 中初始化好所有所需属性。
  • 当需要在对象上添加新属性时,你应该
    • 使用 Vue.set(obj, 'newProp', 123), 或者
    • 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }

例如:

//mutation.js文件
export default {
  // { ...state.obj, ...payload } 是指以新对象替换老对象
  changeNum1 (state, payload) {
    state.obj = { ...state.obj, ...payload }
  }
}

// HelloWorld.vue文件
<template>
  <div class="hello">
     <button @click="changeName">对象新增属性按钮</button>
     <div>对象新增属性测试:{{name}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  data () {
    return {
      name: { // 定义name对象
        a: 'aaaa'
      }
    }
  },
  methods() {
    // 5.以新对象替换老对象
    changeName () {
      // this.name.b = 'bbbb' // 这样新增属性是错误的
      this.$set(this.name, 'b', 'bbbb') // 当需要在对象上添加新属性时,你应该 Vue.set(obj, 'xxx', xx)
      // this.name = { ...this.name, b: 'bbbb' } // 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写
    }
  }
}
</script>

使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.js
export const SET_NUMS = 'SET_NUMS' // 数量
export const SET_PRICE = 'SET_PRICE' // 加个
export const SET_FIRSTNAME = 'SET_FIRSTNAME' // firstname
export const SET_LASTNAME = 'SET_LASTNAME' // lastname

//mutations.js
import * as types from './mutation-types'
export default {
 // 使用常量替代 Mutation 事件类型
  [types.SET_NUMS] (state, nums) {
    // 必写state
    state.nums = nums
  },
  // 使用常量替代 Mutation 事件类型
  [types.SET_PRICE] (state, price) {
    state.price = price
  }
}

// HelloWorld.vue
<template>
  <div class="hello">
     <button @click="add">add事件</button>
     <div href="#">add事件操作后的nums值:{{a}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  data () {
    return {
      name: { // 定义name对象
        a: 'aaaa'
      }
    }
  },
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
  },
  methods() {
    add () {
      let nums = this.a
      nums++
      this.setNums(nums)
    },
    ...mapMutations({
      setNums: type.SET_NUMS,
      setprice: type.SET_PRICE
    })
  }
}
</script>

Mutation 必须是同步函数
mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

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

在组件中提交mutation

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

// mutation-types.js
export const SET_NUMS = 'SET_NUMS' // 数量
export const SET_PRICE = 'SET_PRICE' // 加个
export const SET_FIRSTNAME = 'SET_FIRSTNAME' // firstname
export const SET_LASTNAME = 'SET_LASTNAME' // lastname

//mutations.js
import * as types from './mutation-types'
export default {
 // 使用常量替代 Mutation 事件类型
  [types.SET_NUMS] (state, nums) {
    // 必写state
    state.nums = nums
  },
  // 使用常量替代 Mutation 事件类型
  [types.SET_PRICE] (state, price) {
    state.price = price
  }
}

// HelloWorld.vue
<template>
  <div class="hello">
     <button @click="add">add事件</button>
     <div href="#">add事件操作后的nums值:{{a}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  data () {
    return {
      name: { // 定义name对象
        a: 'aaaa'
      }
    }
  },
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
  },
  methods() {
    add () {
      let nums = this.a
      nums++
      this.setNums(nums)
    },
     // mapMutations辅助函数
    ...mapMutations({
      setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
      setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
    })
  }
}
</script>

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:接下来聊聊actions

Actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
action注册
// state.js
export default {
  nums: 0, // 数量
  price: 100, // 金额
  count: 0, // 总计
  obj: {} // 定义一个空对象
}

// mutations
export default {
 incrementsFour (state, payload) {
    state.count = state.count + payload.num1 + payload.num2
  }
}

// actions.js
import * as types from './mutation-types'
export default {
// 注册一个简单的action
  increment ({ commit }, payload) {
    commit({
      type: 'incrementsFour',
      num1: payload.num1,
      num2: payload.num2
    })
  },
}

分发Action

Action 通过 this.$store.dispatch方法触发:

// state.js
export default {
  nums: 0, // 数量
  price: 100, // 金额
  count: 0, // 总计
  obj: {} // 定义一个空对象
}

// mutations
export default {
 incrementsFour (state, payload) {
    state.count = state.count + payload.num1 + payload.num2
  }
}

// actions.js
import * as types from './mutation-types'
export default {
// 注册一个简单的action
  increment ({ commit }, payload) {
    commit({
      type: 'incrementsFour',
      num1: payload.num1,
      num2: payload.num2
    })
  },
}

// HelloWorld.vue
<template>
  <div class="hello">
     <button @click="useAction">action调用</button>
    <div>被action调用后的值:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
  data () {
    return {
      name: { // 定义name对象
        a: 'aaaa'
      }
    }
  },
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
  },
  methods() {
    useAction () {
     // action的dispatch分发
      this.$store.dispatch('increment', { num1: 40, num2: 30 })
    }
}
</script>

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
  increment ({ commit }, payload) {
    setTimeout(() => {
      commit({
        type: 'incrementsFive',
        num1: payload.num1,
        num2: payload.num2
      })
    }, 1000)
  }
}

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

 useAction () {
   // 1.action的dispatch分发  action支持以载荷形式分发
   // this.$store.dispatch('increment', { num1: 40, num2: 30 })
   // 2.action的dispatch分发  action支持以对象形式分发
   this.$store.dispatch({ type: 'increment', num1: 40, num2: 30 })
 }

在组件中分发Action

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

// HelloWorld.vue
<template>
  <div class="hello">
     <button @click="useAction">action调用</button>
    <div>被action调用后的值:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
  },
  methods() {
    useAction () {
    	this.changeIncrement()
    },
     // mapAction辅助函数
    ...mapActions({
      changeIncrement: 'increment' // 将 `this.changeIncrement()` 映射为 `this.$store.dispatch('increment')`
    }),
}
</script>


组合Action

我们如何才能组合多个 action,以处理更加复杂的异步流程?

// action.js
import * as types from './mutation-types';

export default {
  changeNumAndPrice ({ commit }) {
    // 自定义触发mutations里函数的方法,context与store 实例具有相同方法和属性
    commit(types.SET_NUMS, 100)
    commit(types.SET_PRICE, 100)
  },
  // 注册一个简单的action
  increment ({ commit }, payload) {
    commit({
      type: 'incrementsFive',
      num1: payload.num1,
      num2: payload.num2
    })
  },
  actionB (context) {
    console.log('失败')
  },
  // 分组action
  actionA ({ dispatch }, payload) {
    return dispatch('increment', {
      num1: payload.num1,
      num2: payload.num2
    }).then(() => {
      return dispatch('actionB')
    })
  }
}
// HelloWorld.vue
<template>
  <div class="hello">
     <button @click="useAction">action调用</button>
    <div>被action调用后的值:{{count}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
  },
  methods() {
    useAction () {
    	this.changeIncrement({ num1: 20, num2: 30 })
    },
     // mapAction辅助函数
    ...mapActions({
      changeIncrement: 'actionA'
    }),
}
</script>

有一点要注意的是,将 store 中的 state 绑定到 Vue 组件中的 computed 计算属性后,对 state 进行更改需要通过 mutation 或者 action,在 Vue 组件中直接进行赋值 (this.myState = ‘ABC’) 是不会生效的。

Getters

有时候我们需要根据store 中的 state 来派生出一些其他状态,例如对列表进行过滤并计数,再比如:依据商品单价和商品数量计算商品总价。这时候可以使用getters计算属性:

Getters获取
// getters.js
export default {
  total: state => { // 商品总价
    return state.nums * state.price
  }
}

// HelloWorld.vue
<template>
  <div class="hello">
    <button @click="add">add事件</button>
    <div>getters获取的值:{{total}}</div>
    <button @click="changePrice">改变价格</button>
    <div>{{total}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
     // 1.获取getters
    total () {
      return this.$store.getters.total
    }
  },
  methods() {
     add () {
      let nums = 1
      nums++
      this.setNums(nums)
    },
    // changePrice
    changePrice () {
      let nums = 3
      nums++
      this.setprice(nums)
    },
    // 6. mapMutations辅助函数
    ...mapMutations({
      setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
      setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
    }),
}
</script>

辅助函数

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

// getters.js
export default {
  total: state => { // 商品总价
    return state.nums * state.price
  }
}

// HelloWorld.vue
<template>
  <div class="hello">
    <button @click="add">add事件</button>
    <div>getters获取的值:{{total}}</div>
    <button @click="changePrice">改变价格</button>
    <div>{{total}}</div>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions,mapGetters} from 'vuex';
export default {
  computed: {
  ...mapState({
      a: state => state.nums, //获取nums状态
      b: 'price',
      count: 'count'
    }),
     // 2.mapGetters辅助函数
    ...mapGetters(['total'])
  },
  methods() {
     add () {
      let nums = this.a
      nums++
      this.setNums(nums)
    },
    // changePrice
    changePrice () {
      let price = this.price
      price++
      this.setprice(price)
    },
    // 6. mapMutations辅助函数
    ...mapMutations({
      setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
      setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
    }),
}
</script>

Module

因为在大多数的项目中,我们对于全局状态的管理并不仅仅只有一种情况的需求,有时有多方面的需求,比如写一个商城项目,你所用到的全局state可能是关于购物车这一块儿的也有可能是关于用户信息这一块儿的;像这样的情况我们就要考虑使用vuex中的 modules 模块化了,具体怎么使用modules呢?请往下看

首先,在store文件夹下面新建一个modules文件夹,然后在modules文件里面建立需要管理状态的js文件,既然要把不同部分的状态分开管理,那就要把它们给分成独立的状态文件了,如下图:
image.png
而对应的store文件夹下面的index.js 里面的内容就直接改写成:

// store文件下index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'

import userinfo from './module/userinfo'

Vue.use(Vuex)

const store = new Vuex.Store({
  state,
  getters,
  mutations,
  actions,
  modules: {
    userinfo
  }
})
export default store

命名空间:namespaced: true, 为什么要加这个呢?默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的。比如说:你在全局的action.js下定义了一个方法,然后在某个模块下定义了相同的方法就会产生覆盖,加命名空间的目的是为了在某个模块下定义的方法加上模块名,与全局的方法区分开。

// module文件下的userinfo.js
import * as types from '../mutation-types'

export default {
  namespaced: true, // 命名空间
  state: {
    firstName: '--',
    lastName: '--'
  },

  getters: {
    fullName (state) {
      return state.firstName + state.lastName
    }
  },

  mutations: {
    [types.SET_FIRSTNAME] (state, payload) {
      state.firstName = payload
    },
    [types.SET_LASTNAME] (state, payload) {
      state.lastName = payload
    }
  },

  actions: {
    changeName ({ commit }) {
       // 注意: 想要访问跟级别的状态可以使用rootState
      setTimeout(() => {
        commit(types.SET_FIRSTNAME, 'ling')
        commit(types.SET_LASTNAME, 'xi')
      }, 1000)
    }
  }
}

// HelloWorld.vue调用
<template>
  <div class="hello">
    <h1>获取userinfo模块的状态:firstName: {{firstName}} lastName: {{lastName}}</h1>
  </div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions,mapGetters} from 'vuex';
export default {
  computed: {
     ...mapState('userinfo', ['firstName', 'lastName']),
  },
  methods() {
    ...mapActions('userinfo', ['changeName'])
  },
  created () { // 调用userinfo的changeName方法
    this.changeName()
  }
}
</script>

最后

好了,本文到此结束,希望对你有帮助 😃
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值