精读Javascript系列(八)事件循环细则 II: NodeJS事件循环相关

前端福音:Serverless SSR 的天作之合 什么是 SSR SSR 顾名思义就是 Server-Side Render, 即服务端渲染。原理很简单,就是服务端直接渲染出 HTML 字符串模板,浏览器可以直接解析该字符串模版显示页面,因此首屏的内容不再依赖 Javascript 的渲染(CSR - 客户端渲染)。 SSR 的核心优势: 首屏加载时间:因为是 HTML 直出,浏览器可以直接解析该字符串模版显示页面。 SEO 友好:正是因为服务端渲染输出到浏览器的是完备的 html 字符串,使得搜索引擎 能抓取到真实的内容,利于 SEO。 SSR 需要 阅读详情

前言

本篇也是过渡篇,主要补充NodeJS的重点和难点。

嘛,废话不多说,正文开始。

NodeJS

首先,理解 NodeJS 有一个很重要的前提,就是知道它究竟是 单线程的,还是 多线程 的?

还是如以往一样,越是简单的问题答案就每个人的答案就越是令人迷惑;
实践是检验真理的唯一标准,最后看看 node线程数就知道了——7个,所以它是多线程的。
但是为何有人说它是 单线程 呢?

单线程这个说法应该被严重曲解的,导致出现了一些误导性; 完整说法应该是 NodeJS运行Javascript代码只有一个线程, 这个特征与浏览器如出一辙,浏览器运行Javascript也是单线程的。然而却不能说浏览器就是单线程,同样的NodeJS也是这个道理。

所以从根本上来说,NodeJS只有一个运行JS的主线程,它也是负责事件循环的线程; 与其他异步模型一般,它永远不会阻塞。 Node官网的事件循环,太过简洁了在细节方面做得不够详尽,因此只能参考实现它的组件——libuv但这个细节也太多了)。

NodeJS 的事件循环其实是由 libuv 实现的,所以只有理解了 libuv 的事件循环才能算彻底理解了 nodejs 的事件循环。libuv 实现机制要细说三天三夜也讲不完, 所以长话短说——

  1. libuv 负责收集所有事件(它也可能会来自外部、也包括内核的)
  2. 针对这些事件注册回调函数
  3. 事件发生后被执行回调函数。

其实原理与浏览器环境大同小异,都是采用消息传递的异步通信方式,并且都是基于事件循环来执行代码的。不过要注意的是Node 程序处理最多的不是 页面渲染, 而是 输入和输出,统称作IO, 例如文件读写、网络请求等等,这类操作统一特点都是阻塞(响应时间超长)的,传统的读写事件都是同步的,也就是说在获取文件全部内容之前,根本无法做任何事情,自然也效率低下,我们的任务就是狠狠的压榨CPU性能

libuv便是上面问题的一个解决方案,基本思路是将一些阻塞任务(主要是IO,网络请求.etc)这类阻塞任务分配到工作线程中等待稍后执行,这些工作线程会统一保存在线程池中;然后开启事件循环一一处理。 至于一次能处理多少事件(可能一次事件循环爆发式接收到500个请求),取决于计算机硬件。

所以,理解 nodejs 事件循环, 还是要从 libuv事件循环开始。
下面是官网给出的事件循环示意图:
libuv 概览图:
在这里插入图片描述

理解事件循环难点:

  • 在主模块代码结束后,事件注册才会完成。一旦有注册的事件,就必然会产生一个handler,它可以标识任务的上下文(术语:事件句柄、文件描述符)但这时还没有正式开启事件循环
  • 当有handler存在,初始化线程池。然后将已经注册事件的handler分配到线程池的对应workthread中。例如setTimeouthandlers(Timer)会分配到TimersetImmediatehandler(CheckHandler)分配到Check, 其他事件分配到poll中统一处理。
  • 开启事件循环,然后更新事件循环的handler计数 ; 现在能够看出,事件循环的每个phase其实都保存在了线程池中。
  • 也正因为如此,phase并不是严守顺序的;或是说它是跳跃性的。例如当第一次进行事件循环时,Timer没有到期,所以不会执行回调。但反过来说一旦到期就一定会执行回调。由于没有需要异步完成的IO Callback,所以不会进入pending callback
  • 因此,首次事件循环会直接跳跃到poll阶段完成IO Callbackpoll阶段极其特殊,它有一个阻塞时长,如果Callback在阻塞时长内没有完成,相应的io callback异步完成,即在下一个事件循环的pending callback处理完所有剩余的callback。同样的道理,如果在阻塞时长内存在setImmediate的回调,那么就会立即进入到check阶段(不用在意几回合事件循环)。
  • 每个事件循环跳跃下一个事件循环前,都会有一个Close Handler过程,它会清理掉完成状态(done=true)的handler,更新事件循环的引用计数等等。

事件循环的核心,poll阶段的总结就是(有点都合主义的味道):

  1. 计算Poll阶段的阻塞时间(即Timeout)
  2. 执行已发生事件的回调函数,但还有:
    1. 是否有nextTickmicrotasks? 如果有则执行,否则继续
    2. 检查是否有setImmediate回调,如果有执行回调然后进入Check阶段。
    3. 若无,继续。
  3. 在阻塞时间内等待事件发生,如果存在Timer,那么阻塞时间是最近的Timer到期时间,否则不限制阻塞时间
    1. 若有事件发生,立即执行相应的回调函数,此步骤完全等价于(2)
    2. 若无则继续等待下去(直至node 停止监听、或没有活跃的handler)。
    3. 如果在阻塞时间内仍有未回调完成的任务handler),挂起在下一个事件循环的pending callback阶段排队处理。
  4. 超过阻塞时间后,进入下一个事件循环,执行到期Timer
    1. 此时不再有可用setImmediate回调,因此前移
    2. 阻塞时间是最近的Timer到期时间

特别注意

  • (2)实际上是(3)其中的一个步骤,但是分离出来更容易理解一些。
  • 虽然官网说是绕回,但是我觉得这里应该是前移到下一个事件循环中更准确一点。这是和nodejs官网矛盾点。
    想要了解libuv事件循环所有细节的,可以直接跳到后面。

setTimeout VS setImmediate

这是很令人兴奋的话题,它们到底谁更优先? 事实上它们并没有优先级关系,因为事件循环没办法用顺序去理解,因此不要用谁优先谁就执行原则,而是谁触发谁就必须执行原则。

注意两点:

  1. setTimeout是到期立即执行
  2. setImmediate是进入到check阶段后执行。

也就是说,TimerCheckHandler是没有逻辑上的必然关系的。

例如:

setTimeout(()=>{
    var start = Date.now();
    setTimeout(()=>{
        console.log('------------------');
        console.log('timeout 1 = '+(Date.now()-start)+'ms');
    })

    setTimeout(()=>{
        console.log('------------------');
        console.log('timeout 2 = '+(Date.now()-start)+'ms');
    })

    setImmediate(()=>{
        console.log('------------------');
        console.log('check 1 = '+(Date.now()-start)+'ms');
    })

    setImmediate(()=>{
        console.log('------------------');
        console.log('check 2 = '+(Date.now()-start)+'ms');
    })
})

输出结果:

------------------
check 1 = 9ms
------------------
check 2 = 10ms
------------------
timeout 1 = 11ms
------------------
timeout 2 = 11ms

从结果上看,setImmediate的回调永远先于setTimeout

5aaaa

简而言之, 在Timer到期之前就已经触发了CheckHandler,因此会跳跃到Check。执行完毕后Timer必然到期,因此执行。

但是为什么在主模块setTimeoutsetImmediate这两个函数是随机呢?


var start = Date.now()
setImmediate(()=>{
    console.log('------------------');
    console.log('check = '+(Date.now()-start)+'ms');
})

setTimeout(()=>{
    console.log('------------------');
    console.log('timeout = '+(Date.now()-start)+'ms');
})

输出:

timeout = 10ms
------------------
check = 12ms
------------------
check = 8ms
------------------
timeout = 9ms

说明:

  • 开启事件循环后,更新now时间,如果Timernow时间到期,那么就会直接运行。否则不会运行。简而言之,如果能够尽快进入事件循环,就不会执行Timer反之就会执行。

这个解释是在github看到的,感觉很有道理的样子。


nextTick VS Promise

NodeJS中,也有微任务microtasks,不过它分为两种微任务:

  1. nextTick : 即nextTick
  2. otherMicrotasksPromise
    其中nextTick总是会先于Promise执行。无论哪种微任务,它们都能在跳跃到其他phase前执行完毕。

例如:

console.log('start');

Promise.resolve('promise 1').then(console.log)
Promise.resolve('promise 2').then(console.log)

process.nextTick(()=>{
    console.log('ha,ha,ha,ha');
    console.log('im tickObject!!');
})

console.log('end');

验证输出:

start
end
ha,ha,ha,ha
im tickObject!!
promise 1
promise 2

然后四个一起,综合来PK一下(正常版本):


const fs = require('fs')
const start = Date.now()


fs.readFile(__filename,()=>{
    console.log('start');
    setImmediate(()=>{
        console.log('checkhandler 1');
        Promise.resolve('promise 1').then(console.log)
        console.log('-----------------------------------');
    })    
    setTimeout(()=>{
        console.log('timeout 1');
        
    })
    setImmediate(()=>{
        console.log('checkhandler 2');
        Promise.resolve('promise 2').then(console.log)
        process.nextTick(()=>{console.log('nextTick 2');
        })
        console.log('-----------------------------------');
    })  
    process.nextTick(()=>{
        console.log('nextTick 1');
        
    })  
    console.log('end');
})


输出:

start
end
nextTick 1
checkhandler 1
-----------------------------------
promise 1
checkhandler 2
-----------------------------------
nextTick 2
promise 2
timeout 1

上面的过程如下:

  1. poll阶段:调用readFile,开始文件读写。
  2. poll / pending_callback 阶段: 文件读写完成。开始调用回调函数:
    1. 输出 start
    2. 调用 setImmediate ; checkhandler1活跃状态
    3. 调用 setTimeout timeout 1被初始化,等待到期。
    4. 调用 setImmediate checkhandler2活跃状态
    5. 调用nextTick, 产生一个TickQueue
    6. 输出end
    7. 执行TickQueue中的所有任务,输出nextTick 1
      存在checkhandlers , 进入check阶段
  3. check阶段:处理所有的checkhandlers的回调:
    1. 执行checkhander1的回调:
      1. 输出checkhandler1
      2. Promise.resolve().then , 产生一个promiseList
      3. 输出横线
      4. 处理promiseList中所有的任务,输出promise 1
    2. 执行checkhander2的回调:
      1. 输出checkhandler2
      2. Promise.resolve().then , 产生一个promiseList
      3. nextTick, 产生一个TickQueue
      4. 输出横线
      5. 处理TickQueue所有任务,输出nextTick 2
      6. 处理promiseList所有任务,输出promise 2
        Timeout到期,跳跃到Timer
  4. Timer阶段: 处理Timeout回调,输出timeout 1

注意:

  • 每处理完一个回调,就会调用一次nextTickotherMicrotask
  • io callback究竟是在哪个阶段完成的,是未确定的

实际上在其他模块,也会出现随机顺序问题,例如:

const fs = require('fs')


setTimeout(()=>{
  
    fs.readFile(__filename,()=>{
        console.log('file one');
        setTimeout(()=>{
            console.log('timeout one');
        })
    })

    fs.readFile('./file.md', ()=>{
        console.log('file two');
        setTimeout(()=>{
            console.log('timeout two');
        })
    })

})

然后出现以下三种结果(更离谱的代码就没写):666

这也侧面证明了IO Callback的随机性,它的完成与文件大小也是息息相关的。左右两侧的结果是比较容易理解的,中间的是为什么?
大概率是因为file two是在pending callback阶段完成的。

  1. poll阶段,fileone回调完成,添加一个定时器。
  2. poll阶段,但是filetwo回调未完成, 定时器到期,立即回到Timer阶段
  3. 执行Timer
  4. pending callback,回调完成,添加一个定时器。

下面是小生看了一遍源代码得出的结论,大致上补充了点细节。不过因为linux编程这一块遗忘了不少,可能未必是正解,这方面还请多提宝贵意见。

同步转发:
https://juejin.im/post/5ed36676e51d457b3a67ccef

附注:libuv事件循环所有细节

首先要知道几个术语:

  • handler来标识一个任务,事实上它就是句柄(文件描述符),总之通过它可以引用任务的一切信息:任务的回调函数、回调状态等等。
  • 一个handler普遍有三种状态:初始化挂起(即暂停) 、 活跃 。常用后面的两种。

开始吧:

Bootstrap/Dispatcher:

  • 初始化Node环境
  • 执行同步JS代码并进行事件分发(注册)。
  • 初始化线程池

然后正式进入到事件循环中:

UpdateLoop:

  1. 事件循环是否是活跃的(主要检查是否有活跃的handler
  2. 如果是,更新当前时间(now),正式进入Phase I
  3. 否则终止事件循环,关闭。

Phase I: Timer

  1. 检查 Timer 是否到期
  2. 如果到期,立即触发事件然后继续执行回调。
  • 上面是最简单的,就是setTimeout/setInterval这类调用。

Phase II:Pending (IO) Callbacks

  1. 检查 Pending_Queue是否有handler
  2. 如果有,立即从Pending_Queue取出handler进行回调操作。
  3. 如果handler中回调函数代码中存在CheckHandlers(即setImmediate(...)),那么处理完成后跳转到Phase V
  4. 否则继续。

附注:

  • Pending_Queue用于放置未回调完成的handler的队列
  • 一般IO Events 会在这个阶段完成处理。例如:文件操作时抛出异常、读取完成然后执行回调等等。
  • CheckHandlers是极其特殊的handler,简单来说就是setImmediate(...)的回调。不只会在Phase II导致迭代前移(直接跳转到Phase V),甚至在其他Phases也会如此。

Phase III.a: Idle/Idling

  • 进入空转监听阶段
  • 它会不断地监听handler是否发生事件(此时handler活跃的),如果是进入Phase III.b
  • …(记录日志等,与我们无关了……)
  • 达到事先规定好的循环次数后,也未曾监听到新事件 那么Idling会被终止;事件循环也会被终止(因为一直监听不到事件发生)。

附注:

  • Idling 阶段永远不会停止;也就是说即使事件发生后转入到Phase III.b时, Idling也是一直在空转监听
  • 也正因为如此,我们才能任何时刻监听到事件
  • 虽说如此, Idling会有一个极限循环次数,这个次数由硬件决定的。过了这个次数,它就结束了。

Phase III.b: Prepare

  • 为活跃的handler执行回调前做些准备
  • ……(准备上下文……等)
  • 进入 Phase IV,正式处理回调。

附注:

  • 事实上,了解Phase III没什么大用,因为不可能编写一定处于Phase III阶段的代码。
  • 它是libuv执行的。
  • 只是补充了一点细节

Phase IV Poll:

  1. 计算超时时间(Timeout
  2. 在超时时间内(Timeout):
    • 如果checkhandler存在,执行相应回调
    • 如果发现IO Event,立即执行回调。
    • 如果无事件发生,检查是否超时
    • 如果超时,立即退出Phase IV。否则回到(2.1)
  3. 超过超时时间后,会有Timer到期,直接前移到下一个事件循环
  4. 会存在未回调的handler,此时它们会在下一个事件循环的pending callbacks完成处理。
    未完待续

附注:

Timeout计算规则:

  • 在以下情形,Timeout=0
    1. 事件循环将被终止(由IdleHandler设置)
    2. 没有活跃的handler
    3. Idle不再监听(即IdleHandler终止)
    4. Pending_Queue不为空.
  • 除却以上情形, 如果有待处理的Timer,那么 Timeout是最近的Timer到期时间。
  • 如果没有待处理的Timer, 那么Timeout-1 ,可以理解为 poll阻塞时间为 无穷大(这块我忘了,应该是阻塞时间不确定)。

Phase V : Check

  1. 检查CheckHandlers是否存在(即setImmediate的消息序列)
  2. 若有则处理所有CheckHandlers
  3. 完成, 进入Phase VI

Phase VI : Close handlers

  1. 对处理完的handler进行Close操作,例如 send()/close()/destory()等等。
  2. 调用GC,进行垃圾回收
  3. 转至UpdateLoop
精读Javascript系列(二)环境记录与词法环境 前言 关于变量,在Javascript核心知识体系中,占比不重,即使有些迷惑行为,也认为Javascript本应就如此,就因为下意识的草率,导致这些小问题成了日后进阶的壁障。 这里就先从两个极为经典的问题开始吧。 变量提升暂存死区 先来两个示例: 其一:变量提升 console.log(typeof number); // undefined var number = 1000; console.log(number); // 1000 应用中,var声明的变量能够提前使用虽然只是und 阅读详情

相关推荐

精读Javascript系列(一) 变量 、 初识词法环境

前言 其实,我觉得Javascript核心中重要的东西并非是从旧版本扩展来的高大上的语法,例如解构赋值啊、展开语法剩余参数(嘛……虽然的确是很666),但是用好这些,其实都建立在你对变量的认识上(常有人不知道什么是左值或右值的区别)正因如此,我觉得了解一个Javascript还是从最基本做起,就是了解一下何为变量吧。 本文其实也并非完全基础,还是建立在对Javascript有一定了解之上的,至少对对象要有一定的认知。开始吧。 变量与数据 什么是变量? 越简单的问题答案往往越是让人感到意外,多数人的答案都与

krfwill的博客 1034

nodejs之next_tick源码分析

next_tick函数是process对象的一个属性。他是在bootstrap_node.js中设置的。 bootstrap_node.js NativeModule.require('internal/process/next_tick').setup(); internal/process/next_tick process.nextTick = nextTick; functio...

标子 的专栏 719

精读Javascript系列(六)并发编程、 Javascript异步框架

前言 从本质上来讲,Javascript是非阻塞型单线程编程语言,因此JS自身是没有异步功能的。常说的异步,是浏览器API(WebWorker.etc)与JS强强联合实现的。与初次接触JS时不同,想要真正掌握JS的异步,就必须明白什么是JS采用的是什么并发编程框架。 实际上,JS异步已经最大程度的简化并发编程的繁杂性,基本上只要牢记异步常见套路,就能轻轻松松实现异步。 但是,套路并不适合分析,毕竟JS在实现上可不是按套路来的。因此在JS进阶这个层次,就必须完全弥补这个缺口。想要揭开JS实现异步的神秘面纱,就

krfwill的博客 966

nodejs 异步之 Timer &Tick; 篇

nodejs 异步之 Timer &Tick; 篇 nodejs 异步之 Timer &Tick; 篇 - CNodenodejs 异步之 Timer &Tick; 篇Timer: 在前端开发中,我们 经常会使用setTimeout 函数...

264

serverless 框架_前端福音:Serverless SSR 的天作之合

什么是 SSRSSR 顾名思义就是 Server-Side Render, 即服务端渲染。原理很简单,就是服务端直接渲染出 HTML 字符串模板,浏览器可以直接解析该字符串模版显示页面,因此首屏的内容不再依赖 Javascript 的渲染(CSR - 客户端渲染)。SSR 的核心优势:首屏加载时间:因为是 HTML 直出,浏览器可以直接解析该字符串模版显示页面。SEO 友好:正是因为服务端渲染输出...

weixin_39584571的博客 199

为什么使用服务器端渲染 (SSR)

什么是 SSR SSR 顾名思义就是Server-Side Render, 即服务端渲染。原理很简单,就是服务端直接渲染出 HTML 字符串模板,浏览器可以直接解析该字符串模版显示页面,因此首屏的内容不再依赖 Javascript 的渲染(CSR - 客户端渲染)。 SSR 的核心优势: 首屏加载时间:因为是 HTML 直出,浏览器可以直接解析该字符串模版显示页面。 SEO 友好:正是因为服务端渲染输出到浏览器的是完备的 html 字符串,使得搜索引擎 能抓取到真实的内容,利于 SEO。 SSR .

飞翔的熊blabla 4522

脚手架架构设计框架搭建 - 框架搭建

脚手架架构设计框架搭建 - 框架搭建 npm 项目本地依赖引用方法 脚手架框架 yargs 快速入门 基于Lerna重新设计简历

chengbo_eva的博客 561

Python机器学习:从零基础到项目实战

当您翻开此书,您正踏入一场数据与智慧的修行。机器学习,并非冰冷的符码,而是机器模拟人类洞察世界的法门。本书将带您,以Python为舟,泛游于算法之海。我们不只传授“术”,更探求其后的“道”——从数据的生灭流转中观照规律,于模型的迭代演进里体悟得失。愿您合上书卷时,收获的不仅是驾驭数据的技能,更有一双洞悉复杂、化繁为简的“智慧之眼”。现在,让我们一同启程。

YunWisdom 862

精读Javascript系列(三) 执行上下文、 执行栈、初识事件循环

前言 这时可以接触真正实用的东西了,毕竟变量也不能代表整个Javascript语言,虽然有些不可思议,但变量的确是Javascript必经之路之一,关于变量的奇特行为数不胜数(真的是这样),不过这些我想高阶Javascript都努力回避这些,新手也不懂,所以我就跳过了。 下面的这些概念,无论是执行上下文、 还是执行栈,它在规范中的概念都很抽象,很多内容的理解实际靠的都是想象力,若有错误之处,还请指正。 执行上下文 简而言之,执行上下文(Execution Context)是正在运行的可执行代码所处环境的抽象

krfwill的博客 5056

精读Javascript系列(10) Promise——Promise/A+规范解读Promise

Promise简介

krfwill的博客 588

精读Javascript系列(四)过渡篇:左值与This绑定

前言: 在前三篇中,可以将学习路线图归纳为以下: 变量与标识符 —> 词法环境与作用域 —> 词法环境与环境记录 —> 词法环境与执行上下文 执行上下文与执行栈 —> 执行栈与任务序列 —> 事件循环入门 。 现在基本上可以看出,Javascript进阶的路程脉络了。所有的内容都是围绕核心进行描述总结的,并且尽可能避免知识断层或是概念套路化这种情形。否则真的,即便是尝试进阶Javascript多少次,可能也要重新来一遍喜滋滋。 我的资料大部分来自github大佬博客、ES8标

krfwill的博客 509

精读Javascript系列(七)事件循环细则 I:微任务、宏任务

前言 之前笔记扩展大量的异步基础内容,个人觉得应该是所有开发人员应该理解的,Javascript开发人员也不应该有例外。现在开始,算是正式进入Javascript的环节。例如事件循环的处理机制,微任务、宏任务…… 由于篇幅所限,暂时就录入这些吧。 开始: 事件循环 首先补充一下上次事件循环的更多细节: 事件循环开启 更新当前消息序列 从当前消息序列中取出任务 消息序列是先入先出结构,也就是说它是按照顺序取出的。 处理任务 自上而下运行JS代码 如果发出异步请求,然后将消息保存到这个新消息序列(若无则新建

krfwill的博客 466

精读Javascript系列(五) 函数闭包

前言 本文专门介绍闭包,但事实上,闭包的难点并不在概念,而是在词法环境的嵌套上。只要将词法环境的嵌套关系整理清楚,闭包就瞬间被克服了。 总之,先不废话了,正文开始。 闭包 如果一个函数在定义的词法环境外运行并记住了定义时的词法环境,这样的现象就可以称作函数闭包(Function Closure) 举个简单例子: function f(x=0){ var count=x; function getCount(){ return

krfwill的博客 435

精读Javascript系列(9coll) Promise — 回调地狱、Promise构造器

前言 最后

krfwill的博客 419

精读Javascript系列序---导读

前言 趁现在还能学习的时候学习点真正有用的东西吧。这是我从某人那里听到了这样的一句话,我决定翻一翻过去学习Javascript的导图笔记,之后发现对Javascript有了亿点点更新的认识。我很想这种心情记录下来,这就是我决定在有时间闲的蛋疼的时候写写博客的原因。 Javascript学习历程 总的来说,Javascript的学习是无止境的;不有一日对 ECMAScript规范掌握的滚瓜烂熟 ,始终不会到达尽头;基本上我们所看到的javascript书籍,都能反映我们javascript的水平。 请看(从上

krfwill的博客 386

three js 9 补间动画tween

如果使用了yoyo的动画,且没有加delay,会有如下问题。AI给的解释为这是已知的bug。1 加上一个极短的delay。2 改用两个动画相互调用。

吴梓穆的博客 27
上一篇: 精读Javascript系列(七)事件循环细则 I:微任务、宏任务
下一篇: 精读Javascript系列(9coll) Promise — 回调地狱、Promise构造器
krfwill
博客等级 码龄11年 38粉丝 16原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值