vuex 源码整体架构学习,2024年最新字节跳动前端面试全套真题解析在互联网火了

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

}

最终每个Vue的实例对象,都有一个$store属性。且是同一个Store实例。

用购物车的例子来举例就是:

const vm = new Vue({

el: ‘#app’,

store,

render: h => h(App)

})

console.log('vm. s t o r e = = = v m . store === vm. store===vm.children[0]. s t o r e ′ , v m . store', vm. store,vm.store === vm. c h i l d r e n [ 0 ] . children[0]. children[0].store)

// true

console.log(‘vm. s t o r e = = = v m . store === vm. store===vm.children[0]. c h i l d r e n [ 0 ] . children[0]. children[0].store’, vm. s t o r e = = = v m . store === vm. store===vm.children[0]. c h i l d r e n [ 0 ] . children[0]. children[0].store)

// true

console.log(‘vm. s t o r e = = = v m . store === vm. store===vm.children[0]. c h i l d r e n [ 1 ] . children[1]. children[1].store’, vm. s t o r e = = = v m . store === vm. store===vm.children[0]. c h i l d r e n [ 1 ] . children[1]. children[1].store)

// true

Vuex.Store 构造函数


先看最终 new Vuex.Store 之后的 Store 实例对象关系图:先大致有个印象。

export class Store {

constructor (options = {}) {

// 这个构造函数比较长,这里省略,后文分开细述

}

}

if (!Vue && typeof window !== ‘undefined’ && window.Vue) {

install(window.Vue)

}

如果是 cdn script 方式引入vuex插件,则自动安装vuex插件,不需要用Vue.use(Vuex)来安装。

// asset 函数实现

export function assert (condition, msg) {

if (!condition) throw new Error([vuex] ${msg})

}

if (process.env.NODE_ENV !== ‘production’) {

// 可能有读者会问:为啥不用 console.assert,console.assert 函数报错不会阻止后续代码执行

assert(Vue, must call Vue.use(Vuex) before creating a store instance.)

assert(typeof Promise !== ‘undefined’, vuex requires a Promise polyfill in this browser.)

assert(this instanceof Store, store must be called with the new operator.)

}

条件断言:不满足直接抛出错误

1.必须使用 Vue.use(Vuex) 创建 store 实例。

2.当前环境不支持Promise,报错:vuex 需要 Promise polyfill

3.Store 函数必须使用 new 操作符调用。

const {

// 插件默认是空数组

plugins = [],

// 严格模式默认是false

strict = false

} = options

从用户定义的new Vuex.Store(options) 取出pluginsstrict参数。

// store internal state

// store 实例对象 内部的 state

this._committing = false

// 用来存放处理后的用户自定义的actoins

this._actions = Object.create(null)

// 用来存放 actions 订阅

this._actionSubscribers = []

// 用来存放处理后的用户自定义的mutations

this._mutations = Object.create(null)

// 用来存放处理后的用户自定义的 getters

this._wrappedGetters = Object.create(null)

// 模块收集器,构造模块树形结构

this._modules = new ModuleCollection(options)

// 用于存储模块命名空间的关系

this._modulesNamespaceMap = Object.create(null)

// 订阅

this._subscribers = []

// 用于使用 $watch 观测 getters

this._watcherVM = new Vue()

// 用来存放生成的本地 getters 的缓存

this._makeLocalGettersCache = Object.create(null)

声明Store实例对象一些内部变量。用于存放处理后用户自定义的actionsmutationsgetters等变量。

提一下 Object.create(null) 和 {} 的区别。前者没有原型链,后者有。即 Object.create(null).__proto__是 undefined ({}).__proto__ 是 Object.prototype

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

}

给自己 绑定 commit 和 dispatch

为何要这样绑定 ?

说明调用 commit 和 dispach 的 this 不一定是 store 实例

这是确保这两个函数里的 this 是 store 实例

// 严格模式,默认是false

this.strict = strict

// 根模块的state

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)

// initialize the store vm, which is responsible for the reactivity

// (also registers _wrappedGetters as computed properties)

resetStoreVM(this, state)

上述这段代码 installModule(this, state, [], this._modules.root)

初始化 根模块。

并且也递归的注册所有子模块。

并且收集所有模块的 getters 放在 this._wrappedGetters 里面。

resetStoreVM(this, state)

初始化 store._vm 响应式的

并且注册 _wrappedGetters 作为 computed 的属性

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

插件:把实例对象 store 传给插件函数,执行所有插件。

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools

if (useDevtools) {

devtoolPlugin(this)

}

初始化 vue-devtool 开发工具。

参数 devtools 传递了取 devtools 否则取Vue.config.devtools 配置。

初读这个构造函数的全部源代码。会发现有三个地方需要重点看。分别是:

this._modules = new ModuleCollection(options)

installModule(this, state, [], this._modules.root)

resetStoreVM(this, state)

阅读时可以断点调试,赋值语句this._modules = new ModuleCollection(options),如果暂时不想看,可以直接看返回结果。installModuleresetStoreVM函数则可以断点调试。

class ModuleCollection

收集模块,构造模块树结构。

注册根模块 参数 rawRootModule 也就是 Vuex.Store 的 options 参数

未加工过的模块(用户自定义的),根模块

export default class ModuleCollection {

constructor (rawRootModule) {

// register root module (Vuex.Store options)

this.register([], rawRootModule, false)

}

}

/**

  • 注册模块

  • @param {Array} path 路径

  • @param {Object} rawModule 原始未加工的模块

  • @param {Boolean} runtime runtime 默认是 true

*/

register (path, rawModule, runtime = true) {

// 非生产环境 断言判断用户自定义的模块是否符合要求

if (process.env.NODE_ENV !== ‘production’) {

assertRawModule(path, rawModule)

}

const newModule = new Module(rawModule, runtime)

if (path.length === 0) {

this.root = newModule

} else {

const parent = this.get(path.slice(0, -1))

parent.addChild(path[path.length - 1], newModule)

}

// register nested modules

// 递归注册子模块

if (rawModule.modules) {

forEachValue(rawModule.modules, (rawChildModule, key) => {

this.register(path.concat(key), rawChildModule, runtime)

})

}

}

class Module

// Base data struct for store’s module, package with some attribute and method

// store 的模块 基础数据结构,包括一些属性和方法

export default class Module {

constructor (rawModule, runtime) {

// 接收参数 runtime

this.runtime = runtime

// Store some children item

// 存储子模块

this._children = Object.create(null)

// Store the origin module object which passed by programmer

// 存储原始未加工的模块

this._rawModule = rawModule

// 模块 state

const rawState = rawModule.state

// Store the origin module’s state

// 原始Store 可能是函数,也可能是是对象,是假值,则赋值空对象。

this.state = (typeof rawState === ‘function’ ? rawState() : rawState) || {}

}

}

经过一系列的注册后,最后 this._modules = new ModuleCollection(options) this._modules 的值是这样的。笔者画了一张图表示:

installModule 函数

function installModule (store, rootState, path, module, hot) {

// 是根模块

const isRoot = !path.length

// 命名空间 字符串

const namespace = store._modules.getNamespace(path)

if (module.namespaced) {

// 省略代码:模块命名空间map对象中已经有了,开发环境报错提示重复

// module 赋值给 _modulesNamespaceMap[namespace]

store._modulesNamespaceMap[namespace] = module

}

// … 后续代码 移出来 待读解释

}

注册 state

// set state

// 不是根模块且不是热重载

if (!isRoot && !hot) {

// 获取父级的state

const parentState = getNestedState(rootState, path.slice(0, -1))

// 模块名称

// 比如 cart

const moduleName = path[path.length - 1]

// state 注册

store._withCommit(() => {

// 省略代码:非生产环境 报错 模块 state 重复设置

Vue.set(parentState, moduleName, module.state)

})

}

最后得到的是类似这样的结构且是响应式的数据 实例 Store.state 比如:

{

// 省略若干属性和方法

// 这里的 state 是只读属性 可搜索 get state 查看,上文写过

state: {

cart: {

checkoutStatus: null,

items: []

}

}

}

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

module.context 这个赋值主要是给 helpers 中 mapStatemapGettersmapMutationsmapActions四个辅助函数使用的。

生成本地的dispatch、commit、getters和state。

主要作用就是抹平差异化,不需要用户再传模块参数。

遍历注册 mutation

module.forEachMutation((mutation, key) => {

const namespacedType = namespace + key

registerMutation(store, namespacedType, mutation, local)

})

/**

  • 注册 mutation

  • @param {Object} store 对象

  • @param {String} type 类型

  • @param {Function} handler 用户自定义的函数

  • @param {Object} local local 对象

*/

function registerMutation (store, type, handler, local) {

// 收集的所有的mutations找对应的mutation函数,没有就赋值空数组

const entry = store._mutations[type] || (store._mutations[type] = [])

// 最后 mutation

entry.push(function wrappedMutationHandler (payload) {

/**

  • mutations: {

  • pushProductToCart (state, { id }) {

  •    console.log(state);
    
  • }

  • }

  • 也就是为什么用户定义的 mutation 第一个参数是state的原因,第二个参数是payload参数

*/

handler.call(store, local.state, payload)

})

}

遍历注册 action

module.forEachAction((action, key) => {

const type = action.root ? key : namespace + key

const handler = action.handler || action

registerAction(store, type, handler, local)

})

/**

  • 注册 mutation

  • @param {Object} store 对象

  • @param {String} type 类型

  • @param {Function} handler 用户自定义的函数

  • @param {Object} local local 对象

*/

function registerAction (store, type, handler, local) {

const entry = store._actions[type] || (store._actions[type] = [])

// payload 是actions函数的第二个参数

entry.push(function wrappedActionHandler (payload) {

/**

  • 也就是为什么用户定义的actions中的函数第一个参数有

  • { dispatch, commit, getters, state, rootGetters, rootState } 的原因

  • actions: {

  • checkout ({ commit, state }, products) {

  •    console.log(commit, state);
    
  • }

  • }

*/

let res = handler.call(store, {

dispatch: local.dispatch,

commit: local.commit,

getters: local.getters,

state: local.state,

rootGetters: store.getters,

rootState: store.state

}, payload)

/**

  • export function isPromise (val) {

return val && typeof val.then === ‘function’

}

  • 判断如果不是Promise Promise 化,也就是为啥 actions 中处理异步函数

也就是为什么构造函数中断言不支持promise报错的原因

vuex需要Promise polyfill

assert(typeof Promise !== ‘undefined’, vuex requires a Promise polyfill in this browser.)

*/

if (!isPromise(res)) {

res = Promise.resolve(res)

}

// devtool 工具触发 vuex:error

if (store._devtoolHook) {

// catch 捕获错误

return res.catch(err => {

store._devtoolHook.emit(‘vuex:error’, err)

// 抛出错误

throw err

})

} else {

// 然后函数执行结果

return res

}

})

}

遍历注册 getter

module.forEachGetter((getter, key) => {

const namespacedType = namespace + key

registerGetter(store, namespacedType, getter, local)

})

/**

  • 注册 getter

  • @param {Object} store Store实例

  • @param {String} type 类型

  • @param {Object} rawGetter 原始未加工的 getter 也就是用户定义的 getter 函数

  • @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {}

  • @param {Object} local 本地 local 对象

*/

function registerGetter (store, type, rawGetter, local) {

// 类型如果已经存在,报错:已经存在

if (store._wrappedGetters[type]) {

if (process.env.NODE_ENV !== ‘production’) {

console.error([vuex] duplicate getter key: ${type})

}

return

}

// 否则:赋值

store._wrappedGetters[type] = function wrappedGetter (store) {

/**

  • 这也就是为啥 getters 中能获取到 (state, getters, rootState, rootGetters) 这些值的原因

  • getters = {

  •  cartProducts: (state, getters, rootState, rootGetters) => {
    
  •    console.log(state, getters, rootState, rootGetters);
    
  •  }
    
  • }

*/

return rawGetter(

local.state, // local state

local.getters, // local getters

store.state, // root state

store.getters // root getters

)

}

}

遍历注册 子模块

module.forEachChild((child, key) => {

installModule(store, rootState, path.concat(key), child, hot)

})

resetStoreVM 函数

resetStoreVM(this, state, hot)

初始化 store._vm 响应式的

并且注册 _wrappedGetters 作为 computed 的属性

function resetStoreVM (store, state, hot) {

// 存储一份老的Vue实例对象 _vm

const oldVm = store._vm

// bind store public getters

// 绑定 store.getter

store.getters = {}

// reset local getters cache

// 重置 本地getters的缓存

store._makeLocalGettersCache = Object.create(null)

// 注册时收集的处理后的用户自定义的 wrappedGetters

const wrappedGetters = store._wrappedGetters

// 声明 计算属性 computed 对象

const computed = {}

// 遍历 wrappedGetters 赋值到 computed 上

forEachValue(wrappedGetters, (fn, key) => {

// use computed to leverage its lazy-caching mechanism

// direct inline function use will lead to closure preserving oldVm.

// using partial to return function with only arguments preserved in closure environment.

/**

  • partial 函数

  • 执行函数 返回一个新函数

export function partial (fn, arg) {

return function () {

return fn(arg)

}

}

*/

computed[key] = partial(fn, store)

// getter 赋值 keys

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

// 使用一个 Vue 实例对象存储 state 树

// 阻止警告 用户添加的一些全局mixins

// 声明变量 silent 存储用户设置的静默模式配置

const silent = Vue.config.silent

// 静默模式开启

Vue.config.silent = true

store._vm = new Vue({

data: {

$$state: state

},

computed

})

// 把存储的静默模式配置赋值回来

Vue.config.silent = silent

// enable strict mode for new vm

// 开启严格模式 执行这句

// 用 $watch 观测 state,只能使用 mutation 修改 也就是 _withCommit 函数

if (store.strict) {

enableStrictMode(store)

}

// 如果存在老的 _vm 实例

if (oldVm) {

// 热加载为 true

if (hot) {

// dispatch changes in all subscribed watchers

// to force getter re-evaluation for hot reloading.

// 设置 oldVm._data.$$state = null

store._withCommit(() => {

oldVm._data.$$state = null

})

}

// 实例销毁

Vue.nextTick(() => oldVm.$destroy())

}

}

到此,构造函数源代码看完了,接下来看 Vuex.Store 的 一些 API 实现。

Vuex.Store 实例方法


Vuex API 文档

commit

提交 mutation

commit (_type, _payload, _options) {

// check object-style commit

// 统一成对象风格

const {

type,

payload,

options

} = unifyObjectStyle(_type, _payload, _options)

const mutation = { type, payload }

// 取出处理后的用户定义 mutation

const entry = this._mutations[type]

// 省略 非生产环境的警告代码 …

this._withCommit(() => {

// 遍历执行

entry.forEach(function commitIterator (handler) {

handler(payload)

})

})

// 订阅 mutation 执行

this._subscribers.forEach(sub => sub(mutation, this.state))

// 省略 非生产环境的警告代码 …

}

commit 支持多种方式。比如:

store.commit(‘increment’, {

count: 10

})

// 对象提交方式

store.commit({

type: ‘increment’,

count: 10

})

unifyObjectStyle函数将参数统一,返回 { type, payload, options }

dispatch

分发 action

dispatch (_type, _payload) {

// check object-style dispatch

// 获取到type和payload参数

const {

type,

payload

} = unifyObjectStyle(_type, _payload)

// 声明 action 变量 等于 type和payload参数

const action = { type, payload }

// 入口,也就是 _actions 集合

const entry = this._actions[type]

// 省略 非生产环境的警告代码 …

try {

this._actionSubscribers

.filter(sub => sub.before)

.forEach(sub => sub.before(action, this.state))

} catch (e) {

if (process.env.NODE_ENV !== ‘production’) {

console.warn([vuex] error in before action subscribers: )

console.error(e)

}

}

const result = entry.length > 1

? Promise.all(entry.map(handler => handler(payload)))
entry 0

return result.then(res => {

try {

this._actionSubscribers

.filter(sub => sub.after)

.forEach(sub => sub.after(action, this.state))

} catch (e) {

if (process.env.NODE_ENV !== ‘production’) {

console.warn([vuex] error in after action subscribers: )

console.error(e)

}

}

return res

})

}

replaceState

替换 store 的根状态,仅用状态合并或时光旅行调试。

replaceState (state) {

this._withCommit(() => {

this._vm._data.$$state = state

})

}

watch

响应式地侦听 fn 的返回值,当值改变时调用回调函数。

/**

  • 观测某个值

  • @param {Function} getter 函数

  • @param {Function} cb 回调

  • @param {Object} options 参数对象

*/

watch (getter, cb, options) {

if (process.env.NODE_ENV !== ‘production’) {

assert(typeof getter === ‘function’, store.watch only accepts a function.)

}

return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)

}

subscribe

订阅 store 的 mutation

subscribe (fn) {

return genericSubscribe(fn, this._subscribers)

}

// 收集订阅者

function genericSubscribe (fn, subs) {

if (subs.indexOf(fn) < 0) {

subs.push(fn)

}

return () => {

const i = subs.indexOf(fn)

if (i > -1) {

subs.splice(i, 1)

}

}

}

subscribeAction

订阅 store 的 action

subscribeAction (fn) {

const subs = typeof fn === ‘function’ ? { before: fn } : fn

return genericSubscribe(subs, this._actionSubscribers)

}

registerModule

注册一个动态模块。

/**

  • 动态注册模块

  • @param {Array|String} path 路径

  • @param {Object} rawModule 原始未加工的模块

  • @param {Object} options 参数选项

*/

registerModule (path, rawModule, options = {}) {

// 如果 path 是字符串,转成数组

if (typeof path === ‘string’) path = [path]

// 省略 非生产环境 报错代码

// 手动调用 模块注册的方法

this._modules.register(path, rawModule)

// 安装模块

installModule(this, this.state, path, this._modules.get(path), options.preserveState)

// reset store to update getters…

// 设置 resetStoreVM

resetStoreVM(this, this.state)

}

unregisterModule

卸载一个动态模块。

/**

  • 注销模块

  • @param {Array|String} path 路径

*/

unregisterModule (path) {

// 如果 path 是字符串,转成数组

if (typeof path === ‘string’) path = [path]

// 省略 非生产环境 报错代码 …

// 手动调用模块注销

this._modules.unregister(path)

this._withCommit(() => {

// 注销这个模块

const parentState = getNestedState(this.state, path.slice(0, -1))

Vue.delete(parentState, path[path.length - 1])

})

// 重置 Store

resetStore(this)

}

hotUpdate

热替换新的 action 和 mutation

// 热加载

hotUpdate (newOptions) {

// 调用的是 ModuleCollection 的 update 方法,最终调用对应的是每个 Module 的 update

this._modules.update(newOptions)

// 重置 Store

resetStore(this, true)

}

组件绑定的辅助函数


文件路径:vuex/src/helpers.js

mapState

为组件创建计算属性以返回 Vuex store 中的状态。

export const mapState = normalizeNamespace((namespace, states) => {

const res = {}

// 非生产环境 判断参数 states 必须是数组或者是对象

if (process.env.NODE_ENV !== ‘production’ && !isValidMap(states)) {

console.error(‘[vuex] mapState: mapper parameter must be either an Array or an Object’)

}

normalizeMap(states).forEach(({ key, val }) => {

res[key] = function mappedState () {

let state = this.$store.state

let getters = this.$store.getters

文末

逆水行舟不进则退,所以大家要有危机意识。

同样是干到35岁,普通人写业务代码划水,榜样们深度学习拓宽视野晋升管理。

这也是为什么大家都说35岁是程序员的门槛,很多人迈不过去,其实各行各业都是这样都会有个坎,公司永远都缺的高级人才,只用这样才能在大风大浪过后,依然闪耀不被公司淘汰不被社会淘汰。

为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了《前端工程师核心知识笔记》电子稿文件。

内容包括html,css,JavaScript,ES6,计算机网络,浏览器,工程化,模块化,Node.js,框架,数据结构,性能优化,项目等等。

269页《前端大厂面试宝典》

包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。

前端面试题汇总

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

热替换新的 action 和 mutation

// 热加载

hotUpdate (newOptions) {

// 调用的是 ModuleCollection 的 update 方法,最终调用对应的是每个 Module 的 update

this._modules.update(newOptions)

// 重置 Store

resetStore(this, true)

}

组件绑定的辅助函数


文件路径:vuex/src/helpers.js

mapState

为组件创建计算属性以返回 Vuex store 中的状态。

export const mapState = normalizeNamespace((namespace, states) => {

const res = {}

// 非生产环境 判断参数 states 必须是数组或者是对象

if (process.env.NODE_ENV !== ‘production’ && !isValidMap(states)) {

console.error(‘[vuex] mapState: mapper parameter must be either an Array or an Object’)

}

normalizeMap(states).forEach(({ key, val }) => {

res[key] = function mappedState () {

let state = this.$store.state

let getters = this.$store.getters

文末

逆水行舟不进则退,所以大家要有危机意识。

同样是干到35岁,普通人写业务代码划水,榜样们深度学习拓宽视野晋升管理。

这也是为什么大家都说35岁是程序员的门槛,很多人迈不过去,其实各行各业都是这样都会有个坎,公司永远都缺的高级人才,只用这样才能在大风大浪过后,依然闪耀不被公司淘汰不被社会淘汰。

为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了《前端工程师核心知识笔记》电子稿文件。

内容包括html,css,JavaScript,ES6,计算机网络,浏览器,工程化,模块化,Node.js,框架,数据结构,性能优化,项目等等。

269页《前端大厂面试宝典》

包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。

前端面试题汇总

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-t3DiqbH2-1713402126203)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值