【前端进阶良品】Vue 组件间通信方式完整版

注:文中有许多链接,可以点击原文,获得更好的阅读体验

前言

Vue.js 在现今使用有多广泛不用多说,而 Vue 的一大特点就是组件化。本期要讲的,便是 Vue 组件间通信方式的总结,这也几乎是近年 Vue 面试中的必考题。注:文中示例都基于 Vue 脚手架讲解,会用到一些 Element UI 示例。

  • 文中示例依然在 ??? webrtc-stream

  • 文章仓库 ?? fe-code

【前端进阶之路】会作为一个新系列连载,后续会更多优质前端内容,感兴趣的同学不妨关注一下。 文章最后有 交流群 和 公众号,可以一起学习交流,感谢?。

  • 下期预告:深入 Vue 响应式原理,手写一个 mvvm

组件

组件是可以复用的 Vue 实例。 — Vue 官方文档;

在进入主题之前,还是决定先简单聊聊组件。在 Vue 中,根据注册方式的不同,可以分为:

  • 局部组件 (局部注册)

  • 全局组件 (全局注册)

顾名思义,全局注册的组件,可以用在 Vue 实例的任意模板中。但是带来的隐患是,在 webpack 模块化构建时,即便你没有在项目中使用这个组件,依然会打包到最终的项目代码中。而局部组件,则需要在使用到的实例中注册该组件。

// 全局注册	
// install.js	
import Icon from './Icon.vue';	
const install = {	
    install:function(Vue){	
        Vue.component('VIcon', Icon);	
    }	
};	
export default install;	
// main.js	
import install from './install.js'; // 引入全局插件	
Vue.use(install); // 注册	
// 局部注册	
import VIcon from './Icon.vue';	
export default{	
    components: {	
        VIcon	
    }	
}	
// 使用	
<v-icon> </v-icon>

根据应用场景的不同,又可以分为:

  • 页面组件:我们使用 Vue 时,每个路由代表的页面,都可以称之为组件。

  • 基础组件:就像上面栗子中的 Icon 组件,就是一个典型的基础组件。基本上不掺杂业务逻辑,在项目中可能被大量使用,易于移植。类似的基础组件还有 Button、Input 等,常见于各类 UI 组件库。

  • 业务组件:业务组件和项目具体的业务逻辑有大量耦合,一般抽离于当前项目。

以上就是组件的简单介绍,那我们到底为什么要推崇组件化?组件化有什么好处?复用?我个人认为组件化最大的好处,便是解耦,易于项目管理。所以在大型项目管理中,组件化是非常有必要的。当然,这并不是今天学习的重点,以后有机会再聊。

正因为在 Vue 中处处都是组件,而我们也偏向于组件化、模块化。那我们在一堆组件中,便需要解决一个问题 — 组件间通信。下面,我们就进入今天的主题,Vue 的组件间通信。

组件间通信

组件间通信是我们在 Vue 项目中不可避免的问题,深刻了解了 Vue 组件间通信的几种方式,才能让我们在处理各种交互问题时游刃有余。

Props

Vue 中,最基本的通信方式就是 Props,它是父子组件通信中父组件传值给子组件的一种方式。它允许以数组形式接收,但是更推荐你开启类型检查的形式。更详细的类型检查前往 vue 文档。

// communication.vue	
<communication-sub v-bind="dataProps"></communication-sub>	
// v-bind="dataProps" 等同于 :title="title",适用于多个参数一起传递	
···	
data() {	
    return {	
        dataProps: {	
            title: '我是父组件的值',	
        }	
    }	
}	
// communication-sub.vue	
<div class="communication-sub">	
    {{title}}	
</div>	
···	
props: ['title']	
// 更推荐开启类型检查	
props: {	
    title: {	
        type: String,	
        required: true,	
        default: '' // 允许指定默认值,引用类型需要函数返回	
    }	
}	
···

我们都知道,Props 是单向数据流,这是 Vue 为了避免子组件意外改变父组件的状态,从而导致数据流向难以理解而做出的限制。所以 Vue 推荐需要改动的时候,通过改变父组件的值从而触发 Props 的响应。或者,我们可以在接收非引用类型的值时,使用子组件自身的 data 做一次接收。

props: ['title'],	
data: function () {	
  return {	
    text: this.title	
  };	
}

为什么是非引用类型呢,因为在 JavaScript 中,引用类型的赋值,实际是内存地址的传递。所以上面栗子中的简单赋值,显然会指向同一个内存地址,所以如果是数组或是对象,你可能需要一次深拷贝。

let obj = JSON.parse(JSON.stringify(obj));

上面这个操作有一些缺陷,不能序列化函数、undefined、循环引用等,详见传送门,但是也能应付一些日常情况了。

事实上,在 Props 是引用类型时,单独修改对象、数组的某个属性或下标,Vue 并不会抛出错误。当然,前提是你要非常清楚自己在做什么,并写好注释,防止你的小伙伴们疑惑。

有的同学可能知道,在组件上绑定的属性,如果没有在组件内部用 Props 声明,会默认绑定到组件的根元素上去。还是之前的栗子:

<communication-sub v-bind="dataProps" class="one" type="div"></communication-sub>

结果如下:

640?wx_fmt=png

这是 Vue 默认处理的,而且,除了 class 和 style 采用合并策略,其它特性(如上栗 type)会替换掉原来根元素上的属性值。当然,我们也可以显示的在组件内部关闭掉这个特性:

...	
inheritAttrs: false,	
props: ['title']

利用 inheritAttrs,我们还可以方便的把组件绑定的其它特性,转移到我们指定的元素上。这就需要用到下一个我们要讲的 $attrs 了。

attrs、listeners

我们在使用组件库的时候经常会这么写:

<el-input v-model="input" placeholder="请输入内容"></el-input>

实际渲染后:

640?wx_fmt=png

可以看到我们指定的的 placeholder 是渲染在 input 上的,但是 input 并不是根元素。难道都用 Props 声明后,再赋值给 input?这种情况就可以用到 $attrs 了,改造一下我们之前那个栗子。

// communication.vue	
<template>	
    <div class="communication">	
        <communication-sub v-bind="dataProps" class="input" type="text" placeholder="请输入内容">	
        </communication-sub>	
    </div>	
</template>	
<script>	
import communicationSub from './communication-sub.vue';	
export default{	
    name: 'communication',	
    data() {	
        return {	
            dataProps: {	
                title: '我是 communication 的值',	
            }	
        }	
    },	
    components: {	
        communicationSub	
    }	
}	
</script>	
// communication-sub.vue	
···	
<div class="communication-sub">	
    <input v-bind="$attrs" v-model="title"></input>	
</div>	
···	
 export default {	
    inheritAttrs: false	
}

640?wx_fmt=png

可以看到,type 已经转移到了子元素 input 标签上,但是 class 没有。这是因为 inheritAttrs:false 选项不会影响 style 和 class 的绑定。可以看出 $attrs 则是将没有被组件内部 Props 声明的传值(也叫非 Props 特性)收集起来的一个对象,再通过 v-bind 将其绑定在指定元素上。这也是 Element 等组件库采用的策略。

这里需要注意一点,通过 $attrs 指定给元素的属性,不会与该元素原有属性发生合并或替换,而是以原有属性为准。举个例子,假如我将上述 input 的 type 默认设置为 password。

<input v-bind="$attrs" v-model="title" type="password"></input>

则不会采用 $attrs 中的 type: 'text',将以 password 为准,所以如果需要默认值的属性,建议不要用这种方式。

$listeners 同 $attrs 类似,可以看做是一个包含了组件上所有事件监听器(包括自定义事件、不包括.native修饰的事件)的对象。它也支持上述的写法,适用于将事件安放于组件内指定元素上。

// communication.vue	
<communication-sub v-bind="dataProps"	
class="input"	
type="text"	
placeholder="请输入内容"	
@focus="onFocus" >	
</communication-sub>	
···	
methods: {	
    onFocus() {	
        console.log('onFocus');	
    }	
}	
// communication-sub.vue	
<input v-bind="$attrs" v-model="title" v-on="$listeners"></input>

给之前的栗子绑定一个聚焦事件,在子组件中通过 $listeners 绑定给 input,则会在 input 聚焦时触发。

那么除了用在这种给组件内指定元素绑定特性和事件的情况,还有哪些场景可以用到呢?官方说明:在创建更高层次的组件时非常有用。比如在祖孙组件中传递数据,在孙子组件中触发事件后要在祖辈中做相应更新。我们继续之前的栗子:在孙辈组件触发点击事件,然后在祖辈中修改相应的 data。

640?wx_fmt=gif

// communication.vue	
<communication-sub v-bind="dataProps"	
                @click="onCommunicationClick">	
</communication-sub>	
···	
methods: {	
    onCommunicationClick() {	
        this.dataProps.title = '我是点击之后的值';	
    }	
};	
// communication-sub.vue	
<communication-min-sub v-on="$listeners"></communication-min-sub> // 子组件中将事件透传到孙辈	
// communication-min-sub.vue	
<template>	
    <div class="communication-min-sub">	
        <p>我是 communication-min-sub</p>	
        <button v-on="$listeners">click</button>	
    </div>	
</template>	
<script>	
export default{	
    name: 'communication-min-sub',	
    inheritAttrs: false	
}	
</script>

这样就能很方便的在多级组件的子级组件中,快速访问到父组件的数据和方法。正如在刚才的例子中,button 点击时,是直接调用的 communication.vue 中定义的方法。

依赖注入 provide、inject

上面的方法,在大多数多级组件嵌套的场景很有用,但有时我们遇到的并不一定是有父子关系的组件。比如基础组件中的 Select 下拉选择器。

<el-select v-model="value" placeholder="请选择">	
    <el-option	
      v-for="item in options"	
      :key="item.value"	
      :label="item.label"	
      :value="item.value">	
    </el-option>	
</el-select>

相信大家都使用过上栗或者类似于上栗的基础组件,它们借助 vue 插槽 实现。所以这个时候,el-select 和 el-option 之间的数据通信,我们之前的 $attrs、 $listeners就没有用武之地了。有同学可能不太理解上面的代码为什么要通信,我简单介绍一下 Element 的处理方式:

640?wx_fmt=png

我们可以简单的认为(Element 源码比这个要稍复杂,为了方便理解,简化一下,如有需要,可直接前往源码阅读),在 el-select 中有一个 input 元素,el-option 中是一列渲染好的 li。根据需求,我们在选中某个 li 的时候,要通知 input 展示相应的数据。而且我们在实际使用的时候,一般还伴随 el-form、el-form-item等组件,所以迫切需要一种方式:

可以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。--- Vue 文档

有同学可能会想到,这种多级的可以用 Vuex、EventBus等方式,当然可以。只不过我们现在的前提是基础组件,一般第三方组件库是不会增加一些额外的依赖的。事实上 Vue 本身并不推荐直接在业务中使用 provide、inject,一般在组件、插件库用到的比较多。

但是在项目比较小、业务逻辑比较简单的时候,我们完全不必特意引入 Vuex。只要使用得当,provide、inject 确实不失为一种好办法。说了这么多,我们来看一下具体用法,我们将之前的栗子,改为用 provide、inject 来实现。

// communication.vue	
<communication-sub v-bind="dataProps" >	
</communication-sub>	
// @click="onCommunicationClick" 移除之前绑定的时间	
···	
// 在 provide 添加子代需要接收的方法 onCommunicationClick,	
// 也可以直接指定为 this,子代便能访问父代所有的数据和方法。	
provide: function () {	
    return {	
        onCommunicationClick: this.onCommunicationClick	
    }	
},	
methods: {	
    onCommunicationClick() {	
        this.dataProps.title = '我是点击之后的值';	
    }	
};	
// communication-sub.vue	
<communication-min-sub></communication-min-sub>	
// 移除之前的 v-on="$listeners",因为在这个组件中不需要用到父组件的方法,所以不用做其它处理	
// communication-min-sub.vue	
<template>	
    <div class="communication-min-sub">	
        ···	
        <button @click="onCommunicationClick">click</button>	
        // 移除 v-on="$listeners",然后绑定 inject 接收到的方法	
    </div>	
</template>	
<script>	
export default{	
    name: 'communication-min-sub',	
    inject: ['onCommunicationClick'] // inject 接收父组件的方法	
}	
</script>

这种写法和之前的 $listeners 得到的效果是一样的,就不再放图了。大家可以自己尝试一下,也可以前往源码 webrtc-stream。

思考:有些同学可能会想到,如果我在根实例,app.vue 中如此设置:

<script>	
  export default {	
    provide () {	
      return {	
        app: this // 设置app为this	
      }	
    },	
    data () {	
      return {	
        userInfo: null,	
        otherState: null	
      }	
    }	
  }	
</script>

那这样把所有的状态管理都放在 app.data 中,所有的子代中不就可以共享了吗?是不是就不需要 Vuex 了呢?实际上,Vue 本身就提供了一个方法来访问根实例 $root,所以即使没有 provide 也是可以做到的。那为什么不这么用呢?还是前面提到的原因,不利于追踪维护,也失去了所谓状态管理的意义。不过,如果你的项目足够小的话,依然可以这么使用。

ref、parent、children

我们前面一直说的都是子组件如何触达父组件,那么父组件能不能访问到子组件呢?当然是可以的。

  • ref

简单来说就是获取元素的 Dom 对象和子组件实例。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。获取 Dom 元素就是为了进行一些 Dom 操作,需要注意的一点就是,要在 Dom 加载完成后使用,否则可能获取不到。比如我要将之前 input 的字体颜色改成红色:

<input type="text" v-model="dataProps.title" ref="input">	
...	
mounted() {	
    this.$nextTick(_ => { // 确保 Dom 更新完成	
        this.$refs['input'].style.color = 'red';	
    });	
}	
// 这里只是举一个栗子,实际项目中的需求,最好通过 class 的方式,尽量减少 Dom 操作。

那什么情况下需要获取组件实例呢?比如父元素的某个状态改变,需要子组件进行 http 请求更新数据。通常情况下,我们会选择通过 Props 将状态传递给子组件,然后子组件进行 Watch 监测,如果有变更,则进行相应操作。这个时候,我们便可以选择使用 ref。

<child ref="child"></child>	
···	
<script>	
  export default {	
    methods () {	
      onStateChange() { // 变更状态后直接调用子组件方法进行更新	
          this.$refs['child'].updateData();	
      }	
    }	
  }	
</script>
  • $children、 $parent

无独有偶, $children 同样可以完成上面的任务。 $children 和 $parent,顾名思义,一个会找到当前组件的子组件,一个会找到当前组件的父组件。如果有多个子组件,需要依赖组件实例的 name 属性。改写一下上面的方法:

<script>	
  export default {	
    methods () {	
      onStateChange() { // 子组件返回的是一个数组,多个子组件用 $options.name 区分。	
          this.$children[0].updateData();	
      }	
    }	
  }	
</script>

$parent 和 $children 用法一样,不过 $parent 返回的父组件实例,不是数组,因为父组件肯定只有一个。ref、parent、children 它们几个的一个缺点就是无法处理跨级组件和兄弟组件,后续我们会介绍 dispatch 和 broadcast 方法,实现跨级通信。

emit、on、off

$emit,想必大家都非常熟悉,我们通常用作父子组件间通信,我们也叫它自定义事件。 $emit 和 $on都是组件自身的方法, $on 可以监听 $emit 派发的事件, $off 则用来取消事件监听。这也是我们下一个要讲的通信方式 EventBus 所依赖的原理。

// 父组件	
<template>	
    <button-component @clickButton="clickButton"></button-component>	
    // 在父组件利用 v-on 监听	
</template>	
<script>	
export default {	
    methods: {	
      clickButton () { ··· }	
    }	
}	
</script>	
// 子组件	
<template>	
    <button @click="handleClick"></button>	
</template>	
<script>	
export default {	
    methods: {	
      handleClick () { // 触发 $emit	
        this.$emit('clickButton');	
      }	
    },	
    mounted() {	
        this.$on('clickButton', (...arr) => { // 也可以自己监听 $emit,虽然没什么用···	
            console.log(...arr);	
        })	
    }	
}	
</script>

EventBus

$emit的痛点依然是支持跨级和兄弟组件,Vue 官方推荐我们使用一个新的 Vue 实例来做一个全局的事件通信(或者叫中央事件总线···),也就是我们要讲的 EventBus。了解过的同学都知道,正常的 bus,我们一般会挂载到 Vue 的 prototype 上,方便全局调用。

// main.js	
Vue.prototype.$bus = new Vue();

依旧改写之前的栗子:

<!--communication.vue-->	
<communication-sub v-bind="dataProps" >	
</communication-sub>	
···	
beforeDestroy() { <!-- 实例销毁时,需要卸载监听事件 -->	
    this.$bus.$off('busClick');	
},	
created() {  <!-- 监听子组件触发的 Bus 事件-->	
    this.$bus.$on('busClick', (data) => {	
        this.dataProps.title = data;	
    });	
}	
<!--communication-min-sub.vue-->	
<template>	
    <div class="communication-min-sub">	
        <button @click="busClick">click bus</button>	
        <!--子组件触发点击事件-->	
    </div>	
</template>	
<script>	
export default{	
    methods: {	
        busClick() {	
            this.$bus.$emit('busClick', 'bus 触发了');	
        }	
    }	
}	
</script>

这是一个基础的 EventBus 的实现。现在我们设想一下,类似于 userInfo 这样的信息,在很多页面都需要用到,那我们需要在许多页面都做 $on 监听的操作。那能否将这些操作整合到一起呢?我们一起来看:

 // 新建一个 eventBus.js	
import Vue from 'vue';	
const bus = new Vue({	
    data () {	
        return {	
            userInfo: {}	
        }	
    },	
    created () {	
        this.$on('getUserInfo', val => {	
            this.userInfo = val;	
        })	
    }	
});	
export default bus;	
// main.js	
import bus from './eventBus';	
Vue.prototype.$bus = bus;	
// app.vue	
methods: {	
    getUserInfo() {	
        ajax.post(***).then(data => {	
            this.$bus.$emit('getUserInfo', data); // 通知 EventBus 更新 userInfo	
        })	
    }	
}

这样在其他页面用到 userInfo 的时候,只需要 this.$bus.userInfo 就可以了。注意刚刚其实没有用 off 卸载掉监听,因为其实 userInfo 这种全局信息,并没有一个准确的说要销毁的时机,浏览器关闭的时候,也用不着我们处理了。但是,如果只是某个页面组件用到的,建议还是用最开始的方法,在页面销毁的时候卸载掉。

不过反过来讲,既然用到了 EventBus,说明状态管理并不复杂,否则还是建议用 Vuex 来做。最后再给大家推荐一篇文章 Vue中eventbus很头疼?我来帮你,作者处理 EventBus 的思路很巧妙,大家不妨仔细看看。

派发与广播:dispatch 与 broadcast

此部分参考自 Element 源码

如果有接触过 Vue.js 1.x 的同学,应该对此有所了解。在 1.x 的实现中,是有 $dispatch 和 $broadcast 方法的。 $dispatch 的主要作用是向上级组件派发事件, $broadcast 则是向下级广播。它们的优点是都支持跨级,再看一下官方废弃这两个方法的理由:

因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。并且 $dispatch 和 $broadcast 也没有解决兄弟组件间的通信问题。

可以看到,主要原因是在组件结构扩展后不易理解,以及没有解决兄弟组件通信的问题。但是对于组件库来说,这依旧是十分有用的,所以它们大多自己实现了这两个方法。对我们来讲,也许在项目中用不到,但学习这种解决问题的思路,是十分必要的。

派发和广播,依赖于组件的 name(最怕此处有人说:如果不写 name,这方法不就没用了?2333···),以此来逐级查找对应的组件实例。Element 的实现中,给所有的组件都加了一个 componentName 属性,所以它是根据 componentName 来查找的。我们在实现的时候还是直接用 name。

我们先来看一下 $dispatch 的简单用法,再来分析思路。

<!--communication-min-sub.vue-->	
<template>	
  <button @click="handleDispatch">dispatch</button>	
</template>	
<script>	
import Emitter from '../../utils/emitter';	
export default {	
  mixins: [Emitter], // 混入,方便直接调用	
  methods: {	
    handleDispatch () {	
      this.dispatch('communication', 'onMessage', '触发了dispatch');	
    }	
  }	
}	
</script>
<!--communication.vue-->	
<script>	
  export default {	
    beforeDestroy() { // 销毁	
        this.$off('onMessage');	
    },	
    mounted () {	
        this.$on('onMessage', (data) => { // 监听	
            this.dataProps.title = data;	
        })	
    }	
  }	
</script>

现在明确一下目标,dispatch 方法接收三个参数,组件 name、事件名称、基础数据(可不传)。要做到向上跨级派发事件,需要向上找到指定 name 的组件实例,利用我们前文提到的 $emit方法做派送,所以在指定组件就可以用 $on 来监听了。所以 dispatch 本质上就是向上查找到指定组件并触发其自身的 $emit,以此来做响应,broadcast 则相反。那么如何做到跨级查找呢?

function broadcast(componentName, eventName, params) {	
  this.$children.forEach(child => { // 遍历所有的 $children	
    var name = child.$options.name; // 拿到实例的name,Element 此处用的 componentName	
    if (name === componentName) { // 如果是想要的那个,进行广播	
      child.$emit.apply(child, [eventName].concat(params));	
    } else { // 不是则递归查找 直到 $children 为 []	
      broadcast.apply(child, [componentName, eventName].concat([params]));	
    }	
  });	
}	
export default {	
  methods: {	
    dispatch(componentName, eventName, params) {	
      var parent = this.$parent || this.$root;	
      var name = parent.$options.name;	
      while (parent && (!name || name !== componentName)) {	
      // 存在 parent 且 (不存在 name 或 name 和 指定参数不一样) 则继续查找	
        parent = parent.$parent; // 不存在继续取上级	
        if (parent) {	
          name = parent.$options.name; // 存在上级 再次赋值并再次循环,进行判断	
        }	
      }	
      if (parent) { // 找到以后 如果有 进行事件派发	
        parent.$emit.apply(parent, [eventName].concat(params));	
      }	
    },	
    broadcast(componentName, eventName, params) {	
      broadcast.call(this, componentName, eventName, params);	
    }	
  }	
};

以上是详细的 emitter.js,可以看见,这和我们之前讲到的 $parent、 $children、 $emit、 $on都密切相关。这也是为什么把它放到后面讲的原因。之前说过,派发和广播并没有解决兄弟组件通信的问题,所以这里大家也可以拓展思考一下,如何支持兄弟组件间通信。依然是依赖于 $parent、 $children,可以找到任意指定组件。

Vuex

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

Vuex 相信大家都比较熟悉了,我不打算在这里把 API 再演示一遍。因为我觉得,官方文档 已经非常详细了。Vuex 的核心是单向数据流,并以相应规则保证所有的状态管理都可追踪、可预测。

我们需要知道什么时候该用 Vuex,如果你的项目比较小,状态管理比较简单,完全没有必要使用 Vuex,你可以考虑我们前文提到的几种方式。

总结

本期文章内容到这里就讲完了,我们来总结回顾一下:

  • 子组件触达父组件的方式:Props、 $parent、 $attrs、 $listeners、provide 和 inject、 $dispatch

  • 父组件触达子组件的方式: $emit 和 $on、 $children、 $ref、 broadcast

  • 全局通信:EventBus、Vuex

本来想按照是否支持跨级来分,但是这里的界定比较模糊:如果逐级传递,有些也能做到跨级,但这并不是我们想要的。所以我们只要自己清楚在什么情况下该怎么用就好了。

后记

如果你看到了这里,且本文对你有一点帮助的话,希望你可以动动小手支持一下作者,感谢?。文中如有不对之处,也欢迎大家指出,共勉。

  • 本文示例 源码库 webrtc-stream

  • 文章仓库 ??fe-code

往期文章:

  • 从头到脚撸一个多人视频聊天 — 前端 WebRTC 实战(一)

  • JavaScript 原型和原型链及 canvas 验证码实践

  • 站住,你这个Promise!

  • ???Vchat — 从头到脚,撸一个社交聊天系统(vue + node + mongodb)

欢迎关注公众号 前端发动机,江三疯的前端二三事,专注技术,也会时常迷糊。希望在未来的前端路上,与你一同成长。

640?wx_fmt=jpeg


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值