Vue3 源码阅读(10):组件化 —— 实现原理

 我的开源库:

组件是对页面中内容的一种模块化封装,这篇博客讲解 Vue 中组件化的实现方式。

1,Vue 中组件的定义方式

在讲解组件化的实现原理前,首先说明在 Vue 中,一个组件的本质到底是什么。Vue 中有两种形式的组件,分别是有状态组件和无状态组件,有状态组件的本质是一个对象字面量,无状态组件的本质是一个函数。也就是说,在 Vue 中,组件的本质是一个对象字面量或者一个函数,这里以有状态组件进行讲解。

2,VNode 简介

VNode 的本质就是一个普通的对象字面量,只不过这个对象字面量能够很好的描述真实 DOM,通过 VNode 可以渲染出页面中真实的 DOM。

VNode 是通过组件的 render 函数创建出来的,我们平时在开发中,一般都是使用 template 字符串描述页面内容,这个模板字符串会被 Vue 的编译器编译成 render 函数,所以在 Vue 的运行时,用于描述组件渲染内容的是 render 函数。

下面展示一下 Vue3 中 VNode 的 TypeScript 的类型定义:

export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
  __v_isVNode: true
  [ReactiveFlags.SKIP]: true
  type: VNodeTypes
  props: (VNodeProps & ExtraProps) | null
  key: string | number | symbol | null
  ref: VNodeNormalizedRef | null
  scopeId: string | null
  slotScopeIds: string[] | null
  children: VNodeNormalizedChildren
  component: ComponentInternalInstance | null
  dirs: DirectiveBinding[] | null
  transition: TransitionHooks<HostElement> | null
  el: HostNode | null
  anchor: HostNode | null // fragment anchor
  target: HostElement | null // teleport target
  targetAnchor: HostNode | null // teleport target anchor
  staticCount: number
  suspense: SuspenseBoundary | null
  ssContent: VNode | null
  ssFallback: VNode | null
  shapeFlag: number
  patchFlag: number
  dynamicProps: string[] | null
  dynamicChildren: VNode[] | null
  appContext: AppContext | null
  memo?: any[]
  isCompatRoot?: true
  ce?: (instance: ComponentInternalInstance) => void
}

VNode 对象的属性还是很多的,这里不用看这么多,先关注一下 type 属性。

export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
  type: VNodeTypes
}

export type VNodeTypes =
  | string
  | VNode
  | Component
  | typeof Text
  | typeof Static
  | typeof Comment
  | typeof Fragment
  | typeof TeleportImpl
  | typeof SuspenseImpl

type 属性用于描述 VNode 的类型,VNode 的类型有很多种,这里我们看下 string 和 Component 类型,当 VNode 的 type 属性是字符串的时候,说明当前的 VNode 描述的是普通的元素,当 VNode 的 type 是 Component 的时候,说明当前的 VNode 描述的是一个组件。

假设我们的 Vue 中有一个 MyComponent 组件,我们在一个模板字符串中使用了这个组件,代码如下所示:

<template>
  <MyComponent></MyComponent>
</template>

上面的模板字符串会被编译成一个 render 函数,render 函数执行返回一个 VNode,这个 VNode 是一个组件类型的 VNode,表明需要渲染一个组件。

componentVNode = {
  type: {
    ...组件的定义对象...
  },
  ......
}

有了组件类型的 VNode,接下来看看这个组件 VNode 是如何渲染和更新的。

3,组件的挂载和更新

组件挂载和更新的逻辑都写在渲染器中,我们直接看源码。

const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  // 解构获取 n2 的 type、ref、shapeFlag
  const { type, ref, shapeFlag } = n2
  // 根据 n2 Vnode 的类型进行不同的处理
  switch (type) {
    ......
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 处理元素节点
        processElement()
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 处理组件节点
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (__DEV__) {
        // 如果以上条件都不满足,并且是在开发模式下的话,则打印出相关警告:违法的 vnode 类型
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }
}

在 patch 函数中,会根据 VNode 类型的不同使用不同的函数进行处理,如果当前的 VNode 表示的是组件的话,则会使用 processComponent 函数进行处理,processComponent 函数的内容如下所示:

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  if (n1 == null) {
    mountComponent(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    )
  } else {
    updateComponent(n1, n2, optimized)
  }
}

在这里,判断 oldVNode 是否存在,如果存在的话,则执行 updateComponent 函数进行组件的更新,如果不存在的话,则执行 mountComponent 函数进行组件的挂载,我们首先看组件的挂载。

3-1,组件的挂载

// 挂载组件节点
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 创建组件实例对象
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // resolve props and slots for setup context
  // 解析初始化一些数据
  if (!(__COMPAT__ && compatMountInstance)) {
    setupComponent(instance)
  }
  
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

在 mountComponent 函数中,首先创建组件的实例,每渲染一次组件,就会创建一个对应的实例,组件实例就是一个对象,这个对象维护着组件运行过程中的所有信息,例如:注册的生命周期函数、组件上次渲染的 VNode,组件状态等等。一个组件实例的内容如下所示:

const instance: ComponentInternalInstance = {
  uid: uid++,
  vnode,
  type,
  parent,
  appContext,
  root: null!, // to be immediately set
  next: null,
  subTree: null!, // will be set synchronously right after creation
  effect: null!,
  update: null!, // will be set synchronously right after creation
  scope: new EffectScope(true /* detached */),
  render: null,
  proxy: null,
  exposed: null,
  exposeProxy: null,
  withProxy: null,
  provides: parent ? parent.provides : Object.create(appContext.provides),
  accessCache: null!,
  renderCache: [],

  // local resolved assets
  components: null,
  directives: null,

  // resolved props and emits options
  propsOptions: normalizePropsOptions(type, appContext),
  emitsOptions: normalizeEmitsOptions(type, appContext),

  // emit
  emit: null!, // to be set immediately
  emitted: null,

  // props default value
  propsDefaults: EMPTY_OBJ,

  // inheritAttrs
  inheritAttrs: type.inheritAttrs,

  // state
  ctx: EMPTY_OBJ,
  data: EMPTY_OBJ,
  props: EMPTY_OBJ,
  attrs: EMPTY_OBJ,
  slots: EMPTY_OBJ,
  refs: EMPTY_OBJ,
  setupState: EMPTY_OBJ,
  setupContext: null,

  // suspense related
  suspense,
  suspenseId: suspense ? suspense.pendingId : 0,
  asyncDep: null,
  asyncResolved: false,

  // lifecycle hooks
  // not using enums here because it results in computed properties
  isMounted: false,
  isUnmounted: false,
  isDeactivated: false,
  bc: null,
  c: null,
  bm: null,
  m: null,
  bu: null,
  u: null,
  um: null,
  bum: null,
  da: null,
  a: null,
  rtg: null,
  rtc: null,
  ec: null,
  sp: null
}

上面的对象包含着很多的状态信息,是实现组件化一个很重要的内容。

创建完组件实例后,Vue 使用 setupComponent 函数进行一些数据的解析和初始化,下面调用的 setupRenderEffect 函数是重点。

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    // 使用组件实例的 isMounted 属性判断组件是否挂载
    // 如果为属性为 false,说明还未挂载,所以执行挂载逻辑
    // 如果属性为 true 的话,说明已经挂载,所以执行更新逻辑
    if (!instance.isMounted) {
      const { bm, m } = instance
      
      // beforeMount hook
      // 触发执行 beforeMount 生命周期函数
      if (bm) {
        invokeArrayFns(bm)
      }
      // 执行 render 函数,获取组件当前的 VNode
      const subTree = (instance.subTree = renderComponentRoot(instance))
      // 使用 patch 函数进行组件内容的渲染
      patch(
        null,
        subTree,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG
      )

      // mounted hook
      // 触发执行 mounted 生命周期函数
      if (m) {
        queuePostRenderEffect(m, parentSuspense)
      }
      // 将组件实例的 isMounted 属性设为 true,表明当前的组件已经完成了挂载操作
      instance.isMounted = true
    } else {
      let { bu, u } = instance

      // beforeUpdate hook
      // 触发执行 beforeUpdate 生命周期函数
      if (bu) {
        invokeArrayFns(bu)
      }
      // render
      // 执行 render 函数,获取组件最新的 VNode
      const nextTree = renderComponentRoot(instance)
      // 获取组件上次渲染的 VNode
      const prevTree = instance.subTree
      instance.subTree = nextTree
      // 使用 patch 函数进行组件的更新
      patch(
        prevTree,
        nextTree,
        // parent may have changed if it's in a teleport
        hostParentNode(prevTree.el!)!,
        // anchor may have changed if it's in a fragment
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG
      )
      // updated hook
      // 触发执行 updated 生命周期函数
      if (u) {
        queuePostRenderEffect(u, parentSuspense)
      }
    }
  }

  // 组件的更新借助了响应式系统中的 ReactiveEffect 类
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  const update: SchedulerJob = (instance.update = () => effect.run())

  update()
}

上段代码中,借助 ReactiveEffect 类实现组件的更新,关于这个类的作用和源码可以看我的这篇博客,这里就不过多赘述了。

这里实现功能的重点在 componentUpdateFn 函数中,在上面代码的最后,执行了 update 函数,这会进而触发执行上面 componentUpdateFn 函数的执行,componentUpdateFn 函数的内部会执行组件的 render 函数,render 函数会读取组件的响应式数据,这会触发依赖收集

componentUpdateFn 函数的解析看上面的注释即可。

3-2,组件的更新

当后续 render 函数依赖的响应式数据发生变化的时候,会再次触发执行 componentUpdateFn 函数进行组件的重新渲染,详细解释看上面源码的注释。

4,组件的 render 函数执行时,如何通过 this 访问到组件的响应式数据

结论:Vue 通过代理让 render 函数执行时能够通过 this 访问到组件实例中的响应式数据。

Vue 通过 renderComponentRoot 函数执行 render 函数,获取 VNode。

instance.subTree = renderComponentRoot(instance)
export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,
    renderCache,
    data,
    setupState,
    ctx,
    inheritAttrs
  } = instance

  result = normalizeVNode(
    render!.call(
      proxy,
      proxy!,
      renderCache,
      props,
      setupState,
      data,
      ctx
    )
  )

  return result;
}

render 函数执行的时候,函数中的 this 指向 instance.proxy,接下来看 instance.proxy 属性是如何创建出来的。

export function setupComponent(instance) {
  ......
  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance) {
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
}

export const PublicInstanceProxyHandlers = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance

    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      accessCache![key] = AccessTypes.SETUP
      return setupState[key]
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      accessCache![key] = AccessTypes.DATA
      return data[key]
    } else if (
      (normalizedProps = instance.propsOptions[0]) &&
      hasOwn(normalizedProps, key)
    ) {
      accessCache![key] = AccessTypes.PROPS
      return props![key]
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
      accessCache![key] = AccessTypes.CONTEXT
      return ctx[key]
    }
  }
};

可以发现,在 render 函数中通过 this 读取某些属性的时候,代理会判断 instance 的 setupState、data、props 中有没有同名的属性,如果有的话,就进行数据的读取和返回,并且这些被读取属性已经是响应式的了。
 

5,setup 函数的实现

setup 的官方文档点击这里

setup 函数只会在组件挂载的时候执行一次,setup 函数既可以返回一个对象,也可以返回一个函数,如果返回的是一个对象的话,这个对象中的数据可以像 data 和 props 一样使用,如果返回的是一个函数的话,这个函数会被当成组件的 render 函数。

源码如下所示:

// 挂载组件节点
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  ......
  // 解析初始化一些数据
  if (!(__COMPAT__ && compatMountInstance)) {
    setupComponent(instance)
  }

  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

export function setupComponent(instance) {
  ......
  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance) {
  const Component = instance.type as ComponentOptions
  ......
  const { setup } = Component
  if (setup) {
    setCurrentInstance(instance)
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    unsetCurrentInstance()
    handleSetupResult(instance, setupResult, isSSR)
  }
}

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult)
  }
}

在 setupStatefulComponent 函数中,获取用户编写的 setup 函数,执行它并获取 setup 函数的返回值 setupResult。

接下来使用 handleSetupResult 函数处理结果,如果 setupResult 是一个函数的话,则将它赋值给组件实例的 render 属性,如果 setupResult 是一个对象的话,则将它赋值给组件实例的 setupState 属性上,当我们想在 render 函数中访问 setup 函数返回的数据时,Vue 会将读取操作代理到 setupState 属性上,源码如下所示:

export const PublicInstanceProxyHandlers = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance

    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      accessCache![key] = AccessTypes.SETUP
      return setupState[key]
    } 
    ......
  }
};

6,组件生命周期的实现原理

组件的生命周期原理很简单,主要分为两部分,分别是生命周期的注册以及生命周期的执行。

首先说生命周期的注册,这里以 setup 函数中进行的生命周期注册为例。

function setupStatefulComponent(instance) {
  const Component = instance.type as ComponentOptions
  ......
  const { setup } = Component
  if (setup) {
    setCurrentInstance(instance)
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    unsetCurrentInstance()
    handleSetupResult(instance, setupResult, isSSR)
  }
}


export let currentInstance: ComponentInternalInstance | null = null

export const setCurrentInstance = (instance: ComponentInternalInstance) => {
  currentInstance = instance
}

export const unsetCurrentInstance = () => {
  currentInstance = null
}

在生命周期函数执行前,会执行一个 setCurrentInstance 函数,这个函数的作用是将当前的组件实例设置到全局中。

接下来看生命周期注册函数的内容,以 onMounted 函数为例:

export const onMounted = createHook(LifecycleHooks.MOUNTED)

export const createHook =
  <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
  (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
    // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
    (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
    injectHook(lifecycle, hook, target)

export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        // disable tracking inside all lifecycle hooks
        // since they can potentially be called inside effects.
        pauseTracking()
        // Set currentInstance during hook invocation.
        // This assumes the hook does not synchronously trigger other hooks, which
        // can only be false when the user does something really funky.
        setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        unsetCurrentInstance()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  }
}

当我们在 setup 函数中执行 onMounted 等生命周期注册函数时,Vue 会将我们想要注册的生命周期函数保存到组件实例中,组件实例用于保存生命周期函数的属性如下所示:

const instance: ComponentInternalInstance = {
  // lifecycle hooks
  bc: null,
  c: null,
  bm: null,
  m: null,
  bu: null,
  u: null,
  um: null,
  bum: null,
  da: null,
  a: null,
  rtg: null,
  rtc: null,
  ec: null,
  sp: null
}

知道了生命周期函数是如何注册的,接下来看看生命周期函数是如何触发的,生命周期函数触发的代码在 setupRenderEffect 函数中,代码如下所示:

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    // 使用组件实例的 isMounted 属性判断组件是否挂载
    // 如果为属性为 false,说明还未挂载,所以执行挂载逻辑
    // 如果属性为 true 的话,说明已经挂载,所以执行更新逻辑
    if (!instance.isMounted) {
      const { bm, m } = instance
      
      // beforeMount hook
      // 触发执行 beforeMount 生命周期函数
      if (bm) {
        invokeArrayFns(bm)
      }
      // 执行 render 函数,获取组件当前的 VNode
      const subTree = (instance.subTree = renderComponentRoot(instance))
      // 使用 patch 函数进行组件内容的渲染
      patch(
        null,
        subTree,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG
      )

      // mounted hook
      // 触发执行 mounted 生命周期函数
      if (m) {
        queuePostRenderEffect(m, parentSuspense)
      }
      // 将组件实例的 isMounted 属性设为 true,表明当前的组件已经完成了挂载操作
      instance.isMounted = true
    } else {
      let { bu, u } = instance

      // beforeUpdate hook
      // 触发执行 beforeUpdate 生命周期函数
      if (bu) {
        invokeArrayFns(bu)
      }
      // render
      // 执行 render 函数,获取组件最新的 VNode
      const nextTree = renderComponentRoot(instance)
      // 获取组件上次渲染的 VNode
      const prevTree = instance.subTree
      instance.subTree = nextTree
      // 使用 patch 函数进行组件的更新
      patch(
        prevTree,
        nextTree,
        // parent may have changed if it's in a teleport
        hostParentNode(prevTree.el!)!,
        // anchor may have changed if it's in a fragment
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG
      )
      // updated hook
      // 触发执行 updated 生命周期函数
      if (u) {
        queuePostRenderEffect(u, parentSuspense)
      }
    }
  }

  // 组件的更新借助了响应式系统中的 ReactiveEffect 类
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  const update: SchedulerJob = (instance.update = () => effect.run())

  update()
}

在 componentUpdateFn 函数中,进行了组件的初始挂载和更新,生命周期函数就是在这些操作的前后触发执行的,在上面的源码中,使用 invokeArrayFns 函数进行生命周期函数的触发执行,它的源码如下所示:

export const invokeArrayFns = (fns: Function[], arg?: any) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}

7,结语

这篇博客讲解了 Vue3 中是如何实现组件化的,下面的博客详细讲讲异步组件以及 Vue 提供的一些内置组件。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue组件实现原理基于Vue的核心概念——虚拟DOM(Virtual DOM)。组件是指将页面拆分为多个独立、可复用的组件,每个组件都有自己的模板、样式和行为。Vue通过提供组件系统来支持这种开发方式。 在Vue中,我们可以使用Vue.component()方法创建一个全局组件,或者使用components选项在一个父组件中注册子组件。 当组件被创建时,Vue会根据组件的模板生成一个虚拟DOM树。在数据变时,Vue会对比新旧虚拟DOM树的差异,并通过最小地修改真实DOM来更新页面。 组件的模板通常使用Vue的模板语法来描述,包括插值表达式、指令、事件绑定等。当组件被渲染时,模板中的表达式会被动态地计算和更新。 组件还可以定义自己的样式和行为。样式可以使用普通的CSS或CSS预处理器编写,并通过scoped属性限定作用域,确保样式只应用于当前组件组件之间可以通过props属性和自定义事件进行通信。props属性用于父组件向子组件传递数据,子组件可以通过props选项声明接收的数据类型和默认值。自定义事件则用于子组件向父组件发送消息,子组件可以通过$emit方法触发事件,并传递数据给父组件。 通过组件开发,我们可以将页面拆分为多个独立的组件,使代码更加模块、可复用和易于维护。同时,通过虚拟DOM的高效更新机制,Vue能够在数据变时高效地更新页面,提升性能和用户体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值