React快速入门(四)ReactHooks


ReactHooks

  • Hook是React 16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
  • class组件相对于函数式组件的优势:
    • class组件可以定义自己的state,用来保存组件自己内部的状态
      • 函数式组件不可以,因为函数每次调用都会产生新的临时变量
    • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑
      • 在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
      • 函数式组件在之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一网络请求
    • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等
      • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次
  • class组件存在的问题
    • 复杂组件难以理解,逻辑冗余,代码复杂度高
    • class难以理解,需要关注this的指向问题
    • 组件复用比较难,需要通过高阶组件来实现复用

Hook的出现

  • 可以让我们在不编写class的情况下使用state以及其他的React特性;我们可以由此延伸出非常多的用法,来解决class组件存在的问题
  • Hook的使用场景:
    • Hook的出现基本可以代替我们之前所有使用class组件的地方
    • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它
    • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
  • Hook是:完全可选的、100%向后兼容的、当前可用
  • 可以参考:「React 进阶」 React 全部 Hooks 使用大全 (包含 React v18 版本 ) - 掘金 (juejin.cn)

useState

  • useState会帮助我们定义一个state变量,useState是一种新方法,它与class里面的this.state提供的功能完全相同。
    • 一般来说,在函数退出后变量就会”消失”,而state 中的变量会被React保留。
    • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。
    • useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。
// 直接给定初始值
const [状态名, set函数] = useState(初始值)
// 函数返回值的形式
const [value, setValue] = useState(() => 初始值)
  • 例如:
//类式组件写法:
import React, { PureComponent } from 'react'

export class CounterClass extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      counter: 0
    }
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  decrement() {
    this.setState({
      counter: this.state.counter - 1
    })
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.decrement()}>-1</button>
      </div>
    )
  }
}

export default CounterClass
//函数式组件hook写法:

//只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
//只能在React的函数组件中调用Hook。不要在其他JavaScript函数中调用 。
import { memo, useState } from "react";

function CounterHook(props) {
// useState是一个hook,参数:初始化值,不设置则为undefined;返回值:数组,包含两个元素,[当前值的状态,设置状态值的函数]
  const [counter, setCounter] = useState(0)

  return (
    <div>
      <h2>当前计数: {counter}</h2>
      <button onClick={e => setCounter(counter+1)}>+1</button>
      <button onClick={e => setCounter(counter-1)}>-1</button>
    </div>
  )
}

export default memo(CounterHook)
  • 注意:当函数式组件被重新执行时,不会重复调用 useState() 给数据赋初值,而是会复用上次的 state 值。
// 以函数的形式为状态赋初始值
import React, { useState } from 'react'

const App: React.FC = () => {
  const [date, setDate] = useState(() => {
    const dt = new Date()
    return { year: dt.getFullYear(), month: dt.getMonth() + 1, day: dt.getDate() }
  })

  return (
    <div>
      <h1>今日信息:</h1>
      <p>年份:{date.year}年</p>
      <p>月份:{date.month}月</p>
      <p>日期:{date.day}日</p>
    </div>
  )
}

export default App
  • 注意:以函数的形式为状态赋初始值时,只有组件首次被渲染才会执行 fn 函数;当组件被更新时,会以更新前的值作为状态的初始值,赋初始值的函数不会执行。
  • 注意事项
    1. 如果要更新对象类型的值,并触发组件的重新渲染,则必须使用展开运算符或**Object.assign()**生成一个新对象,用新对象覆盖旧对象,才能正常触发组件的重新渲染。
    2. 当连续多次以相同的操作更新状态值时,React 内部会对传递过来的新值进行比较,如果值相同,则会屏蔽后续的更新行为,从而防止组件频繁渲染的问题。这虽然提高了性能,但也带来了一个使用误区。为了解决这个问题,我们可以使用函数的方式给状态赋新值。当函数执行时才通过函数的形参,拿到当前的状态值,并基于它返回新的状态值。
    3. 在函数组件中,我们可以通过 useState 来模拟 forceUpdate 的强制刷新操作。因为只要 useState 的状态发生了变化,就会触发函数组件的重新渲染,从而达到强制刷新的目的。

useRef

  • useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
  • 用途:获取DOM元素或子组件的实例对象,存储渲染周期之间共享的数据,这个对象在整个生命周期中可以保存不变
import React, { memo, useRef } from 'react'

const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()
  
  function showTitleDom() {
    console.log(titleRef.current)
    inputRef.current.focus()
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello World</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的dom</button>
    </div>
  )
})

export default App
  • 注意事项:

    1. 组件rerender时useRef不会被重复初始化
    2. ref.current变化时不会造成组件的rerender
    3. ref.current 值的变化不会造成组件的 rerender,而且 React 也不会跟踪 ref.current 的变化,因此 ref.current 不可以作为其它 hooks(useMemo、useCallback、useEffect 等) 的依赖项。
    4. ref 的作用是获取实例,但由于函数组件不存在实例,因此无法通过 ref 获取函数组件的实例引用React.forwardRef 会创建一个 React 组件,这个组件能够将其接收到的 ref 属性转发到自己的组件树。
    // 被包装的函数式组件,第一个参数是 props,第二个参数是转发过来的 ref
    const Child = React.forwardRef((props, ref) => {
      // 省略子组件内部的具体实现
    })
    
    1. 通过uselmperativeHandle可以值暴露固定(forwardRef会将子组件的DOM直接暴露给了父元素)
    useImperativeHandle(通过forwardRef接收到的父组件的ref对象, () => 自定义ref对象, [依赖项数组(可选)])
    
    • 关于第三个参数(依赖项):
      1. 空数组:只在子组件首次被渲染时,执行 useImperativeHandle 中的 fn 回调,从而把 return 的对象作为父组件接收到的 ref。
      2. 依赖项数组:子组件首次被渲染时,依赖项改变时,会执行 useImperativeHandle 中的 fn 回调,从而让父组件通过 ref 能拿到依赖项的新值。
      3. 省略依赖项数组(省略第三个参数):此时,组件内任何 state 的变化,都会导致 useImperativeHandle 中的回调的重新执行。

useEffect

  • Effect Hook可以让你来完成一些类似于class中生命周期的功能;事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects) ;所以对于完成这些功能的Hook被称之为Effect Hook
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个回调函数
  • class组件的编写过程中,某些副作用的代码,需要在componentWillUnmount中清除。useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B。这是effect可选的清除机制,每个effect都可以返回一个清除函数,可以将添加和移除订阅的逻辑放在一起。React会在组件更新和卸载的时候执行清除操作。
  • useEffect有两个参数:
    • 参数一:执行的回调函数;
    • 参数二:依赖项,该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
      • 省略依赖项数组,每次更新渲染完毕之后都会执行
      • 指定依赖项数组,每次渲染完毕之后,判断依赖项是否变化,再决定是否执行
      • 指定空数组,仅在首次渲染完毕之后,执行唯一的一次
  • 基础语法:
useEffect(() => { /* 依赖项变化时,要触发的回调函数 */ }, [依赖项])
  • 例如:
// 类组件写法
import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: 100
    }
  }

  componentDidMount() {
    document.title = this.state.counter
  }

  componentDidUpdate() {
    console.log('-------')
    document.title = this.state.counter;
  }

  componentWillUnmount() {
    
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>计数: {counter}</h2>
        <button onClick={e => this.setState({ counter: counter+1 })}>+1</button>
      </div>
    )
  }
}

export default App
// 函数式组件写法
import React, { memo } from 'react'
import { useState, useEffect } from 'react'

const App = memo(() => {
  const [count, setCount] = useState(200)

  useEffect(() => {
    // 当前传入的回调函数会在组件被渲染完成后, 自动执行
    // 网络请求/DOM操作(修改标题)/事件监听
    document.title = count
  })

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
    </div>
  )
})

export default App
  • 清理副作用:如果当前组件中使用了定时器或绑定了事件监听程序,可以在返回的函数中清除定时器或解绑监听程序。
// 组件卸载或依赖项发生变化时执行,react会判断是否存在return函数,如果存在先执行return再执行useEffect函数
useEffect(() => {
  // 1. 执行副作用操作
  // 2. 返回一个清理副作用的函数
  return () => { /* 在这里执行自己的清理操作 */ }
}, [依赖项])
  • 注意事项:
    1. 不要在 useEffect 中改变依赖项的值,会造成死循环。
    2. 依赖项的值不应该是对象,会判断对象的引用;可以使用对象的具体属性或者使用展开运算符或**Object.assign()**生成一个新对象
    3. 多个不同功能的副作用尽量分开声明,不要写到一个 useEffect 中。

useLayoutEffect

  • useEffect会在渲染的内容更新到DOM上执行,异步执行,不会阻塞DOM的更新
  • useLayoutEffect会在渲染的内容更新到DOM上之执行,同步执行,会阻塞DOM的更新
  • 如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect
  • 注意:React 保证了 useLayoutEffect 中的代码以及其中任何计划的状态更新都会在浏览器重新绘制屏幕之前得到处理。

useInsertionEffect

  • 本质上 useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。除非你正在使用 CSS-in-JS 库并且需要注入样式,否则你应该使用 useEffect或者 useLayoutEffect
export default function Index(){

React.useInsertionEffect(()=>{
     /* 动态创建 style 标签插入到 head 中 */
     const style = document.createElement('style')
     style.innerHTML = `
       .css-in-js{
         color: red;
         font-size: 20px;
       }
     `
     document.head.appendChild(style)
  },[])

  return <div className="css-in-js" > hello , useInsertionEffect </div>
}

useReducer

  • useReducer仅仅是useState的一种替代方案:
    • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
    • 或者这次修改的state需要依赖之前的state时,也可以使用;useReducer只是useState的一种替代品,并不能替代Redux
    • 更好的描述“状态”,让代码逻辑更清晰,代码行为更易预测
  • 基础语法:
/*
1. reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示处理完毕后的新状态。
2. initState 表示初始状态,也就是默认值。
3. initAction 是进行状态初始化时候的处理函数,它是可选的,如果提供了 initAction 函数,则会把 initState 传递给 initAction 函数进行处理,initAction 的返回值会被当做初始状态。
4. 返回值 state 是状态值。dispatch 是更新 state 的方法,让他接收 action 作为参数,useReducer 只需要调用 dispatch(action) 方法传入的 action 即可更新 state。
*/
const [state, dispatch] = useReducer(reducer, initState, initAction?)

使用 Immer 编写更简洁的 reducer 更新逻辑

  1. 安装immer相关的依赖包:
npm install immer use-immer -S
  1. use-immer 中导入 useImmerReducer 函数,并替换掉 React 官方的 useReducer 函数的调用:
// 1. 导入 useImmerReducer
import { useImmerReducer } from 'use-immer'

// 父组件
export const Father: React.FC = () => {
 // 2. 把 useReducer() 的调用替换成 useImmerReducer()
 const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
}
  1. 修改 reducer 函数中的业务逻辑,case 代码块中不再需要 return 不可变的新对象了,只需要在 prevState 上进行修改即可。Immer 内部会复制并返回新对象,因此降低了用户的心智负担。

useSyncExternalStore

  • useSyncExternalStore 是一个订阅外部 store 的 React Hook。 能够让 React 组件在 concurrent(同时调度) 模式下安全地有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。当读取到外部状态发生了变化,会触发一个强制更新,来保证结果的一致性。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
/*
参数一:subscribe:订阅函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的 callback。这会导致组件重新渲染。subscribe 函数会返回清除订阅的函数。

参数二:getSnapshot:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。

参数三:可选 getServerSnapshot:hydration模式下的getSnapshot
*/

useContext

  • Context Hook允许我们通过Hook来直接获取某个Context的值;当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext provider的context value值。
  • 使用 props 层层传递数据的维护性太差了,我们可以使用 React.createContext() + useContext() 轻松实现多层组件的数据传递。
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"

const App = memo(() => {
  // 使用Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2>User: {user.name}-{user.level}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
})

export default App

非侵入的方式使用Context

  • 为了保证父组件中代码的单一性,也为了提高 Provider 的通用性,我们可以考虑把 Context.Provider 封装到独立的 Wrapper 函数式组件中
// 声明 TS 类型
type ContextType = { count: number; setCount: React.Dispatch<React.SetStateAction<number>> }
// 创建 Context 对象
const AppContext = React.createContext<ContextType>({} as ContextType)

// 定义独立的 Wrapper 组件,被 Wrapper 嵌套的子组件会被 Provider 注入数据
export const AppContextWrapper: React.FC<React.PropsWithChildren> = (props) => {
  // 1. 定义要共享的数据
  const [count, setCount] = useState(0)
  // 2. 使用 AppContext.Provider 向下共享数据
  return <AppContext.Provider value={{ count, setCount }}>{props.children}</AppContext.Provider>
}
  • 定义好 Wrapper 组件后,我们可以在 App.tsx 中导入并使用 WrapperLevelA 组件,代码如下:
import React from 'react'
import { AppContextWrapper, LevelA } from '@/components/use_context/base.tsx'

const App: React.FC = () => {
  return (
    <AppContextWrapper>
      <!-- AppContextWrapper 中嵌套使用了 LevelA 组件,形成了父子关系 -->
      <!-- LevelA 组件会被当做 children 渲染到 Wrapper 预留的插槽中 -->
      <LevelA />
    </AppContextWrapper>
  )
}

export default App

useMemo

  • 当父组件被重新渲染的时候,也会触发子组件的重新渲染,这样就多出了无意义的性能开销。如果子组件的状态没有发生变化,则子组件是不需要被重新渲染的。在 React Hooks 中,我们可以使用 React.memo 来解决上述的问题,从而达到提高性能的目的。
const 组件 = React.memo(函数式组件)
  • useMemo会返回一个函数的memoized (记忆的)值;在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • useCallback 和 useMemo都是react可用于性能优化的内置hooks。两者的区别在于:useCallback缓存的是一个函数,而useMemo缓存的是计算结果。
// useCallback
// 第一个参数是一个回调函数,useCallback会缓存这个函数,返回缓存的回调函数
// 第二个参数是依赖项,只有当依赖项改变时,才会重新创建这个函数
const memorizedCallback = useCallback(()=>{
    doSomething(a,b);
},[a,b])
 
// useMemo
// 第一个参数是一个函数,useMemo会缓存函数运行返回的值,返回缓存的值
// 第二个参数是依赖项,只有当依赖改变时,才会重新计算这个值
const memorizedValue = useMemo(()=>computeValue(a,b),[a,b])

useCallback

  • useCallback 会返回一个 memorized 回调函数供组件使用,从而防止组件每次 rerender 时反复创建相同的函数,能够节省内存开销,提高性能。
const countRef = useRef()
countRef.current = count
const increment = useCallback(function foo() {
  console.log("increment")
  setCount(countRef.current + 1)
}, [])

useTransition

  • 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。
  • useTransition 可以将一个更新转为低优先级更新,使其可以被打断不阻塞 UI 对用户操作的响应,能够提高用户的使用体验。它常用于优化视图切换时的用户体验。
const [isPending, startTransition] = useTransition();
// 参数:调用 useTransition 时不需要传递任何参数

/*
返回值(数组):
- isPending 布尔值:是否存在待处理的 transition,如果值为 true,说明页面上存在待渲染的部分,可以给用户展示一个加载的提示
- startTransition 函数:调用此函数,可以把状态的更新标记为低优先级的,不阻塞 UI 对用户操作的响应
*/
  • 注意事项:
    1. 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。异步状态更新不会被标记为 transition。
    2. 标记为 transition 的状态更新将被其他状态更新打断。例如在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。
    3. transition 更新不能用于控制文本输入。

useDeferredValue

  • ** useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。**useDeferredValue的作用是一样的效果可以让我们的更新延迟。
  • useTransition 是把 startTransition 内部的更新任务变成了过渡任务transtion;而 useDeferredValue 是把原值通过过渡任务得到新的值,这个值作为延时状态。 也就是说一个是处理一段逻辑,另一个是生产一个新的状态。
  • 有些场景不能使用 useTransition 进行性能优化,因为 useTransition 会把状态更新标记为低优先级被标记为 transition 的状态更新将被其他状态更新打断。因此在高频率输入时,会导致中间的输入状态丢失的问题。
// 根据 kw 得到延迟的 kw
const deferredKw = useDeferredValue(kw);
// 子组件配合React.memo实现中间状态的缓存
  • useDeferredValue 的返回值为一个延迟版的状态
    1. 在组件首次渲染期间,返回值将与传入的值相同
    2. 在组件更新期间,React 将首先使用旧值重新渲染 UI 结构,这能够跳过某些复杂组件的 rerender,从而提高渲染效率。随后,React 将使用新值更新 deferredValue,并在后台使用新值重新渲染是一个低优先级的更新。这也意味着,如果在后台使用新值更新时 value 再次改变,它将打断那次更新。

useDebugValue

  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。这个hooks目的就是检查自定义hooks。在生产环境中,useDebugValue 不会产生任何效果,因为它只用于调试目的。
import { useDebugValue } from 'react';

function useOnlineStatus() {
  // ...
  useDebugValue(isOnline ? 'Online' : 'Offline');
  // ...
}

// useDebugValue(value, format?)
// 参数一:value--值,可以是任何类型
// 参数二:可选的format--格式化函数,不指定的直接显示value

useId

  • useld是一个用于生成横跨服务端和客户端的稳定的唯一ID的同时避免 hydration 不匹配的hook。
import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  // ...
}
  • useld是用于react的同构应用开发的,前端的SPA页面并不需要使用它

    • useld可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成hydration mismatch
  • 在进行SSR时,我们的页面会呈现为HTML。但仅HTML不足以使页面具有交互性。例如,浏览器端JavaScript为零的页面不能是交互式的(没有JavaScript事件处理程序来响应用户操作,例如单击按钮)。为了使我们的页面具有交互性,除了在Node.js中将页面呈现为HTML之外,我们的UI框架(Vue/React…)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在Node.js中呈现的HTML的DOM元素。)
    这个过程称为hydration。


自定义Hook

  • 自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。
  • 举例::
// 使用本地存储
import { useEffect } from "react"
import { useState } from "react"

function useLocalStorage(key) {
  // 1.从localStorage中获取数据, 并且数据数据创建组件的state
  const [data, setData] = useState(() => {
    const item = localStorage.getItem(key)
    if (!item) return ""
    return JSON.parse(item)
  })

  // 2.监听data改变, 一旦发生改变就存储data最新值
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [data])

  // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
  return [data, setData]
}


export default useLocalStorage
// 获取当前鼠标位置
import { useEffect, useState } from "react";

export const useMousePosition = (delay: number = 0) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    let timer: null | NodeJS.Timeout = null;
    const mouseMoveHandler = (e: MouseEvent) => {
      if (timer !== null) return;
      timer = setTimeout(() => {
        setPosition({ x: e.clientX, y: e.clientY });
        timer = null;
      }, delay);
    };
    window.addEventListener("mousemove", mouseMoveHandler);

    return () => window.removeEventListener("mousemove", mouseMoveHandler);
  }, []);
  return position;
};
  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

会思想的苇草i

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

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

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

打赏作者

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

抵扣说明:

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

余额充值