Vue3设计思想及响应式源码剖析

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

一、Vue3结构分析

1、Vue2与Vue3的对比

  • 对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)
  • 大量的API挂载在Vue对象的原型上,难以实现TreeShaking。
  • 架构层面对跨平台dom渲染开发支持不友好,vue3允许自定义渲染器,扩展能力强。
  • CompositionAPI。受ReactHook启发
  • 对虚拟DOM进行了重写、对模板的编译进行了优化操作...

2、Vue3设计思想

  • Vue3.0更注重模块上的拆分,在2.0中无法单独使用部分模块。需要引入完整的Vuejs(例如只想使用使用响应式部分,但是需要引入完整的Vuejs), Vue3中的模块之间耦合度低,模块可以独立使用。拆分模块
  • Vue2中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具Tree-shaking机制实现按需引入,减少用户打包后体积。重写API
  • Vue3允许自定义渲染器,扩展能力强。不会发生以前的事情,改写Vue源码改造渲染方式。扩展更方便

依然保留了Vue2的特点:

依旧是声明式框架,底层渲染逻辑不关心(命令式比较关注过程,可以控制怎么写最优?编写过程不同),如for和reduce

采用虚拟DOM

区分编译时和运行时

内部区分了编译时(模板?编程成js代码,一般在构建工具中使用)和运行时

简单来说,Vue3 框架更小,扩展更加方便

3、monorepo管理项目

Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。也就是说是一种将多个package放在一个repo中的代码管理模式。Vue3内部实现了一个模块的拆分, Vue3源码采用 Monorepo 方式进行管理,将模块拆分到package目录中。

  • 一个仓库可维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用,调用都非常方便
  • 每个包可以独立发布

早期使用yarn workspace + lerna来管理项目,后面是pnpm

pnpm介绍

快速,节省磁盘空间的包管理器,主要采用符号链接的方式管理模块

  1. 快速

  2. 高效利用磁盘空间

pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:

  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用hardlink(硬链接)
  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的hardlink,仅仅写入那一个新增的文件
  1. 支持Monorepo

pnpm 与 npm/yarn 一个很大的不同就是支持了 monorepo

  1. 安全性高

之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性

默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到node_modules的根目录下。

安装和初始化

  • 全局安装(node版本>16)
npm install pnpm -g


  • 初始化
pnpm init


配置workspace

根目录创建pnpm-workspace.yaml

packages:
  - 'packages/*'


将packages下所有的目录都作为包进行管理。这样我们的Monorepo就搭建好了。确实比 lerna + yarn workspace 更快捷

4、项目结构

packages

  • reactivity:响应式系统
  • runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • runtime-test:用于测试
  • server-renderer:用于服务器端渲染
  • compiler-core:与平台无关的编译器核心
  • compiler-dom: 针对浏览器的编译模块
  • compiler-ssr: 针对服务端渲染的编译模块
  • template-explorer:用于调试编译器输出的开发工具
  • shared:多个包之间共享的内容
  • vue:完整版本,包括运行时和编译器
                                    +---------------------+
                                    |                     |
                                    |  @vue/compiler-sfc  |
                                    |                     |
                                    +-----+--------+------+
                                          |        |
                                          v        v
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                      |                     |    |                      |    |                   |
                      +---------------------+    +----------------------+    +-------------------+


scripts

Vue3在开发环境使用esbuild打包,生产环境采用rollup打包

包的相互依赖

安装

把packages/shared安装到packages/reactivity

pnpm install @vue/shared@workspace --filter @vue/reactivity

使用

在reactivity/src/computed.ts中引入shared中相关方法

import { isFunction, NOOP } from '@vue/shared' // ts引入会报错

const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    ...
  } else {
    ...
  }
...


tips:@vue/shared引入会报错,需要在tsconfig.json中配置

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@vue/compat": ["packages/vue-compat/src"],
      "@vue/*": ["packages/*/src"],
      "vue": ["packages/vue/src"]
    }
  },
}


5、打包

所有包的入口均为src/index.ts这样可以实现统一打包.

• reactivity/package.json

{
  "name": "@vue/reactivity",
  "version": "3.2.45",
  "main": "index.js",
  "module":"dist/reactivity.esm-bundler.js",
  "unpkg": "dist/reactivity.global.js",
  "buildOptions": {
    "name": "VueReactivity",
    "formats": [
      "esm-bundler",
      "cjs",
      "global"
    ]
  }
}


• shared/package.json

{
    "name": "@vue/shared",
    "version": "3.2.45",
    "main": "index.js",
    "module": "dist/shared.esm-bundler.js",
    "buildOptions": {
        "formats": [
            "esm-bundler",
            "cjs"
        ]
    }
}


formats 为自定义的打包格式,有 esm-bundler 在构建工具中使用的格式、 esm-browser 在浏览器中使用的格式、 cjs 在node中使用的格式、 global 立即执行函数的格式

开发环境esbuild打包

开发时 执行脚本, 参数为要打包的模块

"scripts": {
    "dev": "node scripts/dev.js reactivity -f global"
}


// Using esbuild for faster dev builds.
// We are still using Rollup for production builds because it generates
// smaller files w/ better tree-shaking.

// @ts-check
const { build } = require('esbuild')
const nodePolyfills = require('@esbuild-plugins/node-modules-polyfill')
const { resolve, relative } = require('path')
const args = require('minimist')(process.argv.slice(2))

const target = args._[0] || 'vue'
const format = args.f || 'global'
const inlineDeps = args.i || args.inline
const pkg = require(resolve(__dirname, `../packages/${target}/package.json`))

// resolve output
const outputFormat = format.startsWith('global')
  ? 'iife'
  : format === 'cjs'
  ? 'cjs'
  : 'esm'

const postfix = format.endsWith('-runtime')
  ? `runtime.${format.replace(/-runtime$/, '')}`
  : format

const outfile = resolve(
  __dirname,
  `../packages/${target}/dist/${
    target === 'vue-compat' ? `vue` : target
  }.${postfix}.js`
)
const relativeOutfile = relative(process.cwd(), outfile)

// resolve externals
// TODO this logic is largely duplicated from rollup.config.js
let external = []
if (!inlineDeps) {
  // cjs & esm-bundler: external all deps
  if (format === 'cjs' || format.includes('esm-bundler')) {
    external = [
      ...external,
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {}),
      // for @vue/compiler-sfc / server-renderer
      'path',
      'url',
      'stream'
    ]
  }

  if (target === 'compiler-sfc') {
    const consolidateDeps = require.resolve('@vue/consolidate/package.json', {
      paths: [resolve(__dirname, `../packages/${target}/`)]
    })
    external = [
      ...external,
      ...Object.keys(require(consolidateDeps).devDependencies),
      'fs',
      'vm',
      'crypto',
      'react-dom/server',
      'teacup/lib/express',
      'arc-templates/dist/es5',
      'then-pug',
      'then-jade'
    ]
  }
}

build({
  entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
  outfile,
  bundle: true,
  external,
  sourcemap: true,
  format: outputFormat,
  globalName: pkg.buildOptions?.name,
  platform: format === 'cjs' ? 'node' : 'browser',
  plugins:
    format === 'cjs' || pkg.buildOptions?.enableNonBrowserBranches
      ? [nodePolyfills.default()]
      : undefined,
  define: {
    __COMMIT__: `"dev"`,
    __VERSION__: `"${pkg.version}"`,
    __DEV__: `true`,
    __TEST__: `false`,
    __BROWSER__: String(
      format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches
    ),
    __GLOBAL__: String(format === 'global'),
    __ESM_BUNDLER__: String(format.includes('esm-bundler')),
    __ESM_BROWSER__: String(format.includes('esm-browser')),
    __NODE_JS__: String(format === 'cjs'),
    __SSR__: String(format === 'cjs' || format.includes('esm-bundler')),
    __COMPAT__: String(target === 'vue-compat'),
    __FEATURE_SUSPENSE__: `true`,
    __FEATURE_OPTIONS_API__: `true`,
    __FEATURE_PROD_DEVTOOLS__: `false`
  },
  watch: {
    onRebuild(error) {
      if (!error) console.log(`rebuilt: ${relativeOutfile}`)
    }
  }
}).then(() => {
  console.log(`watching: ${relativeOutfile}`)
})



生产环境rollup打包

具体代码参考rollup.config.mjs

build.js

二、Vue3中Reactivity模块

1、vue3对比vue2的响应式变化

  • 在Vue2的时候使用defineProperty来进行数据的劫持, 需要对属性进行重写添加gettersetter性能差
  • 当新增属性和删除属性时无法监控变化。需要通过$set$delete实现
  • 数组不采用defineProperty来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理

Vue3中使用Proxy来实现响应式数据变化。从而解决了上述问题

2、CompositionAPI

  • 在Vue2中采用的是OptionsAPI, 用户提供的data,props,methods,computed,watch等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
  • Vue2中所有的属性都是通过this访问,this存在指向明确问题
  • Vue2中很多未使用方法或属性依旧会被打包,并且所有全局API都在Vue对象上公开。Composition API对 tree-shaking 更加友好,代码也更容易压缩。
  • 组件逻辑共享问题, Vue2 采用mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3采用CompositionAPI 提取公共逻辑非常方便

简单的组件仍然可以采用OptionsAPI进行编写,compositionAPI在复杂的逻辑中有着明显的优势~。 reactivity 模块中就包含了很多我们经常使用到的 API 例如:computed、reactive、ref、effect等

3、基本使用

const { effect, reactive } = VueReactivity
// console.log(effect, reactive);
const state = reactive({name: 'qpp', age:18, address: {city: '南京'}})
console.log(state.address);
effect(()=>{
    console.log(state.name)
})


4、reactive实现

import { mutableHandlers } from'./baseHandlers'; 
// 代理相关逻辑import{ isObject }from'./util';// 工具方法
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
function createReactiveObject(target, baseHandler){
    if(!isObject(target)){
        return target;
    }
    ...
    const observed =new Proxy(target, baseHandler);
    return observed
}


baseHandlers

import { isObject, hasOwn, hasChanged } from"@vue/shared";
import { reactive } from"./reactive";
const get = createGetter();
const set = createSetter();
function createGetter(){
    return function get(target, key, receiver){
        // 对获取的值进行放射
        const res = Reflect.get(target, key, receiver);
        console.log('属性获取',key)
        if(isObject(res)){// 如果获取的值是对象类型,则返回当前对象的代理对象
            return reactive(res);
        }
        return res;
    }
}
function createSetter(){
    return function set(target, key, value, receiver){
        const oldValue = target[key];
        const hadKey =hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if(!hadKey){
            console.log('属性新增',key,value)
        }else if(hasChanged(value, oldValue)){
            console.log('属性值被修改',key,value)
        }
        return result;
    }
}
export const mutableHandlers ={
    get,// 当获取属性时调用此方法
    set// 当修改属性时调用此方法
}


这里我只选了对最常用到的get和set方法的代码,还应该有 hasdeletePropertyownKeys 。这里为了快速掌握核心流程就先暂且跳过这些代码

5、effect实现

我们再来看effect的代码,默认effect会立即执行,当依赖的值发生变化时effect会重新执行

export let activeEffect = undefined;
// 依赖收集的原理是 借助js是单线程的特点, 默认调用effect的时候会去调用proxy的get,此时让属性记住
// 依赖的effect,同理也让effect记住对应的属性
// 靠的是数据结构 weakMap : {map:{key:new Set()}}
// 稍后数据变化的时候 找到对应的map 通过属性出发set中effect
function cleanEffect(effect) {
    // 需要清理effect中存入属性中的set中的effect 
    // 每次执行前都需要将effect只对应属性的set集合都清理掉
    // 属性中的set 依然存放effect
    let deps = effect.deps
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect)
    }
    effect.deps.length = 0;

}

// 创建effect时可以传递参数,computed也是基于effect来实现的,只是增加了一些参数条件而已
export function effect<T = any>(
    fn: () => T,
    options?: ReactiveEffectOptions    
){
    // 将用户传递的函数编程响应式的effect
    const _effect = new ReactiveEffect(fn,options.scheduler);
    // 更改runner中的this
    _effect.run()
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect; // 暴露effect的实例
    return runner// 用户可以手动调用runner重新执行
}
export class ReactiveEffect {
    public active = true;
    public parent = null;
    public deps = []; // effect中用了哪些属性,后续清理的时候要使用
    constructor(public fn,public scheduler?) { } // 你传递的fn我会帮你放到this上
    // effectScope 可以来实现让所有的effect停止
    run() {
        // 依赖收集  让熟悉和effect 产生关联
        if (!this.active) {
            return this.fn();
        } else {
            try {
                this.parent = activeEffect
                activeEffect = this;
                cleanEffect(this); // vue2 和 vue3中都是要清理的 
                return this.fn(); // 去proxy对象上取值, 取之的时候 我要让这个熟悉 和当前的effect函数关联起来,稍后数据变化了 ,可以重新执行effect函数
            } finally {
                // 取消当前正在运行的effect
                activeEffect = this.parent;
                this.parent = null;
            }
        }
    }
    stop() {
        if (this.active) {
            this.active = false;
            cleanEffect(this);
        }
    }
}


在effect方法调用时会对属性进行取值,此时可以进行依赖收集。

effect(()=>{
    console.log(state.name)
    // 执行用户传入的fn函数,会取到state.name,state.age... 会触发reactive中的getter
    app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city
    
})


6、依赖收集

核心代码

// 收集属性对应的effect
export function track(target, type, key){}// 触发属性对应effect执行
export function trigger(target, type, key){}

function createGetter(){
    return function get(target, key, receiver){
        const res = Reflect.get(target, key, receiver);
        // 取值时依赖收集
        track(target, TrackOpTypes.GET, key);
        if(isObject(res)){
            return reactive(res);
        }
        return res;
    }
}


function createSetter(){
    return function set(target, key, value, receiver){
        const oldValue = target[key];
        const hadKey =hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if(!hadKey){
            // 设置值时触发更新 - ADD
            trigger(target, TriggerOpTypes.ADD, key);
        }else if(hasChanged(value, oldValue)){
             // 设置值时触发更新 - SET
            trigger(target, TriggerOpTypes.SET, key, value, oldValue);
        }
        return result;
    }
}


track的实现

const targetMap = new WeakMap();
export function track(target: object, type: TrackOpTypes, key: unknown){
    if (shouldTrack && activeEffect) { // 上下文 shouldTrack = true
        let depsMap = targetMap.get(target);
        if(!depsMap){// 如果没有map,增加map
            targetMap.set(target,(depsMap =newMap()));
        }
        let dep = depsMap.get(key);// 取对应属性的依赖表
        if(!dep){// 如果没有则构建set
            depsMap.set(key,(dep =newSet()));
        }
    
        trackEffects(dep, eventInfo)
    }
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  //let shouldTrack = false
  //if (effectTrackDepth <= maxMarkerBits) {
   // if (!newTracked(dep)) {
     // dep.n |= trackOpBit // set newly tracked
     // shouldTrack = !wasTracked(dep)
    //}
  //} else {
    // Full cleanup mode.
  //  shouldTrack = !dep.has(activeEffect!)
  } 

  if (!dep.has(activeEffect!) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    //if (__DEV__ && activeEffect!.onTrack) {
    //  activeEffect!.onTrack({
    //    effect: activeEffect!,
    //    ...debuggerEventExtraInfo!
    //  })
   // }
  }
}


trigger实现

export function trigger(target, type, key){
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return;
    }
    const run=(effects)=>{
        if(effects){ effects.forEach(effect=>effect()); }
    }
    // 有key 就找到对应的key的依赖执行
    if(key !==void0){
        run(depsMap.get(key));
    }
    // 数组新增属性
    if(type == TriggerOpTypes.ADD){
        run(depsMap.get(isArray(target)?'length':'');
    }}


依赖关系

作者:京东物流 乔盼盼

来源:京东云开发者社区 自猿其说Tech 转载请注明来源文章来源地址https://www.toymoban.com/news/detail-746947.html

到了这里,关于Vue3设计思想及响应式源码剖析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 1.微信APP首页UI结构设计思想viewpager

    1.微信APP首页UI结构设计思想 交互界面 fragment渲染速度快  有缓存 attach dettach 绑定 解绑  dettach不会销毁掉fragment 只会销毁掉fragment里面的view 但是fragment仍旧存在,效率更高 生命周期更加可控  由activity管控 activity 由 ams管理  fragment 本质就是view  可以利用缓存解决 提升显示

    2024年02月07日
    浏览(34)
  • 扎实打牢数据结构算法根基,从此不怕算法面试系列之010 week02 01-01 最简单的排序算法-选择排序法的设计思想

    接下类,我们学习另外一类非常基础的算法,即排序算法。 排序算法是计算机科学领域研究的非常深入的一类算法,排序这个动作本身也是非常重要的, 很多时候面对无需的数据,首先需要做的就是对他们进行排序。 排序算法——目的:让数据有序。 排序算法——种类:种

    2023年04月21日
    浏览(57)
  • 【数据结构与算法分析】使用C语言实现队列的两种(带头结点与不带头结点)链式存储,并且给出一种循环队列的设计思想

      当我们编写程序时,经常需要处理各种数据结构。队列是一种常见的数据结构,它有着广泛的应用场景。队列的基本操作包括入队和出队,应用于模拟等待队列、消息队列、计算机缓存等场合。   在实际编程中,我们可以用不同的数据结构来实现队列。本文主要介绍了

    2024年02月08日
    浏览(102)
  • 设计模式-01.设计思想

    此系列文章非本人原创,是学习笔记。 下面讲一些常见的设计思想 这个原则非常重要,是一种非常有效的提高代码质量的手段,在平时的开发中特别经常被用到。 如何解读原则中的“接口”二字? “基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, n

    2024年02月07日
    浏览(69)
  • Spring核心设计思想

    目录 前言: Spring是什么 什么是IoC 传统开发思想 IoC开发思想 Spring IoC 什么是DI 小结:     官网中提出:Spring makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simplicity, and productivity has made it the world\\\'s most popular Java framework.     Spring 使编程 Java 对每个人来

    2023年04月17日
    浏览(42)
  • 算法设计思想——动态规划

    是一种常见的算法设计方法,用于解决一类重叠子问题的优化问题。他的基本思想是将问题分解成多个重叠的子问题,递归求解,并将子问题的求解缓存起来,避免重复计算,从而得到问题的解。 动态规划通常适用于以下两个条件的问题: 1.重叠子问题:原问题可以分解为若

    2024年02月03日
    浏览(45)
  • 【Spring】核心与设计思想

     哈喽,哈喽,大家好~ 我是你们的老朋友: 保护小周ღ   谈起Java 圈子里的框架,最年长最耀眼的莫过于 Spring 框架啦,如今已成为最流行、最广泛使用的Java开发框架之一。不知道大家有没有在使用 Spring 框架的时候思考过这些问题, 什么是框架?Spring 是什么?如何理解

    2024年02月08日
    浏览(43)
  • Spring 核心与设计思想

    ✏️作者:银河罐头 📋系列专栏:JavaEE 🌲 “种一棵树最好的时间是十年前,其次是现在” 通常所说的 Spring 指的是 Spring Framework(Spring 框架)。 Spring 是包含多种工具方法的 IoC 容器。 IoC(Inversion of Control): 控制反转 \\\"控制反转\\\"又是什么意思? 下面以一个程序来举例。 假如我

    2024年02月02日
    浏览(55)
  • Spring框架核心与设计思想

    我们一般所说的Spring指的是Spring Framework(Spring 框架),它是一个开源的框架,Spring支持广泛的应用场景,它可以让Java企业级的应用程序开发变得更简单,官方一点的回答:spring是J2EE应用程序框架,是轻量级的IoC和AOP的容器框架,主要是针对javaBean的生命周期进行管理的轻量级

    2023年04月15日
    浏览(44)
  • 从架构设计思想出发看Flutter

    Flutter 是一种流行的移动应用程序开发框架,它的设计特点之一是可以使用单一代码库构建 iOS 和 Android 应用程序。然而,对于功能比较多、模块比较复杂的应用程序,仅凭单一的代码库就可能导致代码的复杂性和维护难度的增加。在这种情况下,通过合适的应用程序架构设计

    2024年02月07日
    浏览(72)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包