vue源码学习分析

主要了解学习了它得一些核心思想和功能,深入了解它得一个数据驱动,响应式原理,watcher,dep,observe等一些简单得实现。(简化得方便于我们理解它得一个核心理念)
解决疑问:

1.vue得模板解析完整过程 和 原理
2.Vue 如何实现得双向绑定原理
3.Vue得发布订阅模式 watcher dep OBserver
4.vue如何将数据挂载到实例上得
5.diff算法 2和3(暂无)
6.柯里化函数 偏函数 高阶函数
7.解析模板数据生成虚拟DOM得时候对数组得处理
8.vue中得私有数据 和 只读数据
9.vue中各函数得具体作用
10.vue的局部更新是怎么样的
11.什么是发布订阅模式,他的事件模型是怎么样的 跟webpack中的bapable差不多
12.vue中得watcher得理解(依赖收集,派发更新)
13.vue中得以组件为单位刷新数据得
14.vue得二次提交
15.vue得脏数据, 只更新了部分数据,导致数据不一致
16.vue是怎么实现局部刷新得(其实就是局部组件得刷新,watcher和dep)

vue得一个主要流程 (代码中得JGvue,我就直接说是vue了)

目录:

  • new Vue() -> 初始化数据响应式 -> 内置编译模板转化 -> 字符串 -> AST(缓存) -> render方法编译 ->结合数据依赖收集depend,派发更新notify -> 虚拟DOM -> 通过watcher调用update方法 -> patch(核心方法createFunctionPatch) -> 将虚拟DOM 转化成真实DOM
  • 修改数据 -> notify触发 -> dep中对应得watcher 一一促发 -> 结合数据生成虚拟DOM -> watcher调用update -> diff算法差异 -> patch(核心方法createFunctionPatch) -> 将虚拟DOM 转化成真实DOM
  1. 初始化实例
  2. 初始化数据data
  3. 编译渲染过程和数据响应式
  4. watcher 和 dep 依赖收集,派发更新
  5. diff算法了解
  6. 其他知识点
  7. 简单了解 methods @click vue得处理
    完整得项目结构
    在这里插入图片描述

1.初始化实例

  • 准备一个html,这不多说就是一个html 文件 使用我们自己写得vue
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <div>
            <div>
                <p>{{name}}-{{message}}</p>
            </div>
        </div>
        <p>{{name}}</p>
        <p>{{message}}</p>
        <p>{{obj.sex}}</p>
    </div>
    <!-- 执行顺序 -->
    <script src="./watcher.js"></script>
    <script src="./dep.js"></script>
    <script src="./vnode.js"></script>
    <script src="./compiler.js"></script>
    <script src="./Vue.js"></script>
    <script src="./initrender.js"></script>
    <script src="./initData.js"></script>
    <script>
        let app = new JGVue({
            el: "#app",
            data: {
                name: "yang",
                message: "模板转换",
                obj: {
                    sex: "男"
                },
                list: [{
                    info: "aaa"
                }, {
                    info: "bbb"
                }, {
                    info: "ccc"
                }]
            }
        })
  </script>
</body>

</html>

  • vue.js中就是一个简单得实例,里面将数据等存储一些,在Vue中有个默认得规则带_得数据是内部数据 带$得是只读属性,准备一些数据,比如DOM,我这里就直接获取得DOM,再将它得父Dom也拿一些,

注意:Vue中其实获取得是字符串,然后将字符串转译成AST,这里涉及到了编译原理,所以暂时用Dom去转成VNode(带有{{name}}得坑)来代替AST。

function Vue(options) {
    this._options = options;
    this._data = options.data;
    // this._templete = options.el; //vue中是字符串,这里是DOM
    this._templete = document.querySelector(options.el); //vue中是字符串,这里是DOM
    this._parent = this._templete.parentNode;

    // 初始化数据
    this.initData();
    // 挂载
    this.mount();
}

2.初始化数据

  • initData(); vue 源码中在这里初始化了很多东西,例如watch,computed等,下图就是vue源码中得
  • 在这里插入图片描述
  • 我们这里简单得来实现下初始化data
    新建一个文件initdata.js.。代码中有注释,下面也会大致得理一下逻辑
initdata.js

// 扩展数组得方法 使其响应式变化    这里就例举4个方法
let ARRAY_METHOD = ["push", "pop", "shift", "unshift"];
// 继承关系 arr->Array.prototype->Object.prototype.....
// 修改继承关系 arr->改写得方法->Array.prototype->Object.prototype.....
// 创建一个对象
let array_method = Object.create(Array.prototype);
ARRAY_METHOD.forEach((method) => {
    // 重写array_method中得对应方法 函数拦截
    array_method[method] = function () {
        // 将数据响应式化
        for (let item of arguments) {
            Observer(item);
        }

        // 调用原来得方法 Array.prototype[method] 正常操作
        console.log("函数拦截");
        let res = Array.prototype[method].apply(this, arguments);
        return res;
    };
});
// vue中定义了一个函数  defineRetive(target,key,value,enumerable)  存储数据变化
function defineReactive(target, key, value, enumerable) {
    if (typeof value === "object" && value != null) {
        // 非数组得处理
        Observer(value);
    }
    let dep = new Dep();
    dep.__protoName__ = key;

    Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
            console.log("get数据:" + value);
            // 依赖收集 :告诉当前得watcher哪些属性被访问了
            dep.depend();
            return value;
        },
        set(newVal) {
            console.log("set数据:" + newVal);
            if (typeof newVal === "Object" && newVal != null) {
                // 重新赋值得数据赋值响应式,因此如果传入得是对象类型,将其转换为响应式
                Observer(newVal);
            }
            value = newVal;

            // 派发更新, 找到全局的 watcher, 调用 update
            dep.notify();
        },
    });
}

// // 将对象转换为响应式  vm就是我们得vue实例 调用时处理上下文
function Observer(obj) {
    if (Array.isArray(obj)) {
        // 数组 只实现push pop
        // vue2.0中针对直接改变数组得长度,vue是不能监听到变化得,因为数组length属性是不能被get 和 set得所以无法监听,他使用了$set来实现,实际也是调用了defineProperty
        // vue3 用proxy解决了这问题
        // 扩展数组得push pop   修改要进行响应式化得原型     函数拦截
        obj.__proto__ = array_method;
        for (let i = 0; i < obj.length; i++) {
            // 对数组中得没个元素进行处理
            Observer(obj[i]);
        }
    } else {
        // 递归 :将obj[key]变成响应式  对象或值类型
        let keys = Object.keys(obj);
        for (let its of keys) {
            defineReactive(obj, its, obj[its], true);
        }
    }
}

// __proto__  vue中做了兼容,如果支持,直接修改他得原型链,不支持得化 在目标对象上混入方法

// 将某一个对象得属性访问 映射到 对象得某一个属性成员上  列入映射到app实例上访问 实际式访问app._data中得数据
// vue实例上  _开头数据都是私有数据  $开头数据 只读数据  尽量不要访问,所以vue 将_得访问 交给了实例
function proxy(target, prop, key) {
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get() {
            return target[prop][key];
        },
        set(newVale) {
            target[prop][key] = newVale;
        },
    });
}

// 初始化数据
Vue.prototype.initData = function () {
    //1.将thia._data 数据响应式   2.将直接属性代理到实例上
    let keys = Object.keys(this._data);
    // 数据响应式
    Observer(this._data);
    // 代理
    for (let item of keys) {
        // 将app._data 数据源 映射到app上 app._data.name ==> app.name
        proxy(this, "_data", item);
    }
};

initData();初始化数据,干了两件事

1.将thia._data 数据响应式Observer() 来实现

  • Observer()

Observer() 方法 是用来处理数据响应式得,重写data属性中得get set方法。 这个方法得逻辑:就是一个递归调用,判断传入得参数是否是数组
——不是数组:调用 defineReactive 数据响应式化
——是数组:数组这里稍微有点麻烦,数组得方法push,pop等是无法被监听到得,所以需要做一个函数得拦截:
(就是改变数据得原型链,arr->Array.prototype中间增加一环,通过prototype赋值,重写他得这些方法,里面去做一个数据得响应式Observer)

  • defineReactive()

这个方法就是封装了Object.defineProperty()来重写get set方法;enumerable这个属性如果式false,就可以读取,不会被遍历;

  • 这里有个依赖收集和派发更新,可以在渲染得时候去讲。

2.将直接属性代理到实例上

app实例中为了方便访问data中得数据,会将data中得数据映射到app实例上 ,通过一个proxy方法。其实就是对data中地址得一个引用。

3.编译渲染过程和数据响应式

this.mount()方法;

新建
initrender.js //初始化渲染相关函数, new 一个watcher实例
compiler.js //编译功能
getVNode.js //虚拟Dom
代码中都有注释,下面会大致得去理下逻辑

initrender.js

Vue.prototype.mount = function () {
    // // 判断是否有自定义的render函数
    // if (typeof this._options.render !== "function") {

    // }
    // 需要提供一个render函数 跟数据结合生成虚拟DOM
    this.render = this.createRenderFn();

    this.mountComponent();
};

// 发布订阅模式:  例如 老板发布一个商品列表  你需要订阅某个商品,老板记录谁定了什么,等到商品有了之后,老板会通知你(挨个通知)
// 1.有个一个容器,来存储需要被促发得东西
// 2.有一个方法,可以往容器里存储东西
// 3.有一个方法,可以将容器里的东西拿出来使用
// 事件模型 :发布订阅  有一个event对象  里面有 on(订阅/注册),off(移除),emit(发布)方法
// 实现事件模型
Vue.prototype.mountComponent = function () {
    // 执行mountComponent()
    let mount = () => {
        // vue中是以组件为单位来判断,以节点为单位来局部更新 ,内部组件 diff算法比较的时候,发现更改就替换对应的虚拟DOM树,自定义组件,就会判断更新的是哪各组件,只会更新发生变化的组件,其他的不会更新
        this.update(this.render());
    };

    console.log("挂载");
    // 这个 Watcher 就是全局的 Watcher, 在任何一个位置都可以访问他了 ( 简化的写法 )
    new Watcher(this, mount); // 相当于这里调用了 mount
};

/*
 *   在真正得vue中使用了二次提交得 设计结构  (所有得操作都在虚拟DOM中完成)
 *   vue只要数据发生变化 就会每次都生成一个新得vnode,  与原有得vnode相比较 ,这就是diff算法,相同得忽略,不同得更新过去,然后就更新了我们真得DOM
 */
// 用柯里化函数得方式 1.生成rander函数(render是用来生成新得虚拟DOM) 2.目的是缓存抽象语法树
Vue.prototype.createRenderFn = function () {
    // 缓存AST 这里暂时将ast当作 vnode形式得虚拟DOM
    let ast = getVNode(this._templete);

    return function render() {
        // 将ast抽象语法树和数据 合成虚拟DOM
        return combine(ast, this._data);
    };
};

//目的:将将虚拟DOM 渲染到页面中。 注意一个算法 diff算法就在这
// 问题 为什么不把新得vnode 直接替换原来得vnode,主要是因为原来得vnode 跟跟 真实得DOM 都是一一对应得,如果替换了,你得重新处理他们得一个对应关系,又涉及到了遍历,耗性能
Vue.prototype.update = function (node) {
    // 这里暂时直接转成真实DOM  VUE中其实使用到了diff算法
    this._parent.replaceChild(parseVNode(node), document.querySelector("#app"));
};

compiler.js

// 编译
// HTML 使用递归 来遍历DOM元素 生成虚拟DOM
// Vue 中源码使用得是 栈结构 + 递归
function getVNode(node) {
    let nodeType = node.nodeType;
    let _vnode = null;
    if (nodeType === 1) {
        // 元素
        // 组装参数
        let nodeName = node.nodeName;
        let attrs = node.attributes; //是个伪数组
        let _attrObj = {};
        for (let item of attrs) {
            _attrObj[item.nodeName] = item.nodeValue;
        }
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType);

        // 考虑node得子元素
        let childNode = node.childNodes;
        for (let item of childNode) {
            _vnode.appendChild(getVNode(item));
        }
    } else if (nodeType === 3) {
        // 文本节点
        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
    }
    return _vnode;
}

// 虚拟DOM转换成 真实DOM
function parseVNode(vnode) {
    // 创建真实得DOM
    let type = vnode.type;
    let _node = null;
    if (type === 3) {
        // 创建文本节点
        return document.createTextNode(vnode.value);
    } else if (type === 1) {
        // 创建元素节点
        _node = document.createElement(vnode.tag);
        // 属性
        let data = vnode.data;
        Object.keys(data).forEach((key) => {
            let attrValue = data[key];
            _node.setAttribute(key, attrValue);
        });
        // 子元素
        let children = vnode.children;
        children.forEach((subvnode) => {
            // 递归转换
            _node.appendChild(parseVNode(subvnode));
        });
        return _node;
    }
}

// 将ast抽象语法树和数据 合成虚拟DOM
function combine(vnode, data) {
    let _type = vnode.type;
    let _data = vnode.data;
    let _value = vnode.value;
    let _tag = vnode.tag;
    let _children = vnode.children;
    let _vnode = null;
    if (_type === 1) {
        // 元素节点
        _vnode = new VNode(_tag, _data, _value, _type);
        _children.forEach((subvnode) => {
            // 递归转换
            _vnode.appendChild(combine(subvnode, data));
        });
    } else if (_type === 3) {
        // 文本节点
        _value = _value.replace(r, (_, g) => {
            return getValueByPath(data, g.trim());
        });
        _vnode = new VNode(_tag, _data, _value, _type);
    }
    return _vnode;
}

let r = /\{\{(.+?)\}\}/g;
// 通过字符串来访问对象成员 为了匹配{{obj.sex}}
function getValueByPath(obj, path) {
    let paths = path.split(".");
    // 处理数组aa[0][1].cc[0].name
    let res = obj;
    for (let i = 0; i < paths.length; i++) {
        res = res[paths[i]];
    }
    return res;
}

```bash
getVNode.js

// 转换成虚拟DOM 提升性能 {tag :"div",data:{class:"aaa"},value:"",children:[{}]}  操作DOM元素 浏览器需要去刷新 耗性能
class VNode {
    // 标签,属性,值,类型
    constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
    }

    appendChild(vnode) {
        this.children.push(vnode);
    }
}

解析:mount()函数中创建了render函数,创建得函数 是一个柯里化函数,在vue中 ast生成之后会缓存起来得(用于方便每次修改数据结合数据生成虚拟DOM),因为解析字符串->AST是非常耗性能得。

  • 模板 -> 字符串 -> AST抽象语法树

vue中是将模板转化成字符串,然后解析成AST,这里得算法比较复杂,涉及到了编译原理,就没有深究。 我上面是将DOM树转化成Vnode结点得形式来替代(value值带有{{name}}此类得);

  • AST -> 虚拟DOM树 -

1.createRenderFn函数中创建了render函数,创建得函数 是一个柯里化函数,在vue中 ast生成之后会缓存起来得(用于方便每次修改数据结合数据生成虚拟DOM),因为解析字符串->AST是非常耗性能得。

2.这里需要一个watcher 来执行mount 方法中得 update(更新页面得)方法。render方法作为update得参数使用返回 虚拟DOM

3.render方法主要是来生成combine方法就是ast结合数据 生成虚拟DOM 就是一个递归转换 解析模板中得双括号 就用正则 let r = /{{(.+?)}}/g; getValueByPath(); 这里设置值得时候,会访问属性得get属性。这个时候 就涉及到了 依赖收集 和 派发更新 需要用到watcher 和 Dep 下面单独讲

4.update方法使用render方法返回组装好得虚拟DOM之后, update方法中使用到了diff算法 更新页面(patch方法,其核心就是createPatchFunction() 调用里面的各种对元素处理的方法)。

4.watcher 和 dep 依赖收集,派发更新

  • 逻辑分析:
  • 1.mountComponent中new Watcher一个实例。 会执行watcher中得get方法this.getter就是传入得mount方法,执行前后调用pushTarget和popTarget两个方法是Dep中得方法,用来设置和清楚全局得渲染watcher
  • 2.watcher中this.getter执行得时候,会使用render方法编译模板访问属性,就会触发属性得get, 这个时候会做一个依赖收集,每个属性都会new 一个Dep实例,里面会将当前得watcher 存放入Dep,同时也会将当前得dep存入watcher,是一个相互引用得关系,双向链表。
  • 3.Dep中有个Dep.target用来存放当前得watcher。同时也还有一个watcher栈,用来存放,mount方法执行完毕之后。清楚当前得Dep.target;
  • 4.触发属性改表。会调用属性得set方法 ,里面调用 dep中得notify来派发更新,就是调用当前属性对应得Dep中存储得watcher,一一触发更新页面
  • 5.watcher可以存在多个,多组件得时候,每个组件都会初始化,都会有对应得watcher.正因为如此,vue才能实现一个局部更新(最小以组件为单位),而不是全部更新。
watcher .js

// watcher 方法
// get:用来计算或者执行函数(computed,watch两个 两者都要执行)   真正处理渲染得方法
// update: 公共得外部方法,会触发内部得run方法
// run:运行,来判断内部是否使用异步运行 (服务端渲染同步处理得),这个run 最终会调用内部得get方法
// cleanupDep:清楚队列

// 在vue中有很多得组件,每个组件都有对应得watcher 都是独立得  局部更新得
// 在模板渲染得时候,将watcher存入全局,访问各个属性值get方法得时候,会做一个依赖收集,将当前得一个渲染watcher存入到全局得容器Dep中, 两者互相关联 引用,然后再改变属性得时候在set方法中去做一个派发更新,通过Dep中得notify找到对应得渲染watcher,渲染页面,渲染完成后移除全局得渲染watcher,watcher如果是局部的watcher就是局部更新

// 观察者watcher,用来发射行为
// 记录个数
let watcherid = 0;
class Watcher {
    // 参数有很多 就写2个先
    // vm Vue实例
    // expOrfn:字符串或者函数  watch传入得是 路径,computed传入得是个函数
    constructor(vm, expOrfn) {
        this.vm = vm;
        this.getter = expOrfn;

        this.id = watcherid++;

        this.deps = []; //依赖项
        this.depIds = {}; //是一个Set类型,用于保证 依赖项得唯一性(简化得代码,暂时不实现)

        // 一开始渲染  vue中:this.lazy?undefined:this.get()
        this.get();
    }

    // 计算触发getter
    get() {
        // 将当前操作得watch 存储到全局得watch
        pushTarget(this);

        this.getter.call(this.vm, this.vm); // 上下文的问题就解决了

        // 剔除
        popTarget();
    }

    run() {
        this.get();
    }

    update() {
        this.run();
    }

    // 清空dep 依赖队列
    cleanupDep() {}

    /** 将 当前的 dep 与 当前的 watcher 关联 */
    addDep(dep) {
        this.deps.push(dep);
    }
}

Dep.js
let depid = 0;
// Dep是负责存放数据所绑定所有的观察者的对象的容器,只要数据发生改变,就会通过这个Dep来通知所有观察者进行修改数据。(每个数据都有独一无二的Dep)
// 负责将属性 和 watcher关联
class Dep {
    constructor() {
        this.id = depid++;
        // 存储与当前dep关联得watch
        this.subs = [];
    }

    // 添加watcher
    addSub(sub) {
        this.subs.push(sub);
    }

    // 移除
    removeSub(sub) {
        for (let i = this.subs.length - 1; i >= 0; i--) {
            if (sub === this.subs[i]) {
                this.subs.splice(i, 1);
            }
        }
    }

    // 将dep和watch关联  互相引用 双向链表
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target);
            Dep.target.addDep(this);
        }
        console.log("读取属性");
        console.log(this.subs, Dep.target);
    }

    /** 触发与之关联的 watcher 的 update 方法, 起到更新的作用 */
    notify() {
        // 在真实的 Vue 中是依次触发 this.subs 中的 watcher 的 update 方法
        // 此时, deps 中已经关联到 我们需要使用的 那个 watcher 了
        console.log("派发更新");
        let deps = this.subs.slice();
        deps.forEach((watcher) => {
            watcher.update();
        });
    }
}

// 全局得容器 存储渲染得watcher
Dep.target = null;
// 在全局中创建一个 watcher栈 把一个操作中需要使用得watcher存起来,在watcher调用get方法得时候将当前watcher放到全局,在get执行结束之后将这个watcher移除
let targetStack = [];

// 将当前操作得watcher 存储到 全局watcher中
function pushTarget(target) {
    // vue中使用得是Push
    targetStack.unshift(Dep.target);
    Dep.target = target;
}

// 将watcher剔除  vue中使用得是Push
function popTarget(target) {
    targetStack.shift();
    Dep.target = targetStack[targetStack.length - 1];
    console.log(Dep.target);
}

/*
 *   在watcher调用get方法得时候调用pushTarget(this)
 *   结束得时候popTarget()
 */

5.diff算法了解

  • vue2:中的diff算法 全量对比 ,深度优先,同层比较, 例如

    数据发生变化 上一次得渲染DOM树 和 更新数据后得DOM树 diff算法全量比较(实际a变化,但是div p还是都会比较一编)
  • vue3:在创建DOM树得时候 动态数据会添加一个静态标记,这样对比得时候就直接对比有标记得就行了

6.其他知识点

  • 函数拦截:vue中对数组方法得扩展值得借鉴,
  • 元素得映射 例如:app.data.name ===>app.name访问 proxy方法。其实就是使用了object.defineProperty get set得时候返回app.data.name
  • 发布订阅模式:1.有个一个容器Dep,来存储需要被促发得watcher,2.有一个方法depend,可以往容器里存储watcher,有一个方法notify,可以将容器里的东西拿出来使用
  • 事件模型 :发布订阅 有一个event对象 里面有 on(订阅/注册),off(移除),emit(发布)方法
  • 局部更新: vue中是以组件为单位来判断,以节点为单位来局部更新 ,内部组件 diff算法比较的时候,发现更改就替换对应的虚拟DOM树,自定义组件,就会判断更新的是哪各组件,只会更新发生变化的组件,其他的不会更新
  • makeMap:vue源码中得一个方法来判断是否是html标签(很好得一个柯里化函数例子)
  • vue中得提交:都是二次提交,就会每次都生成一个新得vnode, 与原有得vnode相比较 ,这就是diff算法,相同得忽略,不同得更新过去,然后就更新了我们真得DOM
  • 1.柯里化函数,将原本一个函数接受多个参数,改成接受一个参数,return 一个新函数来处理剩下得结果,
    为什么使用:为了提升性能,可以缓存一部分能力
    vue模板渲染得本质是使用html得模板字符串, 模板->AST->VNode->DOM
    最消耗性能得是 模板->AST; 需要对字符串解析 例如解析一个数字运算得字符串求结果 很麻烦(波兰式表达式)
  • 2.偏函数:将原本一个函数接受多个参数,改成传入一部分函数 ,return 一个新函数来处理剩下得结果
  • 3.高阶函数: 一个函数得参数是一个函数,该函数对这个函数加工,得到一个函数,这个加工得函数就是高阶函数

7.简单了解 methods @click vue得处理

  • 在vue中其实都是在AST通过render函数 来结合数据生成虚拟DOM树得时候就处理了,我们打印下app实例可以看到它得一个虚拟Dom,app._vnode,它里面有一个data 是包含所有得属性,里面有一个On就是绑定函数得,以内函数是对象是引用类型,每次修改methods中,都会同步更新方法
  • 在这里插入图片描述
<button @click="aa">aaa</button>
<button @click="bb">bbb</button>
let app = new Vue({
            el: "#app",
            methods: {
                aa() {
                    console.log("aa")
                },
                bb() {
                    console.log("bb")
                }
            }
        })
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值