vue+bootstrap响应式布局_vue响应式原理学习

1349e48ed9d37d4952bd1217811a3ac8.gif点击上方蓝字  关注前端知识
  • vue响应式原理学习(1)

    • 1.什么是响应式

    • 2.实现响应式,我们需要做些什么

    • 3.如何侦测数据的变化

    • 3.2  Proxy实现

    • 4.收集依赖

    • 5 观察者 Watcher

  • 前言:现在前端面试Vue中都会问到响应式原理以及如何实现的,如果你还只是简单回答通过Object.defineProperty()来劫持属性可能已经不够了

vue响应式原理学习(1)

  • 一句话总结:通过Object.defineProperty去劫持data里的属性,将data全部属性替换成getter和setter,配合发布者和订阅者模式,每一个组件都有一个watcher实例,当我们对data属性赋值和改变,就会触发setter,setter会通知watcher,从而使它关联的组件进行重新渲染。

1.什么是响应式

数据发生变化后,会重新对页面渲染,这就是Vue响应式

2.实现响应式,我们需要做些什么

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

专业术语

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

3.如何侦测数据的变化

  • Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty及ES6的Proxy,进行数据劫持或数据代理。这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因
6ed9fee598ff7ce011bf82fa653b42e0.png
在这里插入图片描述
3.1 Object.defineProperty实现

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

  • 语法:Object.defineProperty(obj, prop, descriptor)
    • obj要定义属性的对象。
    • prop要定义或修改的属性的名称或 Symbol 。
    • descriptor要定义或修改的属性描述符
html>
<html lang="en">

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

<body>
    <script>let obj = { text: '111' };Object.defineProperty(obj, 'text', {// writable:true,//value值可被改变
            configurable: true,//属性可删除
            enumerable: true,//可枚举(遍历)get () {console.log("text被访问 get方法")
            },set(){console.log("text被赋值 set方法")
            }
        });console.log(obj.text)//触发get()console.log(obj.text='222')//触发set()script>
body>

html>
eee13b3a6222baa7810a32809423b2c0.png
在这里插入图片描述
  • 当属性text被访问时,触发了get方法,当属性被赋值时触发了set方法(但是访问到的是undefined的,因为get()中没有设置返回值)
  • Object.defineProperty中get()return 的值是什么,该属性的值就是什么
  • Object.defineProperty中定义的数据不会储存,我们需要一个第三者变量来动态定义数据
html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
head>
<body>
    <script>let obj = { text: '' };let _text =''Object.defineProperty(obj, 'text', {// writable:true,//value值可被改变
            configurable: true,//属性可删除
            enumerable: true,//可枚举(遍历)get () {console.log("text被访问 get方法")return _text
            },set(value){console.log("text被赋值 set方法",value)
                _text = value
            }
        });console.log(obj.text='222')console.log(obj.text)script>
body>

html>

b24d97b80904fa10d13346ec996692aa.png
在这里插入图片描述
  • 我们首先要先了解Object.defineProperty基本的用法
3.1.1 Observer实现一个对象对所有成员的代理
html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
head>
<body>
    <script>function render() {console.log('模拟视图渲染')
        }let data = {name: '浪里行舟',location: { x: 100, y: 100 }
        }
        observe(data)function observe(obj) { // 我们来用它使对象变成可观察的// 判断类型if (!obj || typeof obj !== 'object') {return
            }Object.keys(obj).forEach(key => {
                defineReactive(obj, key, obj[key])
            })function defineReactive(obj, key, value) {// 递归子属性
                observe(value)Object.defineProperty(obj, key, {enumerable: true, //可枚举(可以遍历)
                    configurable: true, //可配置(比如可以删除)get: function reactiveGetter() {console.log('get', value) // 监听return value
                    },set: function reactiveSetter(newVal) {
                        observe(newVal) //如果赋值是一个对象,也要递归子属性if (newVal !== value) {console.log('set', newVal) // 监听
                            render()
                            value = newVal
                        }
                    }
                })
            }
        }
        data.location = {x: 1000,y: 1000
        } //set {x: 1000,y: 1000} 模拟视图渲染
        data.name // get 浪里行舟script>
body>
html>
  • observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上 set和get方法,以此来达到实现侦测对象变化。值得注意的是, observe 会进行递归调用。

那我们如何侦测Vue中data 中的数据,其实也很简单:

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。「但是我们发现一个问题,上面的代码无法检测到对象属性的添加或删除(如data.location.a=1,增加一个a属性)。」

  • 这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢?
  1. 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
  2. 也可以给这个对象重新赋值,比如data.location = {...data.location,a:1} Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写

3.2  Proxy实现

  • Proxy 是 JavaScript 2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外Proxy支持代理数组的变化。
  • vue3中Api,ES6的新语法.proxy比Object.defineProperty()性能要好
html>
<html lang="en">

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

<body>
    <script>function render() {console.log('模拟视图的更新')
        }let obj = {name: '前端工匠',age: { age: 100 },arr: [1, 2, 3]
        }let handler = {/**
            * target: 真正的目标数据对象 {a:1,b:2,age:10}
            * key:要访问的属性名
            */get(target, key) {// 如果取的值是对象就再对这个对象进行数据劫持if (typeof target[key] == 'object' && target[key] !== null) {return new Proxy(target[key], handler)
                }return Reflect.get(target, key)
            },/**
         * target 原始的数据对象
         * key 要赋值的属性
         * value  = 后面的值
         */set(target, key, value) {//key为length时,表示遍历完了最后一个属性if (key === 'length') return true
                render()return Reflect.set(target, key, value)
            }
        }/**
         * new Proxy 用来创建一个代理对象
         * 第1个参数:要代理的数据对象
         * 第2个参数:handler 配置代理方法的一个对象
         *    get 属性的访问器
         *    set 属性的修改器
         *    一共有 13 个代理器
         * Proxy 返回一个把原始数据对象代理过的数据对象,我们称之为代理对象
         * 注意:必须操作代理对象才会走代理,原始数据对象还是原来的
         */let proxy = new Proxy(obj, handler)
        proxy.age.name = '浪里行舟' // 支持新增属性console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
        proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
        proxy.arr.length-- // 无效script>
body>

html>

4.收集依赖

4.1 为什么要收集依赖

我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。

let globalData = {
    text: '浪里行舟'
};
let test1 = new Vue({
    template:
        `
{{text}} 
`,data: globalData
});let test2 = new Vue({template:`
{{text}} 
`,data: globalData
});
如果我们执行下面这条语句:
globalData.text = '前端工匠';
此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖。才能知道哪些地方依赖我的数据,以及数据更新时派发更新。。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep和观察者 Watcher 。,然后阐述收集依赖的如何实现的。
4.2订阅者 Dep

「为什么引入 Dep:」

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。「Dep的简单实现:」*
class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}


以上代码主要做两件事情 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。所以当需要依赖收集的时候调用 addSub 方当需要派发更新的时候调用  notify 。- 调用也很简单:
 let dp = new Dep()
dp.addSub(() => {//依赖收集的时候
   console.log('emit here')
})
dp.notify()//派发更新的时候

5 观察者 Watcher

5.1 为什么引入Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释: 当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。 「依赖收集的目的是:」将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。f4e42c8061e56149c3dc07ab9cda8113.png
5.2 Watcher的简单实现
class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}


以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

「依赖的本质:」

所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话, 在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。

function observe (obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    observe(value)  // 递归子属性
    let dp = new Dep() //新增
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
     // 将 Watcher 添加到订阅
       if (Dep.target) {
         dp.addSub(Dep.target) // 新增
       }
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
     // 执行 watcher 的 update 方法
          dp.notify() //新增
        }
      }
    })
  }
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

「完整流程图:」411c3bc585079c66fc974fbeb26d67b5.png

在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。

「最后完整的响应式代码:」

大概结构
  //defineReactive是对Observer的抽离
  const defineReactive = function(obj, key) {
    // 以下代码省略
  }
  
  const Vue = function(options) {
    console.log("Vue",this)
    //打印1  Vue {
                  _data:{
                      text: "123"
                      get text: ƒ get()
                      set text: ƒ set(newVal)
                    },
                  mount: ƒ (),
                  render: ƒ ()
                }
    // 以下代码省略
  }
  
  const Watcher = function(vm, fn) {
    console.log("Watcher",this)
    //打印3 Watcher  this是下面的Dep中subs的对象
    // 以下代码省略
  }
  
  const Dep = function() {
    console.log("Dep",this)
    //打印2  Dep   { 
                    target: null,
                    subs: [
                      {        //是一个Watcher实例
                        subs: Array(1)
                        0: Watcher
                        vm: {    //是一个Vue实例
                            _data:{
                              text: "123",//该属性有了get和set方法
                              get text: ƒ get(),
                              set text: ƒ set(newVal)
                            },
                            mount: ƒ (),
                            render: ƒ ()
                          },

                        addDep: ƒ (dep),
                        update: ƒ (),
                        value: undefined
                      }
                    ],
                    depend: ƒ (),
                    addSub: ƒ (watcher),
                    notify: ƒ ()
                  }

    // 以下代码省略
  }
  
  const vue = new Vue({
    data() {
      return {
        text: 'hello world'
      };
    }
  })
  
  vue.mount(); 
  vue._data.text = '123';

详细代码
const Observer = function(data) {
    console.log(1)   //开始4 new Vue的时候就会执行
  // 循环修改为每个属性添加get set
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
    console.log(2)    //开始5 new Vue的时候就会执行
  // 局部变量dep,用于get set内部调用
  const dep = new Dep();
  // 获取当前值
  let val = obj[key];
  Object.defineProperty(obj, key, {
      
    // 设置当前描述属性为可被循环
    enumerable: true,
    // 设置当前描述属性可被修改
    configurable: true,
    get() {
        console.log(3)//开始10  开始19
      console.log('in get');
      // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
      dep.depend();
      return val;
    },
    set(newVal) {
        console.log(4)//开始15
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify();
    }
  });
}

const observe = function(data) {
    console.log(5)  //开始3 new Vue的时候就会执行
  return new Observer(data);
}

const Vue = function(options) {
    console.log(6)//开始1 new Vue的时候就会执行
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    console.log(7)//开始2   new Vue的时候就会执行
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    console.log(8)  //开始7  new Vue以后,执行vue.mount()
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
    console.log(9) //开始9 开始18  render函数执行后走到这里
    with(self) {
      _data.text;  //这里取data值的时候,就会走get方法
    }
  }
  // 监听this._data
  observe(this._data);  //new Vue的时候就会执行,这里执行完,就表示new Vue的过程执行完了
}

const Watcher = function(vm, fn) {
    console.log(10)  //开始8  执行vue.mount()以后会走到这里
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    console.log(11) //开始13  
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log(12)//开始17
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();   //开始9  fn是render函数,这里fn()就会赋值的时候执行
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

const Dep = function() {
    console.log(13)  //开始6  new Vue的时候就会执行到new Dep,然后执行到这里
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系

  this.depend = function() {
    console.log(14)  //开始11   开始20 走了get获取属性后,就要进行依赖收集 
    if (Dep.target) {
        console.log(15)//开始12  
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    console.log(16)//开始14
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    console.log(17) //开始16
    for (let i = 0; i 1) {
      self.subs[i].update();
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get

「解析:」

一开始new Vue ,会走到46行执行Vue构造函数,打印6

然后46行Vue的入参options实际上是127行的入参{data(){}},是一个包含了data函数的对象,所以options.data是一个data函数,打印7。将vue中的data函数返回的数据赋值给_data。

然后走到67行的observe,会继续往上走到41行定义它的地方。

然后43行 new Observer 的时候会走到第一行Observer(关键函数),打印1。我们发现Observer实际就是给data数据都添加上get和set方法,只不过不添加的方法defineReactive给抽离出去了。

然后走到第9行,执行defineReactive,打印2,然后15行给每个属性加上get和set方法。

然后走到12行,new Dep的时候,会走到95行执行Dep,打印13。Dep函数剩下的代码都只是定义函数,都不会执行,会跳出Dep函数。然后会到defineReactive函数第13行,defineReactive剩下的代码中的函数也不会执行,所以会回到Observer,再回到67行,即new Vue的过程走完了。

然后走到135行的vue.mount(),走到56行,打印8。

然后执行new Watcher走到70行,打印10,然后「Dep.target = this」,这一步将watch实例挂载到了Dep的target属性上,从而关联起来。

72行到88行只是定义,没有执行。89行this.value = fn()中:fn实际是传进来的「render」函数(看57行),然后后面又加了()就会立即执行。然后走到60行的render函数,打印9。「Watcher就执行完了」,然后,「关键的来了」:打印完9它会继续往下走,「读取_data.text」。那么,这一步就会触发get方法(这一步的目的就只是为了触发get,所以获取值就行了,并不需要做其他操作)。

然后走到21行的get,打印3。

然后走到25行,执行dep.depend(),再走到104行,打印14。

这时候判断Dep.target,由于第8步将watch挂载到了Dep.target,这时候为true,所以打印15。

然后走到110行,再跳到77行,打印11。

79行执行后会跳到114行,打印16,完成了依赖收集,然后会回到Watch,执行最后一行,Dep.target = null,避免陷入死循环,然后Watch执行完了,「vue.mount()也执行完了」。。

然后就是136行赋值操作了,这时候会走到28行的set,打印4。

继续向下走,到36行,dep.notify(),然后走到119行,打印17。

然后会走到122行,触发update,走到82行,打印12。

然后执行fn(),即render函数,走到60行,打印9。

然后走到63行,取data值,会走get,走到21,打印3。

然后25行,会跳到104行,打印14。Dep.target为null,15不会打印

参考链接:

Object.defineProperty()[1]

VUE响应式原理[2]

vue.js源码GitHub[3]

Vue-Object.defineProperty[4]

Vue.js 技术揭秘

Vue高级指南-01 Vue源码解析之手写Vue源码[5]

vue响应式详解(重学前端-vue篇1)[6]详解Vue响应式原理

参考资料

[1]

Object.defineProperty(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

[2]

VUE响应式原理: https://juejin.im/post/6872992692268990478

[3]

vue.js源码GitHub: https://github.com/vuejs/vue

[4]

Vue-Object.defineProperty: https://github.com/zhihuifanqiechaodan/Vue-Object.defineProperty

[5]

Vue高级指南-01 Vue源码解析之手写Vue源码: https://juejin.im/post/6844904047921594382

[6]

vue响应式详解(重学前端-vue篇1): https://juejin.im/post/6850418111985352711

e90651c9807cbf1ce77c1d5a1e6bcc0e.png扫码关注04c46f206f0e04a66f5e4782be47f040.pngWEB前端知识分享专业的分享 只为专业的你
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值