Vue2.0 的响应式原理 私

这篇具有很好参考价值的文章主要介绍了Vue2.0 的响应式原理 私。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。这时候会为对象的每个属性创建一个Dep实例  (依赖)。Dep实例可以订阅和通知相关的Watcher实例。,  这一步叫  数据劫持  或者 依赖收集

在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。 这一步叫 派发更新

同时,为了解决对象属性添加和删除的问题,Vue提供了全局的Vue.set和Vue.delete方法,以及实例的vm.$set和vm.$delete方法。

解释:

  1. 在创建 Observer 实例的同时还会创建 Dep 实例,用于保存依赖项。因此每个数据都有 Observer 的实例,每个 Observer 实例中又都有一个 Dep 的实例。
  2. 当 Vue 解析到当解析到模板字符串 {{ }} 时中数据时,就会去创建 Watcher 实例,在 constructor 时会调用自身的 get 方法,该方法不仅将当前的 Watcher 实例赋值给了 Dep.target(表示此时处于依赖收集阶段),还让这个新实例去读取一下 {{ }} 中的数据,一旦读取,就会触发这个数据的 getter 方法。因为此时正在进行收集依赖,Dep.target 一定是为 true 的,于是顺利地把当前的这个 Watcher 实例记录到了 dep 中的 subs 数组里。再然后将 Dep.target 的值重新赋值为 null,表示退出依赖收集阶段

总结:

在Vue2中,响应式原理是通过使用Object.defineProperty方法来实现的。当一个对象被传入Vue的observe函数中时,Vue会为对象的每个属性创建一个Dep实例。Dep实例可以订阅和通知相关的Watcher实例。

当一个属性被访问时,Watcher实例会将自己添加到该属性的Dep实例的订阅列表中。当该属性的值发生变化时,Dep实例会遍历订阅列表,通知所有相关的Watcher实例进行更新。

然而,Vue无法检测到对象属性的添加和删除。为了解决这个问题,Vue提供了全局的Vue.set方法或实例的vm.$set方法来添加属性,使用Vue.delete方法或实例的vm.$delete方法来删除属性。对于数组的变动,Vue无法检测到利用索引设置数组,但可以使用Vue.set方法或实例的vm.$set方法。此外,Vue也无法检测直接修改数组长度,但可以使用splice方法来实现。

总结起来,Vue2的响应式原理通过使用Object.defineProperty方法来实现属性的劫持和侦听,同时使用Dep和Watcher实例来建立属性和依赖之间的关系,并进行更新通知。同时,为了解决对象属性添加和删除的问题,Vue提供了全局的Vue.set和Vue.delete方法,以及实例的vm.$set和vm.$delete方法。

原理:

通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。
在get的时候会调用dep.depend; 

如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()

1 在initState()方法里会对组件的props, methods, data,computed, watch等进行编译初始化=>

2 initData()会先获取组件内部的data数据,然后判断data里的数据和props,或者和methods里的名称重复,则抛出错误提示,然后就会去监听data,执行observe方法=>

3   Observer()  =>   defineReactive, dependArray =>  defineProperty()   =>   Observer()    递归

Vue2.0 的响应式原理 私,Vue3.0X,vue.js,javascript,前端

Vue2.0 的响应式原理 私,Vue3.0X,vue.js,javascript,前端

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。

在get的时候会调用dep.depend;如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()

源码解析:

不同版本的vue的源码实现可能会有些不同,我这里的版本是2.6.14

Vue2.0 的响应式原理 私,Vue3.0X,vue.js,javascript,前端

首先我们要知道定义响应式是在哪个时间段实现的,从源码中我们可以看到,是在执行beforeCreate生命周期函数之后,Created之前。也就是说,这也就是我们在beforeCreate无法拿到Data中的数据的原因。

Vue2.0 的响应式原理 私,Vue3.0X,vue.js,javascript,前端

首先我们要知道定义响应式是在哪个时间段实现的,从源码中我们可以看到,是在执行beforeCreate生命周期函数之后,Created之前。也就是说,这也就是我们在beforeCreate无法拿到Data中的数据的原因。

第一步: 在initState方法里会对组件的props, methods, data,computed, watch等进行编译初始化

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

第二步: initData()会先获取组件内部的data数据,然后判断data里的数据和props,或者和methods里的名称重复,则抛出错误提示,然后就会去监听data,执行observe方法

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) //将data上的属性代理到vm实例上。
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

第三步:在这里observe()中,会先判断data中的数据是否是对象,然后判断data中是否已经有了ob(也就是Observer实例)最后判断是否满足监听的条件。才会创建一个新的Observer对象

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


第四步:

每一个observer实例都有自己的一个Dep, 在new Oberver后,会判断传入的value也就是vm.data是不是数组。

如果是数组,会采用函数劫持的方法重写数组的方法,先判断数组支不支持原型链,支持就将当前数组的原型指向已经重写了Array里的7种方法的arrayMethod,当数组里的方法被调用时,Dep会notify通知视图更新,然后执行ObserveArray方法,如果数组里的数据是对象,则继续回调observe();
如果是对象,则调用this.walk(),在walk()中,会遍历data的属性执行defineReactive()定义响应式
 

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

第五层:

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。
在get的时候会调用dep.depend;如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()
以下是Dep的代码,我们可以将Dep看作一个观察者。
 

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

第六层 解释   第五层:

depend方法就是将当前dep的实例添加到对应的Watcher中,
notify方法就是通知所有收集的Wacher进行更新,subs[i].update()

/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = [] //存储所有订阅的Watcher
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

第七层.Watcher.js

当解析到模板字符串 {{ }} 时,会默认去 new Watcher 实例。

/**
 * 每一次的 new Watcher 都是独立的,因此构造器接收的三个参数,虽然名字一样但确实不同的数据,就像是 vm.$watch() 接收的参数一样,
 * @param {*} target 需要监视的对象,当做修改时,他就是
 * @param {*} expression 这个对象中的某个属性,它是一个表达式 比如 obj.a.b.c
 * @param {*} callback 回调函数,需要执行的操作 
 */

import Dep from "./Dep";

// 这个 uid 用于对每一个的 Watcher 实例添加唯一的 id
var uid = 0

// 在这里哪一步算是调用了 get 方法???????,解析到模板的时候

export default class Watcher {
    constructor(target, expression, callback) {
        console.log('我是 Watcher 构造器');
        this.id = uid++;

        // 模板字符串中的整个表达式
        this.target = target;

        // 通过拆分表达式(对象中的对象...),获得需要 Watch 的那个数据。比如传入的是 a.b.c.d 我们需要监视属性 d,就需要拆分
        this.getter = parsePath(expression)  // 有两种方法供使用 parsePath 会返回一个函数;如果用 reduce 方法,那么 getter 就会是一个具体的值,此时一定要修改下边的 get 方法!!!

        this.callback = callback

        // 调用该方法,进入依赖收集阶段
        this.value = this.get()
    }
    // 当更新 dep 中的依赖项时,会调用每一个 Watcher 实例身上的 update 方法
    update() {
        console.log('我是Watcher实例身上的update方法');
        this.run()
    }

    // 进入依赖收集阶段,让全局的 Dep.target 设置为 Watcher 本身
    get(){
        // Webpack 在打包的时候 Dep 是全局唯一的,不管多少个JS 文件在用 dep 的时候,都是这一个文件
        // 因此执行到这里
        console.log(this);  // Watcher 实例
        Dep.target = this;   
        // debugger;
        const obj = this.target;

        var value;

        // 防止找不到,用try catch一下,只要能找,就一直找
        try {
            value = this.getter(obj)    // 获取需要监视的那个值。这里因为constructor 的时候 this.get() 返回的是一个函数
        } finally {
            Dep.target = null   // 清空全局 target 的指向,同时也表示退出依赖收集阶段
        }
        return value        
    }

    // 其实可以直接 getAndInvoke,但是 Vue 源码时这样写的
    run(){
        this.getAndInvoke(this.callback)
    }

    //
    getAndInvoke(callback){
        // 获取到修改后的新值   旧值是 this.value
        const value = this.get()
        if(value !== this.value || typeof value == 'object'){
            const oldValue = this.value;
            this.value = value;
            callback.call(this.target, value, oldValue)
        }
    }
}





// 拆分表达式:
// 方法一:将 str 用 . 分割成数组 segments,然后循环数组,一层一层去读取数据,最后拿到的 obj 就是 str 中想要读的数据
// 假设 let o = {a:{b:{c:{d:55}}}},我想要取得 d 的值,经过拆分后的 segments 数组的值为 ['a', 'b', 'c', 'd']
// 第一次循环后 obj = {b:{c:{d:55}}}, 第二次 obj = {c:{d:55}}, 第三次 obj = {d:55}, 第四次 obj = 55
function parsePath(str) {
    let segments = str.split(".");
    return function (obj) {
        for (let key of segments) {
            if (!obj) return;   // 当没有传入 obj 时,直接 return
            obj = obj[key];
        }
        return obj;
    };
}
// 方法二 用 reduce 方法实现
// function parsePathReduce(str) {
//     let segments = str.split(".");
//     let result = segments.reduce((total, item) => {
//         total = total[item]
//         return total
//     }, str)
//     return result
// }

前置知识:


首先要了解三个最重要的对象:
Observer 对象:将 Vue 中的数据对象在初始化过程中转换为 Observer 对象。
Watcher 对象:将模板和 Observer 对象结合在一起生成 Watcher 实例,Watcher 是订阅者中的订阅者。
Dep对象:Watcher 对象和 Observer 对象之间纽带,每一个 Observe r都有一个 Dep 实例,用来存储订阅者 Watcher。
 

过程:

  1. 在生命周期的 initState 方法中将 data,prop,method,computed,watch等所有数据全部进行数据劫持,将所有数据变为 Observer 实例,并且每个数据身上还有 Dep 实例。
  2. 然后在 initRender 方法中也就是模板编译过程,遇到的指令和数据绑定都会生成 Watcher 实例,并且把这个实例存入对应数据的 Dep 实例中的 subs 数组里。这样每一个数据的 Dep 实例里就都存放了依赖关系。
  3. 当数据变化时,数据的 setter 方法被调用,触发 dep.notify 方法,就会通知 Dep 实例依赖列表,执行 update 方法通知 Watcher,Watcher 会执行 run 方法去更新视图。
  4. 更新视图的过程,我猜是 Vue 接下来要进行 diff 算法,对比新旧模板,然后重新渲染页面。
     

缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。

  • Vue 是无法检测到对象属性的添加和删除,但是可以使用全局 Vue.set 方法(或 vm.$set 实例方法)。
  • Vue 无法检测利用索引设置数组,但是可以使用全局 Vue.set方法(或 vm.$set 实例方法)。
  • 无法检测直接修改数组长度,但是可以使用 splice。

vue2.0重写数组

		// 数组方法重写
        let oldArrayPrototy = Array.prototype
        // 使用Object.create 将数组原型上的方法放到newArrayPrototy.prototype上
        let newArrayPrototy = Object.create(arrayFn)
        // 需要重写的数组方法列表
        let method = [
            'push',
            'pop',
            'shift',
            'unshift',
            'reverse',
            'sort',
            'splice'
        ]
        method.forEach((item) => {
        	//newArrayPrototy[item] 就是 arr.某一个方法
            newArrayPrototy[item] = function (...args) {
                // 关键部分
                let result = oldArrayPrototy[item].call(this, ...args)
                // 对传进来的数据做一些处理
                let insterted
                switch (item) {
                    case 'push':
                    case 'unshift':
                        insterted = args
                    case 'splice':
                        insterted = args.slice(2)
                    default:
                        break
                }
                return result
            }
        })

Vue2.0响应式原理源码解析_玛已的博客-CSDN博客

Vue2的响应式原理_vue2响应式原理_高等数学真简单的博客-CSDN博客文章来源地址https://www.toymoban.com/news/detail-681132.html

到了这里,关于Vue2.0 的响应式原理 私的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Web前端 ---- 【Vue3】Proxy响应式原理

    目录 前言 安装Vue3项目 安装 Proxy 语法格式 从本文开始进入vue3的学习。本文介绍vue3中的响应式原理,相较于vue2中通过object.defineProperty(vue2中的响应式)来实现响应式,vue3中换成了Proxy来进行实现。 相较于vue2通过vue-cli脚手架来创建项目,这里更推荐使用create-vue来创建vue3的

    2024年01月16日
    浏览(52)
  • 【前端vue升级】vue2+js+elementUI升级为vue3+ts+elementUI plus

    gogo code 是一个基于 AST (源代码的抽象语法结构树状表现形式)的 JavaScript/Typescript/HTML 代码转换工具,可以用它来构建一个代码转换程序来帮助自动化完成如框架升级、代码重构、多平台转换等工作。 当前 GoGoCode 支持解析和操作如下类型的代码: ○JavaScript(JSX) ○Typescript

    2024年02月12日
    浏览(40)
  • 前端2023最全面试题(javaScript、typeScript、vue2、vue3、html、css、uniapp、webpack、vite、react)

    答案:JavaScript中的闭包是一种函数,它有权访问其词法环境的变量和其它函数。这意味着,即使其包含它的函数已经执行完毕,其词法环境仍然存在,因此可以访问其作用域内的变量。 答案:回调函数是在某个特定事件之后执行的函数。在JavaScript中,通常使用回调函数来处

    2024年02月06日
    浏览(44)
  • Vue2和vue3中双向数据绑定的原理,ES6的Proxy对象代理和JavaScript的Object.defineProperty,使用详细

    简介: Object.defineProperty大家都知道,是vue2中双向数据绑定的原理,它 是 JavaScript 中一个强大且常用的方法,用于定义对象属性,允许我们精确地控制属性的行为,包括读取、写入和删除等操作; 而Proxy是vue3中双向数据绑定的原理,是ES6中一种用于创建代理对象的特殊对象,

    2024年02月15日
    浏览(38)
  • 持续不断更新中... 自己整理的一些前端知识点以及前端面试题,包括vue2,vue3,js,ts,css,微信小程序等

    答: 在普通的前端项目工程中,在script标签中增加setup即可使用api 使用setup()钩子函数 答: 不要在计算属性中进行异步请求或者更改DOM 不要直接修改computed的值 区别: 计算属性值基于其响应式依赖被缓存,意思就是只要他之前的依赖不发生变化,那么调用他只会返回之前缓

    2024年02月11日
    浏览(39)
  • Vue2.0 的响应式原理 私

    使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。这时候会为对象的每个属性创建一个Dep实例  (依赖)。Dep实例可以订阅和通知相关的Watcher实例。,  这一步叫  数据劫持  或者 依赖收集 在数据发生更新后调用 set 时会通知发布者 notify

    2024年02月11日
    浏览(26)
  • vue2响应式原理----发布订阅模式

    很多人感觉vue2的响应式其实用到了观察者+发布订阅。我们先来看一下简单的发布订阅的代码: 从上面中发现一个重要的点,发布者和订阅者是根据key值来区分的,然后通过消息中心来中转的,他们家是是实现不知道对方是谁。 而观察者模式中观察者是一开始就知道自己观察

    2024年04月14日
    浏览(28)
  • 202 vue2的响应式原理 通俗易懂!

    Object.defineProperty + 依赖追踪 。 在Vue实例化过程中,会 递归 地将 每个数据对象 的 属性 转换为 getter/setter ,并维护一个 依赖收集器(Dep) 。 每个属性 都有一个关联的 watcher ,当 数据发生 变化时 , watcher 会 被通知 并 更新视图 。 Vue 2.x 实现响应式数据: vue实例化 时,会

    2024年02月06日
    浏览(26)
  • 【前端面经】Vue3和Vue2的区别

    Vue是一种非常流行的JavaScript框架,因其易用性和灵活性在开发人员中备受欢迎。Vue2是Vue框架的上一个重要版本,于2016年发布。但是,Vue3是最新版本的Vue框架,于2020年正式发布并带来了一些重大变化。本文将探讨Vue3和Vue2之间的主要区别。 Vue3的一个显着优势是其更小的代码

    2024年02月02日
    浏览(73)
  • vue3响应式原理

    Vue 3 中的响应式原理是通过使用 ES6 的 Proxy 对象来实现的。在 Vue 3 中,每个组件都有一个响应式代理对象,当组件中的数据发生变化时,代理对象会立即响应并更新视图。 具体来说,当一个组件被创建时,Vue 会为组件的 data 对象创建一个响应式代理对象。这个代理对象可以

    2024年02月15日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包