redux 源码分析

背景

在之前的文章redux从入门到实践当中对redux的使用进行了说明,这次就来看下它的源码,从而进一步的熟悉它。

构建

相关git地址

git clone https://github.com/reduxjs/redux.git
复制代码

构建文档是CONTRBUTING.md

package.json

"main": "lib/redux.js",
// ...
"scripts": {
    "clean": "rimraf lib dist es coverage",
    "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "lint": "eslint src test",
    "pretest": "npm run build",
    "test": "jest",
    "test:watch": "npm test -- --watch",
    "test:cov": "npm test -- --coverage",
    "build": "rollup -c",
    "prepare": "npm run clean && npm run format:check && npm run lint && npm test",
    "examples:lint": "eslint examples",
    "examples:test": "cross-env CI=true babel-node examples/testAll.js"
  }
复制代码

package.json当中可以看到redux的入口文件是lib/redux.js,这个文件是通过打包出来的。那我们看下打包配置文件rollup.config

{
    input: 'src/index.js',
    output: { file: 'lib/redux.js', format: 'cjs', indent: false },
    external: [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
    ],
    plugins: [babel()]
  },
  // ...省略
复制代码

可以看到入口文件应该是src/index.js

我们来看下src/index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

/*
 * This is a dummy function to check if the function name has been altered by minification.
 * If the function has been minified and NODE_ENV !== 'production', warn the user.
 */
// 是否压缩代码,如果运行环境在非生成环境但是代码被压缩了,警告用户
function isCrushed() {}

// 判断环境是否是生成环境,如果是生成环境使用此代码就给出警告提示
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

复制代码

src/index.js主要是将方法暴露出来,给使用者使用

  • createStore 用于创建store
  • combineReducers 用于组合成rootReducers,因为在外部初始化store时,只能传入一个reducers
  • bindActionCreators 组装了dispatch方法
  • applyMiddleware 合并多个中间件
  • compose 将中间件(middleware)和增强器(enhancer)合并传入到createStore

combineReducers

src/combineReducers.js

export default function combineReducers(reducers) {
  // 遍历出reducers的对象名称
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍历reducers名称
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      // 如果reducer对应的值是 undefined 输出警告日志
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    // 当这个reducer是函数 则加入到finalReducers对象中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 读取出过滤后的reducers
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // 开发环境将unexpectedKeyCache设置为空对象
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // 检查各个reducers是否考虑过defualt的情况,不能返回undefined
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // combineRducers返回的是一个方法,dispatch最后执行的方法
  return function combination(state = {}, action) {
    // 如果reducer检查出有问题就会抛出异常
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // 开发者环境下
    if (process.env.NODE_ENV !== 'production') {
      // 对过滤后的reducers和初始化的state进行检查
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      // 如果有问题就会输出
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    // 遍历过滤后的reducers
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      // 根据key取出对应reducer
      const reducer = finalReducers[key]
      // 根据key将state对应的值取出
      const previousStateForKey = state[key]
      // 执行我们reducer的方法,nextStateForKey就是根据actionType返回的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 检查nextStateForKey是否是undefined
      if (typeof nextStateForKey === 'undefined') {
        // 如果undefined就报错
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 将合并后的state赋值到nextState当中
      nextState[key] = nextStateForKey
      // 如果state的值改变过 则hasChanged置为true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果改变过 则返回新的state不然则是原有的state({})
    // 如果state不传值就是 空的对象{}
    return hasChanged ? nextState : state
  }
}

复制代码

combineReducers方法会先对传入的reducers进行校验,reducer的类型只能是function,最后返回的是个方法,这个方法很关键,因为在disptach时,最后执行的就是这个方法。这个方法有2个参数stateaction,方法内会根据传入的action返回state,最后会比较新旧的state,如果不相等,则会返回新的state,如果相等会返回新的state。

那么如果我们直接对store的state进行操作而不是通过dispatch会发生呢,比如说我们这样

const state = store.getState();
state.name = 'baifann';
复制代码

我们看一下combineReducers中的getUnexpectedStateShapeWarningMessage这个方法,它会检查store中初始化的state的key有没有在各个子reducer当中,如果没有就会报错。

/**
 * @inputState 初始化的state
 * @reducers 已经过过滤的reducers 
 * @action 随着combinRecuers传入的action
 * @unexpectedKeyCache 开发者环境是一个空的对象,生成环境是undefined
 */
/**
 * 检查合法的reducers是否存在
 * 
 * 
 */
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // 将过滤的reducers的名取出
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    // 如果这个action的type是预制的ActionTypes.INIT
    // argumentName就是preloadedState argument passed to createStore
    // 不然是previous state received by the reducer
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
  // 如果过滤后的reducer长度为0
  // 则返回字符串告知没有一个合法的reducer(reducer必须是function类型)
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }
  // 判断输入的state是否是obj对象
  // 如果不是 则返回字符串告知inputState不合法
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  // 传入的state进行遍历
  // 如果state的对象名不包含在reducer中 并且不包含在unexpectedKeyCache对象中
  // unexpectedKeyCache在开发者环境是一个空的对象  因此只要state的对象名不包含在reducer中,这个key就会
  // 保存到 unexpectedKeys 当中
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  // 将inputState的key全部设置为true
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  // 如果这个action的type是定义的定义中的ActionTypes.REPLACE 就返回不执行
  if (action && action.type === ActionTypes.REPLACE) return
  // 如果unexpectedKeys中有值,则发出警告
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}
复制代码

compose

compose会返回一个方法,这个方法可以将传入的方法依次执行

export default function compose(...funcs) {
  // 如果函数方法为0 则
  if (funcs.length === 0) {
    // 会将参数直接返回
    return arg => arg
  }

  // 如果只传入一个方法则会返回这个方法
  if (funcs.length === 1) {
    return funcs[0]
  }
  // a为上一次回调函数返回的值 b为当前值
  // 效果就是不断执行数组中的方法最后返回时一个函数
  // 这个方法可以将所有数组中的方法执行
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

复制代码

createStore

我们接下来看下createStore.js这个文件,它只暴露出了createStore的方法,在createStore中,初始化了一些参数,同时返回了一个store,store中包括了dispatchsubscribegetStatereplaceReducer,[$$observable]: observable

import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'


export default function createStore(reducer, preloadedState, enhancer) {
  // 如果初始化的state是一个方法并且enhancer也是方法就会报错
  // 如果enhancer是方法如果第4个参数是方法就会报错
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  // 如果初始化的state是方法,enhancer的参数为undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer赋值初始话的stae
    enhancer = preloadedState
    // preloadedState赋值为undefined
    preloadedState = undefined
    // 这里是一个兼容2个参数的处理,当参数仅为2个 第二个参数为enhcaner时的处理
  }

  // 如果enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 如果enhancer不是方法会报错
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 如果reducer不是方法 则报错
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }


  // rootReducer赋值到currentReducer当中 实际是一个函数
  let currentReducer = reducer
  // 当前store中的state 默认是初始化的state
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // 浅拷贝一个数组 虽然是浅拷贝 但是currentListener不会被nextListener改变
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
      // 省略代码...
  }

  function subscribe(listener) {
      // 省略代码...
  }

  function dispatch(action) {
      // 省略代码...
  }

  function replaceReducer(nextReducer) {
      // 省略代码...
  }

  function observable() {
      // 省略代码...
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  // 执行dispatch 来初始化store中的state
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

复制代码

看完之后,我们可能在这个地方有一点疑惑,就是这里

  // 如果enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 如果enhancer不是方法会报错
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }
复制代码

这个返回的是什么呢,我们知道applyMiddleware返回的其实就是enhancer,那我们结合在一起看一下

applyMiddleware
import compose from './compose'

 /**
  * 创建一个store的增强器,使用中间件来包装dispath方法,这对于各种任务来说都很方便
  * 比如以简洁的方式进行异步操作,或记录每个操作有效负载
  * 
  * 查看`redux-thunk`包,这是一个中间件的例子
  * 
  * 因为中间件可能是异步的,所以应该是对个enhancer传参
  * 
  * 每一个中间件都要提供dispatch和getstate两个方法作参数
  * 
  */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 创建一个store ...args为reducer, preloadedState
    const store = createStore(...args)
    // 默认定义disptach方法,是一个抛出的报错
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    // 中间件的的参数
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 将所有的中间件遍历,将参数传入到中间件函数中,返回一个中间件函数的数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // dispatch做了包装,会在dispatch的同时同时将中间件的方法也返回
    dispatch = compose(...chain)(store.dispatch)
    // 返回store中的属性以及新的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

复制代码

如果直接返回了enhancer那么返回的其实也是store,但是这个store中的dispatch被包装过,当dispatch被执行时,会将所有中间件也依次执行。

接下来分析一下createStore中的方法

  • getState
  • subscribe
  • dispatch
  • replaceReducer
  • observable
getState

很简单,就是返回currentState

/**
   * Reads the state tree managed by the store.
   *
   * @returns {any} The current state tree of your application.
   */
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }
    // 返回state
    return currentState
  }
复制代码
subscribe

这是将一个回调加入到监听数组当中,同时,它会返回一个注销监听的方法。

  function subscribe(listener) {
    // listener必须是一个方法
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true
  
    ensureCanMutateNextListeners()
    // 把listenr加入到nextListeners的数组当中
    nextListeners.push(listener)

    // 解除观察
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false
      // 这里做了个拷贝 做的所有操作不影响currentListener
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 在nextListener将它去除
      nextListeners.splice(index, 1)
    }
  }
复制代码
dispatch

dispatch首先会检查参数,随后会执行currentReducer(currentState, action),而这个方法实际就是combineReducers

  function dispatch(action) {
    // 如果dispatch的参数不是action
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    // action必须得有type属性,如果没有会报错
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 执行reducer 遍历过滤后的reducer,随后依次赋值到state当中
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 获取当前的监听器
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 依次执行监听器回调
      listener()
    }

    // dispatch默认返回action
    return action
  }
复制代码
replaceReducer
  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  /**
   * 替换reducer
   * 
   * 动态替换原有的reducer
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 将reducer赋值
    currentReducer = nextReducer
    // 发送一个dispatch 随后重置store
    dispatch({ type: ActionTypes.REPLACE })
  }
复制代码
observable

这里不谈太多observable

这里有个使用例子

const state$ = store[Symbol.observable]();
const subscription = state$.subscribe({
  next: function(x) {
     console.log(x);
   }
 });
 subscription.unsubscribe();
复制代码
 /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param {Object} observer Any object that can be used as an observer.
       * The observer object should have a `next` method.
       * @returns {subscription} An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further
       * emission of values from the observable.
       */
      /**
       * @param {Object} 任何对象都可以当做observer
       * observer应该有一个`next`方法
       * @returns {subscription} 一个对象,它有`unsubscribe`方法能够
       * 用来从store中unsubscribe observable
       */
      subscribe(observer) {
        // observer必须是一个非空的object
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }
复制代码

bindActionCreators

在讲这个方法前,先看下文档对它的使用说明

Example

TodoActionCreators.js

我们在文件中创建了2个普通的action。

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
​
export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}
复制代码

SomeComponent.js

import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)
// {
//   addTodo: Function,
//   removeTodo: Function
// }class TodoListContainer extends Component {
  constructor(props) {
    super(props)
​
    const { dispatch } = props
​
    // Here's a good use case for bindActionCreators:
    // You want a child component to be completely unaware of Redux.
    // We create bound versions of these functions now so we can
    // pass them down to our child later.this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    console.log(this.boundActionCreators)
    // {
    //   addTodo: Function,
    //   removeTodo: Function
    // }
  }
​
  componentDidMount() {
    // Injected by react-redux:
    let { dispatch } = this.props
​
    // Note: this won't work:
    // TodoActionCreators.addTodo('Use Redux')// You're just calling a function that creates an action.
    // You must dispatch the action, too!// This will work:
    let action = TodoActionCreators.addTodo('Use Redux')
    dispatch(action)
  }
​
  render() {
    // Injected by react-redux:
    let { todos } = this.props
​
    return <TodoList todos={todos} {...this.boundActionCreators} />
​
    // An alternative to bindActionCreators is to pass
    // just the dispatch function down, but then your child component
    // needs to import action creators and know about them.
​
    // return <TodoList todos={todos} dispatch={dispatch} />
  }
}
​
export default connect(state => ({ todos: state.todos }))(TodoListContainer)
复制代码

bindActionCreators.js

我们接下来来看它的源码

/**
 * 
 * 在看`bindActionCreator`方法之前可以先看`bindActionCreators`方法
 * 
 * @param {Function} actionCreator 实际就是 action
 * 
 */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    // 返回的是dispath
    return dispatch(actionCreator.apply(this, arguments))
  }
}

// actionCreators是一个包含众多actions的对象
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    // actionCreators是函数就代表他是单一的action方法
    return bindActionCreator(actionCreators, dispatch)
  }

  // actionCreator如果不是object 或者它是空的则报错
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }
  // 将action的keys遍历出来
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    // 每个action的key
    const key = keys[i]
    // 将action取出 这是一个方法
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      // bindActionCreator返回的是dispatch的返回值
      // 实际是action 所以boundActionCreators是一个dispatch function的对象
      // 同时如果key相同会被覆盖
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

复制代码

使用bindActionCreators实际可以创建一个充满dispatch方法的对象。然后可以将这个对象传递子组件来使用。

总结

看完源码后我们大致了解到为什么reducer必须是functionstore中的state为什么会创建和reducer相应的对象名的state,为什么只能通过dispatch来对store进行操作。另外redux的一个核心不可变性,redux本身并不能保证。所以我们在自己写的reducer当中必须要保证不能改变store原有的对象,必须得重新创建。

转载于:https://juejin.im/post/5c21d523e51d453742727b86

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值