Vue3源码【二】—— watch侦听&computed计算属性原理及简单实现

1、watch监听器

1.1、使用watch

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用,然后下面就是使用watch的一个说明。 小声逼逼:我还是习惯用监听,后面的监听也就是侦听。

/**
 * @param source 监听对象
 *                监听单个            a
 *                监听多个            [a,b]
 *                监听reactive单个值   ()=>{}
 * @param cb 回调函数 (newVal, oldVal)得到变换前后的值
 * @param options 配置项
 *                immediate 是否立即执行
 *                deep 是否深度监听
 *                once 是否只执行一次
 *				  flush 回调执行时机
 */
watch(source,(newVal, oldVal)=>{},{})

1.2、创建watch

直接在源码当中找到watch,路径是packages/runtime-core/src/apiWatch.ts,他本身就是一个函数,之后回去执行doWatch,同时我们可以看一下options配置的类型WatchOptions,这个我们先放在着,等下看一下配置项是如何生效的。

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>,
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

//
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
  once?: boolean
}

1.3、执行监听

  • 首先对once配置项是否只执行一次进行判断,然后提前先对deep深层监听判断
  • 监听源判断,单值监听、多值监听、回调监听。同时需要去判断ref和reactive。
  • 根据前两步去判断是单层还是深层监听,执行traverse
  • 创建一个job,在job当中去更新新旧值
  • 判断flush,用来确定值变换与dom更新的时机(先后顺序)
  • 判断immediate,是否立即执行一次(执行一次job)
  • 最后通过effect.run()开始依赖收集整体调度
  • doWatch返回了unwatch,这也就是const A =watch(a,()=>{}); A();再调用一个A就可以去除监听的原因
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {
    immediate,
    deep,
    flush,
    once,
    onTrack,
    onTrigger,
  }: WatchOptions = EMPTY_OBJ,
  // EMPTY_OBJ是一个Object.freeze({})冻结的空对象,也就是说当没有传options过来时,这个配置都会从这个{}解构得到
): WatchStopHandle {
  // 当指定了once只执行一次,会执行一次cb(callback)然后unwatch结束监听
  if (cb && once) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      unwatch()
    }
  }

  const instance = currentInstance
  const reactiveGetter = (source: object) =>
    deep === true
      ? source // 遍历将发生在下面的包装getter中
      : // 对于deep:false,仅遍历根级属性
        traverse(source, deep === false ? 1 : undefined)

  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  if (isRef(source)) {
    // ref对象的get直接访问value属性
    getter = () => source.value
    // 判断是否是浅层ref
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // 对于reactive对象来说,是通过deep去控制是否需要深层监听的
    getter = () => reactiveGetter(source)
    forceTrigger = true
  } else if (isArray(source)) {
    // 数组
    isMultiSource = true
    // 看是否有深层监听
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return reactiveGetter(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          // 不能监听
        }
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup],
        )
      }
    }
  } else {
    getter = NOOP
  }

  // 有回调并且是单层监听
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }

  // 深层监听
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  let cleanup: (() => void) | undefined
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
      cleanup = effect.onStop = undefined
    }
  }

  // 在SSR中,不需要设置实际效果,它应该是noop
  // 除非它很急切或同步刷新
  let ssrCleanup: (() => void)[] | undefined
  if (__SSR__ && isInSSRComponentSetup) {
    // 我们也不会调用 invalide 回调(没有设置+runner)
    onCleanup = NOOP
    if (!cb) {
      getter()
    } else if (immediate) {
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        getter(),
        isMultiSource ? [] : undefined,
        onCleanup,
      ])
    }
    if (flush === 'sync') {
      const ctx = useSSRContext()!
      ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
    } else {
      return NOOP
    }
  }

  // isMultiSource 用来标记是否是多数据监听
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  // 开始调度
  const job: SchedulerJob = () => {
    // 需要保证依赖收集是开启的
    if (!effect.active || !effect.dirty) {
      return
    }
    if (cb) {
      const newValue = effect.run()
      // 有真则真
      // 深层监听 || 对象监听 || (多数据监听 遍历通过Object.is(value, oldValue)去比较值是否改变)|| (是数组类型 && ?)
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // 再次运行cb之前的清理
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // 第一次更改时将undefined作为旧值传递,到这里oldVal才会有值
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
              ? []
              : oldValue,
          onCleanup,
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }

  // 将job标记为观察程序回调,以便调度程序知道,它被允许自触发 先设置为false
  job.allowRecurse = !!cb // !!undefined false

  let scheduler: EffectScheduler
  /**
   * flush: 'pre' | 'post' | 'sync'
   * pre  在侦听器的回调函数运行之前立即运行更新函数
   * post ------------------之后-------------
   * sync 同步
   * */
  if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    job.pre = true
    // 把当前示例的id作为job任务id
    if (instance) job.id = instance.uid
    // 开始调度
    scheduler = () => queueJob(job)
  }

  // 和响应式那块是一样的,收集依赖
  const effect = new ReactiveEffect(getter, NOOP, scheduler)

  const scope = getCurrentScope()
  const unwatch = () => {
    // 停止依赖收集,并且把这个effect剔除出去
    effect.stop()
    if (scope) {
      remove(scope.effects, effect)
    }
  }

  // initial run
  if (cb) {
    // immediate 是否先调度一次
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense,
    )
  } else {
    effect.run()
  }

  if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
  return unwatch
}

1.4、traverse对象值收集

在上面有调用traverse(baseGetter()),把getter传给了这个函数,简单看一下这个函数,其实就是把所有getter能拿到的值全部给加到seen(set集合当中)之后要是有watch的变更也直接从这里面去掉即可

export function traverse(
  value: unknown,
  depth = Infinity,
  seen?: Set<unknown>,
) {
  if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }

  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  depth--

  // 看是不是ref还包了ref
  // 后面判断递归都是同理,把所有值都给加到seen当中
  if (isRef(value)) {
    traverse(value.value, depth, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], depth, seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, depth, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key], depth, seen)
    }
    for (const key of Object.getOwnPropertySymbols(value)) {
      if (Object.prototype.propertyIsEnumerable.call(value, key)) {
        traverse(value[key as any], depth, seen)
      }
    }
  }
  return value
}

1.5、watchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。官网说明:前往 https://cn.vuejs.org/api/reactivity-core.html#watcheffect

// use,当在watchEffect当中使用了的变量,就会自动追踪哪个属性,当使用了objEffect对象,他里面所有的属性都会被监听到
const objEffect = reactive({a: 1, b: {c: 1, d: {e: 2}}});
watchEffect(() => {
  console.log(' =====', objEffect.a);
});

// 源码
export function watchPostEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
) {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
  )
}

// 这个去执行doWatch时,也就是数据源是一个函数,它执行的逻辑就是这一块
getter = () => {
  if (cleanup) {
    // 通过这个将依赖收集起来
    cleanup()
  }
  return callWithAsyncErrorHandling(
    source,
    instance,
    ErrorCodes.WATCH_CALLBACK,
    [onCleanup],
  )
}

1.6、扩展:watchPostEffect & watchSyncEffect

本质上就是指定了flush,flush默认值是pre,在侦听器的回调函数运行之前立即运行更新函数,也就是watchEffect,而这两个的意义还是用来在语义上对前后、同步调用的一个区分

  • watchPostEffect : 把flush指定为post,也就是回调函数运行之后运行更新函数

  • watchSyncEffect : 同步执行

export function watchPostEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
) {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
  )
}

export function watchSyncEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
) {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
  )
}

2、computed 计算属性

2.1、使用computed

计算属性是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。

使用computed有以下两种方式,一个是传函数直接返回,一个可以通过传递对象给定get/set函数进去控制。

const A = computed(() => {
  return `A:${a.age}`;
});

const B = computed({
  get: () => {
    return a.age;
  },
  set: (value) => {
    a.age = value;
  }
});

2.2、创建computed

先看一下computed是怎么创建的,源码位置:packages/reactivity/src/computed.ts。在这里通过getterOrOptions接收computed传递的参数,也就是可以拿到上面函数和对象两种方式传递的值,直接分别去取对应的get、set方法,通过ComputedRefImpl创建一个实现实例。

export function computed<T>(
  // getterOrOptions传递值  ComputedGetter就是一个回调函数,WritableComputedOptions是一个对象包了get/set方法
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 判断传递过来是那种方式
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    // 将回调给到getter,并且不设置setter
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 传递的是对象形式,直接去对象里面拿get/set方法
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

2.2、ComputedRefImpl实例

  • 直接从构造开始看起,主要关注get/set两个方法。在构造创建了一个ReactiveEffect,也就是响应式的实现方式。并且指定了依赖触发
  • set值在计算属性当中不关系,主要是使用别的值改变后,怎么获取计算之后的值,也就是这的get
  • 在get当中通过了_cacheable是否缓存先执行一遍依赖触发和依赖收集的过程,而后则通过脏值判断是否需要使用缓存当中的值还是用新值
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false
  public _cacheable: boolean

  _warnRecursive?: boolean
  // 从构造开始,先不关心isReadonly和isSSR,就看get/set
  constructor(
    private getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean,
  ) {
    // 创建了ReactiveEffect,也就是前面说到的reactive响应式,并且指定了triggerRefValue(依赖触发)
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      () =>
        triggerRefValue(
          this,
          this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect // 4 === 2
            ? DirtyLevels.MaybeDirty_ComputedSideEffect // 2
            : DirtyLevels.MaybeDirty, // 3
        ),
    )
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR // isSSR === false 添加缓存
    this[ReactiveFlags.IS_READONLY] = isReadonly // isReadonly === false
  }

  get value() {
    // 计算出的ref可能会被其他代理封装,例如readonly() toRaw 转换成原始对象
    const self = toRaw(this)
    /**
     * self._cacheable 变量是否可缓存
     * self.effect.dirty 表示该变量是否被修改过
     * */
    if (
      (!self._cacheable || self.effect.dirty) &&
      hasChanged(self._value, (self._value = self.effect.run()!))
    ) {
      // 值变换之后会触发依赖更新DOM
      triggerRefValue(self, DirtyLevels.Dirty)
    }
    // 触发之后重新收集
    trackRefValue(self)
    // 这里通过 DirtyLevels.MaybeDirty_ComputedSideEffect 脏标记级别用来控制是否需要重新执行依赖触发去更新DOM等操作
    if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
      triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }

  get _dirty() {
    return this.effect.dirty
  }

  set _dirty(v) {
    this.effect.dirty = v
  }
}

2.3、简单实现computed计算属性

  • 首先还是要前面实现的MyReactive响应式的依赖收集依赖触发等方法(
    MyReactive响应式简单示例案例,单击前往
    ),这里就不往下面贴了,但是对应依赖收集、触发(effect、trigger)需要进行细微调整,
    • 依赖收集就是在收集的时候添加了一个options,挂在effect上
    • 依赖触发本质上就是在遍历deps的时候去看一下那些值有scheduler调度。这个调度是我们创建computed去给他添加的。
  • 创建computed,这里简单实现就只接收一个函数入参,之后进行依赖收集,在这里就将scheduler调度给挂载到effect副作用函数上。
  • 当trigger响应式触发之后,会去尝试执行一个scheduler调度。执行了调度之后再去更新DOM时会触发effect,在computed当中get值,在这里判断了dirty,也就是新旧值是否相等(是否去更新cacheValue缓存值)。
const effect = (fn: Function, options: Options) => {
  const _effect = () => {
    activeEffect = _effect;
    return fn();
  };

  _effect.options = options;
  _effect();

  return _effect;
};

const trigger = (target: object, key: any) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const deps = depsMap.get(key);
  if (!deps) return;

  deps.forEach((effect: { (): void; (): void; options: any; }) => {
    if (effect?.options?.scheduler) {
      effect?.options.scheduler?.();
    } else {
      effect();
    }
  });
};

const myComputed = (getter: Function) => {
  let _value = effect(getter, {
    scheduler: () => {
      _dirty = true;
    }
  });
  let _dirty = true;
  let catchValue: any;

  class MyComputerRefImpl {
    get value() {
      if (_dirty) {
        catchValue = _value();
        _dirty = false;
      }
      return catchValue;
    }
  }

  return new MyComputerRefImpl();
};
<think>嗯,用户想了解Vue.jscomputed属性实现原理,特别是源码层面的分析。我之前已经整理过相关引用,现在需要结合这些资料来构造一个详细的回答。首先,我应该从Vue 2和Vue 3的不同实现入手,因为引用里提到了两者的源码解析和设计原理。 首先,用户的问题是关于computed实现原理,需要解释其响应式机制。根据引用[1],Vue 2中计算属性侦听属性的区别在于计算属性是基于依赖的缓存,而引用[2]提到了Vue3computed的依赖追踪和副作用触发。同时,引用[3]的书籍可能提供了更系统的框架设计解析。 接下来,我需要理清Vue的响应式系统如何工作。Vue 2使用Object.defineProperty,而Vue 3改用Proxy,这会影响computed实现方式。计算属性本质是一个getter函数,依赖其他响应式数据,当依赖变化时才会重新计算。 然后,缓存机制是关键点。计算属性只有在相关依赖发生改变时才会重新计算,否则直接返回缓存值。这涉及到依赖收集和派发更新的过程,可能需要结合Watcher和Dep的概念来解释。在Vue 2中,每个计算属性对应一个Watcher,并且这个Watcher是惰性求值的,只有在被访问时才会计算。 另外,Vue 3中的computed通过effect和ReactiveEffect来实现,引用[2]中的代码示例展示了如何利用副作用函数来响应依赖变化。这里需要说明computed内部如何创建effect,以及如何通过调度器(scheduler)来控制重新计算的时机。 还要考虑源码结构,比如在Vue 2中,计算属性在初始化阶段被处理,定义在src/core/instance/state.js中的initComputed函数。而Vue 3computed API位于packages/reactivity/src/computed.ts,使用ComputedRefImpl类来管理依赖和值。 用户可能还想了解惰性求值和缓存的具体实现细节,比如脏检查机制(dirty标志位)。当依赖变化时,标记为dirty,下次访问时才会重新计算。这可以提升性能,避免不必要的计算。 此外,需要区分computedwatch的不同。计算属性是基于依赖的派生值,而watch是观察特定数据变化执行回调。从源码角度看,computedWatcher和user Watcher可能有不同的配置选项,比如lazy和dirty的处理。 最后,总结实现原理时,要分点说明响应式依赖、缓存机制、惰性求值,并结合Vue 2和Vue3的不同实现方式。可能还需要提到源码中的关键函数和类,如Vue2的defineComputedWatcher,Vue3ComputedRefImpl、track和trigger等。 确保回答结构清晰,逐步深入,并引用提供的资料来支持分析,比如引用[1][2][3]中的内容。同时,生成的相关问题要围绕实现细节、优化、与watch的区别以及应用场景展开,帮助用户进一步学习。</think>### Vue.js computed 属性实现原理分析 Vue 的 **计算属性computed)** 核心在于**依赖追踪**和**惰性缓存**,其实现机制可分为以下关键环节: --- #### 一、响应式依赖绑定(以 Vue 3 为例) 1. **初始化阶段** ```javascript const sumRes = computed(() => obj.foo + obj.bar) ``` - 通过 `computed` 函数创建 `ComputedRefImpl` 对象[^2]。 - 内部通过 `ReactiveEffect` 包裹计算函数,建立依赖关系: ```typescript // 源码片段(reactivity/src/computed.ts) this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true // 标记为"脏数据",触发重新计算 trigger(this, "value" /* TriggerOpTypes.SET */) } }) ``` 2. **依赖收集** - 当首次访问 `sumRes.value` 时,触发计算函数的执行。 - 在计算过程中,访问的响应式变量(如 `obj.foo`)会通过 `track` 函数将当前 `ComputedRefImpl` 对象注册为依赖。 --- #### 、缓存与惰性计算 1. **缓存机制** - 计算属性内部维护 `_value` 和 `_dirty` 标志位。 - 若依赖未变化(`_dirty` 为 `false`),直接返回缓存值 `_value`。 - 依赖变化时,标记 `_dirty` 为 `true`,但**不立即重新计算**,而是等待下次访问时触发。 2. **性能优化** ```javascript // 示例:多次访问同一个计算属性 console.log(sumRes.value) // 首次计算,标记_dirty=false console.log(sumRes.value) // 直接读取缓存 ``` - **避免重复计算**:依赖未变化时直接返回缓存值。 - **按需更新**:依赖变化后延迟到实际访问时再重新计算[^1]。 --- #### 三、与侦听属性watch)的本质区别 | **特性** | **计算属性computed)** | **侦听属性watch)** | |----------------|------------------------------|-------------------------------| | **触发时机** | 依赖变化后**延迟计算** | 依赖变化后**立即执行回调** | | **返回值** | 必须返回一个值 | 无返回值,用于执行副作用操作 | | **设计目标** | 派生数据(缓存优化) | 响应数据变化(主动监听) | --- #### 四、Vue 2.x 实现差异 - **基于 Watcher 的惰性求值** Vue 2 中每个计算属性对应一个 `Watcher`,配置 `lazy: true` 实现惰性求值: ```javascript // 源码片段(src/core/instance/state.js) function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers[key] if (watcher.dirty) { watcher.evaluate() // 重新计算 } if (Dep.target) { watcher.depend() // 依赖收集 } return watcher.value } } ``` - **缓存通过 `dirty` 标志控制**,依赖更新时通过响应式系统的 `notify` 方法触发 `dirty` 标记更新。 --- ### 总结 计算属性的核心设计是**将函数式编程与响应式系统结合**,通过依赖追踪实现精准更新,利用缓存机制避免冗余计算Vue 3 通过 `ReactiveEffect` 和调度器优化了性能,而 Vue 2 则依赖 `Watcher` 和 `Dep` 的经典实现[^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Modify_QmQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值