Vue (第三节 响应式原理)

Vue 响应式原理



前言

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。

一、数据驱动

了解数据驱动之前首先了解三个概念:

  • 数据驱动
  • 响应式的核心原理
  • 发布订阅模式和观察者模式

1.数据驱动

数据驱动是vue.js最大的特点。在vue.js中,所谓的数据驱动就是当数据发生变化的时候,用户界面发生相应的变化,开发者不需要手动的去修改dom。

2.响应式核心原理

vue2响应式原理

“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新,通过ES5提出的Object.defineProperty()实现

来看一段内容:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

通过以上的文字,我们可以看到,在Vue2.x中响应式的实现是通过Object.defineProperty来完成的,注意该属性无法降级(shim)处理,所以Vue不支持IE8以及更低版本的浏览器的原因。

首先先了解一下Object.defineProperty()方法
Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

语法

Object.defineProperty(obj, prop, descriptor)

参数

  • obj : 要定义的属性对象
  • prop:要定义或修改的属性的名称或Symbol
  • descriptor:要定义或修改的属性描述符

接下里通过案例了解一下object.defineProperty

通过object.defineProperty来实现简单的响应式。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <input type="text" v-model="name">
    <span v-model="name"></span>
    <span v-model="age"></span>
  </div>
  <script>5

    let data = {
      name: '李白',
      age: 18
    }
    // Object.defineProperty(obj,prop,descriptor)数据劫持三个参数,
    // 1、obj:必须,要操作或者要监视的目标对象
    // 2、prop:必须,需要定义或者修改的属性名字
    // 3、descriptor:必需。目标属性所拥有的特性
    // getter:get方法当obj属性被获取的时候触发
    // setter:set方法当obj的属性被改变的时候被触发参数是更改后的属性值
    // Object.keys(obj)
    // 参数:要返回其枚举自身属性的对象
    //返回值就是对象里面的属性名称组成的字符串数组
    Object.keys(data).forEach(key => {//遍历这个数组得到的是key(每一个属性名称)
      //调用defineReactiveProperty函数将每次循环的属性名称key、对象data和属性的值传过去
      defineReactiveProperty(data, key, data[key])
    })
    //定义一个defineReactiveProperty函数。
    function defineReactiveProperty(data, key, value) {
      Object.defineProperty(data, key, {
        // 使用的时候触发
        get() {
          return value//返回属性的值
        },
        //属性值改变的时候触发
        set(newValue) {
          //将新值赋值给循环出来的属性值
          value = newValue // 使用声明式渲染,就要便利所有节点,看哪个节点有我们的自定义绑定属性 
          // 调用comile()函数
          compile()
        }
      })
    }
    function compile() {
      // 获取顶级父元素
      let app = document.querySelector('#app')
      console.log();
      // 获取 app 下所有子元素 
      const nodes = app.childNodes
      const nodess = app.children
      // 遍历子元素
      nodes.forEach(node => {
        // 判断是否是节点
        if (node.nodeType === 1) {
          //attrs接收当前节点所拥有的所有属性
          const attrs = node.attributes
          //因为是个伪数组所以通过Array.from转换成数组
          // 然后遍历
          Array.from(attrs).forEach(attr => {
            // 接收每个属性的属性名字
            const nodeName = attr.nodeName
            //接收每个属性的属性值
            const nodeValue = attr.nodeValue
            // 如果属性名字等于v-model,就让节点的值等于打他属性中对应的值
            if (nodeName === 'v-model') {
              // 当data中数据发生变化时,改变文本框的value属性,以更新文本框的值 
              // console.log(data[nodeValue]);
              // 更改input的value值
              // node.value = data[nodeValue]
              // 更改span的innerHTML值
              node.innerHTML = data[nodeValue]
              // 当文本框的内容发生变化时,更新 data 中属性的值 
              // 双向数据绑定input  输入框变化时候触发
              node.addEventListener('input', e => {
                console.log(e.target.value);
                // 让值随时等input的输入的值
                data[nodeValue] = e.target.value
              })
            }
          })
        }
      })
    }
    compile() 
  </script>
</body>

</html>

在这里插入图片描述

vue3响应式原理

vue3的响应式原理是通过Proxy来完成。Proxy直接监听对象,所以将多个属性转换成getter或者setter的时候不需要循环了
Proxy是es6的新增,ie不支持

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Proxy</title>
</head>

<body>
  <div id="app">hello</div>
  <script>
    //模拟Vue中的data选项
    let data = {
      msg: "hello",
      count: 0,
    };
    //模拟Vue实例
    //为data创建一个代理对象vm,这样就可以通过vm.msg来获取data中的msg属性的值,而这时候会执行get方法
    let vm = new Proxy(data, {
      // 当访问vm的成员时会执行
      //target表示代理的对象(这里为data对象),key表示所代理的对象中的属性
      get(target, key) {
        console.log("get key:", key, target[key]);
        return target[key];
      },
      //当设置vm的成员时会执行
      //newValue是更改后的新值
      set(target, key, newValue) {
        console.log("set key:", key, newValue);
        if (target[key] === newValue) {
          return;
        }
        target[key] = newValue;
        document.querySelector("#app").textContent = target[key];
      },
    });
    //测试
    vm.msg = "aaaa";
    console.log(vm.msg);
  </script>
</body>

</html>

在这里插入图片描述

Vue3 的proxy和Vue2 的Object.defineProperty的对比

Proxy的优势如下

  • Proxy可以直接监听整个对象而非属性。
  • Proxy可以直接监听数组的变化。
  • Proxy有13种拦截方法,如ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
  • Proxy做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。

Object.defineProperty 的优势如下
兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。

Object.defineProperty 不足在于:

1、Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
2、Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
3、Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
4、Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete来实现响应式的。

3.观察者模式:

观察这模式分为注册环节发布环节
就比如,我想玩一个还没发行的游戏,然后我当时看见一时的兴起很想去玩这款游戏但是游戏还没发布时间久了我可能就忘了这个游戏了即使发布上线了我可能也不会去玩了,像我这样的游戏玩家肯定也很多,所以游戏开发商想留下这些客流量怎么办呢,预定让用户留下账号这就是观察者的注册环节,当游戏发行后游戏开发者会统一通知预定的玩家这就叫观察者的发布环节

<script>
    var players = []
    //新增玩家
    function newPlayers(username) {
      if (username) {
        console.log('预约成功!');
      }
      this.players.push(username)
    }
    // 通知玩家
    function releasingNotices() {
      this.players.forEach(item => {
        console.log('通知' + item + '玩家成功');
      });
    }
    // admin玩家预定
    newPlayers('admin')
    // // admin123玩家预定
    newPlayers('admin123')


    // 游戏发布了,通知所有预约玩家
    releasingNotices()
  </script>

在这里插入图片描述

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、模拟Vue响应式原理

模拟一个小的Vue,在使用vue的时候会先new一个vue实例,现在尝试着写一个vue类,
vue类的功能都有那些呢?

  • 接收初始化的参数(选项)
  • 把data中的属性注入到Vue实例,转换成getter/setter(可以通过this来访问data中的属性)
  • 调用observer监听data中所有属性的变化(当属性值发生变化后更新试图)
  • 调用compiler解析指令/差值表达式结构

Vue中包含了_proxyData这个私有方法,该方法的作用就是将data中的属性转换成getter/setter并且注入到Vue的实例中。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <h1>差值表达式</h1>
    <h3>{{msg}}</h3>
    <h3>{{count}}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg" />
    <input type="text" v-model="count" />
  </div>
  <script src="./Vue.js"></script>
  <script>
    let vm = new Vue({
      el: "#app",
      data: {
        msg: "Hello World",
        count: 12,
      },
    });
  </script>
</body>

</html>
class Vue {
  // 构造函数
  constructor(options) {
    // 1、通过属性保存选项的数据
    // options:表示在创建Vue实例的时候传递过来的参数,将其保存到$options中。
    this.$options = options || {};
    //获取参数中的data属性保存到$data中.
    this.$data = options.data || {};
    this.$el =
      // 如果options.el是字符串类型就让$el等于el节点,
      //否则就等于el自身 
      typeof options.el === "string"
        ? document.querySelector(options.el)
        : options.el;
    // 2、把data中的成员转换成getter和setter,注入到vue实例中.
    //通过proxy函数后,在控制台上,可以通过vm.msg直接获取数据,而不用输入vm.$data.msg
    // `Vue`中包含了`_proxyData`这个私有方法,
    //该方法的作用就是将`data`中的属性转换成`getter/setter`并且注入到`Vue`的实例中。
    this._proxyData(this.$data);
    //3.调用observer对象,监听数据的变化
    //4.调用compiler对象,解析指令和差值表达式
  }
  _proxyData (data) {
    //遍历data中的所有属性
    Object.keys(data).forEach((key) => {
      // 把data中的属性输入注入到Value实例中,注意,这里使用的是箭头函数,this表示的就是Vue的实例。
      //后期我们可以通过this的形式来访问data中的属性。
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {
          return data[key];
        },
        set (newValue) {
          if (newValue === data[key]) {
            return;
          }
          data[key] = newValue;
        },
      });
    });
  }
}

在这里插入图片描述

Observer

observer模块在vue目录的位置 :src / code / observer

observer模块总共分为三个部分:

  • observer : 数据的观察者,监视数据对象的读、写
  • Watcher: 数据的订阅者,Watcher可以监听到数据变化然后做出相应的操作
  • Dep: 他是Observer和Watcher的中心,当数据发生变化的时候,observer会监视到然后通过Dep告诉watcher

Observer的功能 :

  • 将data中的属性转化为响应式数据
  • data中的属性如果也是对象那么也将这个属性转化为对象
  • 数据发生变化给予通知

在这里插入图片描述
完成一个Object.js

class Observer {
  constructor(data) {
    this.walk(data);
  }
  // walk作用:1、判断data是否为空,是不是对象
  //              2、如果是对象就遍历对象的属性值然后调用defineReactive方法并将
  //                 对象、属性名、属性值参数传过去。
  walk (data) {
    //1、判断data是否是对象,以及data是否为空
    if (!data || typeof data !== "object") {
      return;
    }
    // 2、遍历data对象中的所有属性为每一个属性增加get和set
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  //defineReactive的作用:
  //1、调用属性的时候返回属性的值。
  //2、更改属性值的时候先判断是否更改值与原值相同不相同返回新值。
  defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        console.log('属性被调用了');
        return val;
      },
      set (newVal) {
        console.log('属性被更改了');
        if (newVal === val) {
          return;
        }
        val = newVal;
        //发送通知,更新视图
      },
    });
  }
}

然后我们在vue.js中new一下并将data传递过去

    //3.调用observer对象,监听数据的变化
    new Observer(this.$data);

然后再html中引入注意vue.js中用到了observer.js中的类所以将observer.js引入放到上面

  <script src="./observer.js"></script>
  <script src="./Vue.js"></script>

完善defineReactive方法

思考一个问题,如果我们再data中添加一个属性,那么这个属性是不是响应式的呢?

  data: {
        msg: "Hello World",
        count: 12,
        list: {
          name: 'wbw'
        }
      },

在这里插入图片描述
vue中不管是data中的属性还是data中对象中的属性都是响应式的,而我们这里只有data下面的属性是响应式的属性下的对象中的属性不是响应式的。

问题所在,先看一下之前的代码

class Observer {
  constructor(data) {
    this.walk(data);
  }
  walk (data) {
    if (!data || typeof data !== "object") {
      return;
    }
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        return val;
      },
      set (newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
      },
    });
  }
}

首先会先执行walk方法如果data属性不是一个对象就不做什么如果是对象,
就会遍历里面的属性并给予每个属性增加getter和setter,但是如果data属性中有对象那么这个对象属性里面的属性就不会被加上getter和setter。

解决这个问题的办法很简单,我们都知道walk属性对data属性进行判断然后如果是对象的话就会进行遍历,因为只对第一层属性遍历所以第二层的属性就不是响应式,所以我们在遍历方法中重新去调用walk方法对单个方法在进行判断是否为对象是对象再次遍历然后再次判断直到没有对象为止,所以解决办法就是再defineReactive中再次调用walk方法即可。

  defineReactive (obj, key, val) {
    let that = this
    //对data中对象的属性就行判断
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        console.log('属性被调用了');
        return val;
      },
      set (newVal) {
        console.log('属性被更改了');
        if (newVal === val) {
          return;
        }
        val = newVal;
        //发送通知,更新视图
      },
    });
  }

在这里插入图片描述

那么接下来,看一下我们将一个属性更改成对象的话这个对象的中的属性会不会是响应式的呢?
在这里插入图片描述
很显然并没有因为改变属性而触发“属性改变了”的输出,这说明这不是响应式的,接下来就来解决这个问题。
我们知道响应式是通过遍历属性给一个一个附加的但是更改属性被更改后不会被附加上setter,我们要做的就是当属性更改后我们去判断更改后的属性是不是对象是的话就给他加上getter/setter就好了。与上面方法相同在defineReactive方法中的set方法中再次调用walk方法就解决了。

 defineReactive (obj, key, val) {
    let that = this
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        console.log('属性被调用了');
        return val;
      },
      set (newVal) {
        console.log('属性被更改了');
        if (newVal === val) {
          return;
        }
        val = newVal;
        //发送通知,更新视图
        // 重新调用walk方法
        that.walk(newVal)
      },
    });
  }

在这里插入图片描述

Compiler

Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render而render的作用主要是为了生成VNode
Compiler的功能:

  • 负责编译模板,解析指令/差值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

compileText方法的作用就是对对插值表达式进行解析.现在来尝试去实现compileText方法

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    //调用compile方法
    this.compile(this.el)
  }
  //编译模板,处理文本节点和元素节点.
  compile (el) {
    //获取子节点.
    let childNodes = el.childNodes;
    // console.log(Array.from(childNodes));
    //childNodes是一个伪数组,需要转换成真正的数组,然后可以执行forEach来进行遍历,每遍历一次获取一个节点,然后判断节点的类型.
    // Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例
    Array.from(childNodes).forEach((node) => {
      // console.log(node);
      //处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node);
      }
      //判断node节点,是否还有子节点,如果有子节点,需要递归调用compile方法
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }
  // 编译元素节点,处理指令
  compileElement (node) { }
  // 编译文本节点,处理差值表达式
  compileText (node) {
    console.dir(node.textContent);
    // {{ msg }}
    //我们是用data中的属性值替换掉大括号中的内容
    let reg = /\{\{(.+)\}\}/;
    //获取文本节点的内容
    let value = node.textContent;
    //判断文本节点的内容是否能够匹配正则表达式
    // test() 方法用于检测一个字符串是否匹配某个模式.
    if (reg.test(value)) {
      //获取插值表达式中的变量名,去掉空格($1 表示获取第一个分组的内容。)
      let key = RegExp.$1.trim();
      console.log(this.vm[key]);
      //根据变量名,获取data中的具体值,然后替换掉差值表达式中的变量名.
      // replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
      // regexp / substr  必需。规定子字符串或要替换的模式的 RegExp 对象。
      // 请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象。
      node.textContent = value.replace(reg, this.vm[key]);
    }
  }
  //判断元素属性是否为指令
  isDirective (attrName) {
    //指令都是以v-开头
    return attrName.startsWith("v-");
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    //nodeType: 节点的类型  1:元素节点  3:文本节点
    return node.nodeType === 1;
  }
  //判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3;
  }
}

在vue中new一下Compiler

    //4.调用compiler对象,解析指令和差值表达式
    new Compiler(this)

引入

  <script src="./observer.js"></script>
  <script src="./Compiler.js"></script>
  <script src="./Vue.js"></script>

在这里插入图片描述
compileElement方法,是完成指令的解析。冲奥里给!

1、获取当前节点下的所有的属性,然后通过循环的方式,取出每个属性,判断其是否为指令
2、 如果是指令,获取指令的名称与指令对应的值.
3、 分别对v-text指令与v-model指令的情况进行处理.
// 编译元素节点,处理指令
  compileElement (node) {
    //通过node.attributes获取当前节点下所有属性,node.attributes是一个伪数组
    Array.from(node.attributes).forEach((attr) => {
      //获取属性的名称
      let attrName = attr.name;
      //判断是否为指令
      if (this.isDirective(attrName)) {
        //如果是指令,需要分别进行处理,也就是分别对v-text与v-model指令
        //进行处理。
        //为了避免在这里书写大量的if判断语句,这里做一个简单的处理.
        //对属性名字进行截取,只获取v-text/v-model中的text/model
        attrName = attrName.substr(2);
        //获取指令对应的值 v-text指令对应的值为msg,v-model指令对应的值为msg,cout
        let key = attr.value;
        this.update(node, key, attrName);
      }
    });
  }
  update (node, key, attrName) {
    //根据传递过来的属性名字拼接Updater后缀获取方法。
    let updateFn = this[attrName + "Updater"];
    updateFn && updateFn(node, this.vm[key]); //注意:传递的是根据指令的值获取到的是data中对应属性的值。
  }
  //处理v-text指令
  textUpdater (node, value) {
    node.textContent = value;
  }
  //处理v-model
  modelUpdater (node, value) {
    //v-model是文本框的属性,给文本框赋值需要通过value属性
    node.value = value;
  }

在这里插入图片描述

在这里插入图片描述

Dep类

创建Dep类

class Dep {
  constructor() {
    //存储所有的观察者
    this.subs = [];
  }
  //添加观察者
  addSub (sub) {
    //判断传递过来的内容必须有值同时还必须是一个观察者,观察者中会有一个update方法
    if (sub && sub.update) {
      this.subs.push(sub);
      // console.log(sub);
    }
  }
  //发送通知
  notify () {
    this.subs.forEach((sub) => {
      sub.update();
      // console.log(sub);
    });
  }
}

defineReactive中改动一下

  defineReactive (obj, key, val) {
    let that = this
    let dep = new Dep()
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        //收集依赖,就是将watcher观察者添加到subs数组中。
        //这里可以通过Dep中的target来获取观察者(watcher对象),当然target属性还没有创建
        //后期在创建Watcher观察者的时候,来确定target属性
        Dep.target && dep.addSub(Dep.target);
        // console.log(Dep.target);
        // console.log('属性被调用了');
        return val;
      },
      set (newVal) {
        // console.log('属性被更改了');
        if (newVal === val) {
          return;
        }
        val = newVal;
        //发送通知,更新视图
        // 重新调用walk方法
        that.walk(newVal)
        dep.notify();
      },
    });
  }

更改complieText()方法

  compileText (node) {
    // console.dir(node.textContent);
    // {{ msg }}
    //我们是用data中的属性值替换掉大括号中的内容
    let reg = /\{\{(.+)\}\}/;
    //获取文本节点的内容
    let value = node.textContent;
    //判断文本节点的内容是否能够匹配正则表达式
    // test() 方法用于检测一个字符串是否匹配某个模式.
    if (reg.test(value)) {
      //获取插值表达式中的变量名,去掉空格($1 表示获取第一个分组的内容。)
      let key = RegExp.$1.trim();
      //根据变量名,获取data中的具体值,然后替换掉差值表达式中的变量名.
      // replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
      // regexp / substr  必需。规定子字符串或要替换的模式的 RegExp 对象。
      // 请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象。
      node.textContent = value.replace(reg, this.vm[key]);
      //创建Watcher对象,当数据发生变化后,更新视图
      new Watcher(this.vm, key, (newValue) => {
        //newValue是更新后的值
        node.textContent = newValue;
      });
    }
  }

引入js

<script src="./js/dep.js"></script>    
<script src="./js/watcher.js"></script>   
<script src="./js/compiler.js"></script>   
<script src="./js/observer.js"></script>    
<script src="./js/vue.js"></script>

在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值