手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法 - Virtual DOM 的实现原理

目标

在这里插入图片描述

什么是虚拟DOM

在这里插入图片描述
可以看到真实dom非常多,创建成本很高
在这里插入图片描述
通过sel描述标签,text描述文本
创建虚拟dom比真实dom成本小很多
总结:虚拟dom就是普通的js对象用来描述真实dom

为什么使用虚拟DOM

在这里插入图片描述
模板引擎问题:数据变化后页面无法获取上一次的状态只好删除元素后重新创建
在这里插入图片描述
所以就出现了虚拟dom
在这里插入图片描述
虚拟dom项目,发现新增后也没有闪烁问题
在这里插入图片描述
在这里插入图片描述
排序也没有闪烁,因为没有新增闪出,只改变了位置,所以比JQ性能好。

git上virtualDom的描述在这里插入图片描述
总结:在这里插入图片描述

虚拟DOM的作用和虚拟DOM库

在这里插入图片描述
先看看纯Dom操作
在这里插入图片描述
尝试虚拟dom
1创建虚拟dom对象,也就是js普通对象,点击按钮然后对比前后dom进行更新。如果这样会比纯dom操作的性能低一些,所以不是任何时候使用虚拟dom都是最好的,只有在视图较复杂的情况下才会提高性能。

ssr渲染把虚拟dom转换成普通的html,也可以通过特殊手动转换成原生引用或小程序(rn weex uniapp都使用了虚拟dom)。


在这里插入图片描述
因为只有200行所以比看vue的源码轻松得多。
此库功能强大,拓展性强。

创建项目

在这里插入图片描述
在这里插入图片描述

导入Snabbdom

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
没有值是因为,import的原因,但官方文档没有写,可以通过源码找原因
在这里插入图片描述
发现没有默认导出,导出了三个
需要import {} 才能导入
在这里插入图片描述
在这里插入图片描述
疑问:为什么require可以直接导入 import不行呢? 答:因为COMJS中所有模块都会导出一个对象所有可以直接导入,ESM中如果没有使用default导出就需要 { name , name2 }
在这里插入图片描述

代码演示

模块

Snabbdom源码解析

在这里插入图片描述
这里了解核心即可

在这里插入图片描述
tonode.js是把dom转换成vnode
研究这三个核心文件即可
在这里插入图片描述

h函数

vue中的h函数是增强过的,实现了组件机制,snabdom中没有。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
h函数的实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后返回
在这里插入图片描述

必备快捷键

看源码快捷键
在这里插入图片描述
在这里插入图片描述
快速跳回is
在这里插入图片描述
快速定位变量2
在这里插入图片描述

vnode

导入文件
定义属性
在这里插入图片描述
vnode的方法返回了的 必须是继承vnode接口
在这里插入图片描述
children和text互斥有text就不能有children。
elm:把真dom存储到elm。
key:优化,vnode转换真实dom的时候会看到。
在这里插入图片描述

patch的整体过程分析

patch(打补丁)
在这里插入图片描述
updatechildren是最复杂的核心部分
在这里插入图片描述
通过源码体验过程

init

调用init时返回了patch,所以要先了解init。
大致是先导入了需要的模块,然后定义属性和函数。
最后导出了成员。
在这里插入图片描述

3
init函数有俩个参数。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
都是些dom操作。
通过第二个参数,把虚拟dom转换成真dom。
在这里插入图片描述
最后返回了patch函数(函数返回函数属于高阶函数)
使用高阶函数的好处是,init的时候已经传入需要的参数了,patch函数就不需要传入对应的参数。
而且patch访问内部函数的时候,这里产生了闭包
在这里插入图片描述
接下来看看循环

moduls是统一的规范
在这里插入图片描述
hooks中的钩子函数和这里的modules是一致的。
在这里插入图片描述

patch

首次渲染需要真实dom
在这里插入图片描述
把真实dom转换成虚拟节点
在这里插入图片描述
判断是否同节点
在这里插入图片描述
如果是相同节点,执行patchVnode对比,对比内容是否有变化,但不会重新创建这个对应的dom提升效率。
在这里插入图片描述
这里获取父元素是为了待会创建dom后挂载到父元素上。
返回父节点在这里插入图片描述
创建vnode对应dom
在这里插入图片描述
这里是插到div#app之后,因为div后一般是换行文本节点,removeVnodes结束后首次渲染就结束了,后面触发对应钩子函数
在这里插入图片描述
这个队列里存储的是具有insert钩子函数的新vnode节点,这个队列中的元素是createElm中添加的。注意这里的insert是data中获取的,也就是用户传递过来的,那么这样可以做一些相应的操作,例如获取新插入到dom树上的dom的行高。(这里!表示推断是否有值,有就执行,没有就跳过,这样就不用写大量if判断了)
在这里插入图片描述
最后在变量post函数
在这里插入图片描述
最后返回新vnode节点,作为下次操作老节点处理

调试patch

用之前的代码
在这里插入图片描述
打断点
在这里插入图片描述
第一个是 执行下一个断点 跳过函数执行f10 进入函数执行 f11
H函数中没传入模块所以这里的insertedVnodeQueue没有,
f11 进入判断isVnode分支,这里传入的是div不是虚拟节点,
在这里插入图片描述
有sel就认为是虚拟的。
,这里没有所以进入转换虚拟dom函数 emptyNodeat
在这里插入图片描述
在这里插入图片描述
把elm真实dom,作为vnode函数第五个参数元素的值,这里没传data所以key为undefined。
f11 判断是否相同节点
在这里插入图片描述
此时因为传入的不是相同的div所以进入在这里插入图片描述
创建真dom渲染到页面上。并且把旧的从界面上移除,插入elm老节点的旁边(api.nextSibling)
在这里插入图片描述
执行完这行代码后页面渲染能看到东西了。
在这里插入图片描述
此时还没有执行删除 之前的dom还有
在这里插入图片描述
执行删除后没有了
在这里插入图片描述
因为现在没有传入任何钩子函数 这里patch首次执行过程就演示结束了
在这里插入图片描述

createElm

在这里插入图片描述
但不渲染到页面上(不挂载到dom树上),在下面方法中才渲染
在这里插入图片描述
createElm过程开始
在这里插入图片描述
第一过程如果init有值就执行init钩子,这里是创建真实dom前方便用户对vnode再次修改例如样式等,然后把处理完后的vnode.data重新赋值给data
在这里插入图片描述
在这里插入图片描述

第二过程在这里插入图片描述
转换前定义了俩个变量
在这里插入图片描述
如果sel等于感叹号会创建注释节点,不等于则创建对应dom元素,如果为空创建文本节点(这里的三个判断)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
往下就判断是否是数组,children和text互斥
如果是就遍历子节点,如果子节点有的话,继续调用createElm
在这里插入图片描述
如果是text 就判断text是否是原始值(只有string和number类型才直接显示页面上),满足条件就调用,createTextNode
在这里插入图片描述
最后如果有hook函数就执行,然后又判断了是否有insert,有就push,到这,第三过程就处理结束,最后返回dom
在这里插入图片描述
在这里插入图片描述

回到patch函数中
createElm结束后,执行 插入和移除dom方法,然后循环队列
在这里插入图片描述
总结
在这里插入图片描述
在这里插入图片描述

createElm调试

可以传入data,带处理函数,init是创建dom之前执行,create是创建dom完毕后执行,是可以拿到值的。
在这里插入图片描述
打断点
在这里插入图片描述
这里的vnode是新传入的vnode对象
在这里插入图片描述
这里打印的值应该是undefined 因为dom还没创建。
在这里插入图片描述
需要注意的是,这里init执行后又重新给data赋值了,这是因为,在init中用户可能修改了data,重新赋值会好。

在这里插入图片描述
这里继续调试就好了,上一节有做过分析笔记。

目前的操作都是内存中进行的,因为还没挂载到dom树上。
在这里插入图片描述

在这里插入图片描述
返回vnode后执行插入方法。
在这里插入图片描述
再往后就是用户传入的模块hook了

addVnodes和removeVnodes

批量添加和移除对应的vnoodes对应dom元素
在这里插入图片描述
分析内部实现 1 父级,2旧节点,3开始节点位置4结束节点位置。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

真正删除的函数,这里的listeners代表remove函数的个数+1,也就是remove都执行完后执行在这里插入图片描述


addvnodes
把vnode对应元素插入before之前 ,vnodes是添加的节点,后面是开始和结束索引,可以指定vnode中的那些节点插入到parentElm中,最后是insertedVnodeQueue,存储刚刚创建有insert钩子函数的vnode节点。
在这里插入图片描述

patchVnode

在这里插入图片描述
对比新旧vnode
第一部分,触发俩个函数 prepatch 和update
在这里插入图片描述
第二部分 if开始, 真正对比俩个vnode差异的地方,当找到差异地方后立即更新dom
在这里插入图片描述
第三部分,最后触发postpatch
在这里插入图片描述

  • 第一过程分析:
    1, 获取prepatch后立即执行,是对比俩个vnode前执行的。
    2,定义三个属性,把旧节点的elm赋值给新节点的elm(为啥??),后分别获取新旧节点的子节点。
    3,判断新旧节点是否相同节点,如果是就没必要比较,return
    4,如果不是,循环执行cbs中的update,然后获取用户传入的date中的update执行,后执行是因为用户修改vnode的数据data后可以覆盖模块中update的数据。
    总结:这里核心是判断俩节点是否相同,相同就返回,这里不管节点是否相同一定会触发prepatch函数,只有不相同的时候才触发update函数
    在这里插入图片描述
  • 第二过程分析:
    1,如果没有text属性,会继续比较子节点(text和children是互斥的),先看else里的,if分支比较复杂。
    2,老节点text有值并且不等于新的text(一样的话就不做任何dom操作),进一步判断是否有子节点,如果老节点有就删除,没有就直接插入,注意这里数据变化后,没有创建新dom元素,重用之前的dom只是设置了text。在这里插入图片描述
    3,分析刚刚跳过的if分支代码,判断新旧是否有子节点,有并且,不相同调用updatechildren,这里代码最麻烦最后再看。
    4第二个分支中判断新节点是否有子节点,在判断老节点是否有text,有就清空,然后调用addvnodes添加新旧节点。
    5,第三判断是否老节点有没有,有就移除。
    6,第四判断,老节点是否有text,有就清空对应elm的text。
    总结:这里的核心就是判断新老节点和文本属性的差异,然后更新真实dom。
    在这里插入图片描述

最后第三部分,触发postpatch,当对比更新dom完成会触发用户传递的钩子。此函数可以获取最新dom数据。类似vue的updated函数。到此更新完毕。
在这里插入图片描述
补充:
在这里插入图片描述

调试patchVnode

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那么会转换成vnode
在这里插入图片描述
然后通过俩个节点的key判断是否相同节点
在这里插入图片描述
首次渲染肯定进入else分支,转换vnode成真实dom渲染
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
渲染dom到页面
在这里插入图片描述
在这里插入图片描述
f8到下一个断点
在这里插入图片描述
f11进入函数内部
在这里插入图片描述
此时key是一样的 都是undefined
进入patchvnode
在这里插入图片描述
获取新老节点,判断内存地址是否相同
在这里插入图片描述
在这里插入图片描述
**总结:**patchvonode执行过程就是判断新的虚拟节点是否发生了变化,如果text属性和字节点变化了才更新dom。
下面介绍如果子节点变化的情况

updateChildren整体分析

为什么用diff算法?
操作dom是很耗性能的,需要重排重绘,例如大量数据的列表,如果操作dom会重新渲染列表,但用虚拟dom时,当数据变化后不直接操作dom,只会JS对象描述真实dom,数据变化后作对比,然后找到比变化的位置,从而提高性能。

比较笨的方法,对比每一个节点
在这里插入图片描述
snabdom中,只比较同级,不同就直接删除创建。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 1.新开和旧开
    1.用sameVnode判断是否同节点
    2.是就patchVnode继续对比内部差异
    3.更新到真实dom

在这里插入图片描述
4.移动索引对比第二个节点
把第二个节点作为开始节点比较,如果开始节点不是sameVnode会从后往前比较,比较旧的结束节点和新的结束节点是否sameVnode。
在这里插入图片描述
如果是调用patchVnode,比较完成后移动索引(到倒数第二个),作为结束节点,继续调用sameVnode,是相同就patchVnode比较差异更新dom,(注意这里是sameVnode的话会重用之前就节点的dom元素,patchVnode中会对比差异然后把差异更新到重用的dom上),如果text或childre相同是不会dom操作的,不相同只需更旧dom上即可不会新创建,这就是虚拟dom如何提高效率的核心(如果sameVnode就重用旧的dom)。
在这里插入图片描述
前俩种对比就说完了。

  • 2 第三种情况-旧开始节点和新结束节点
    在这里插入图片描述
    1,sameVnode,是就触发patchVnode继续对比,内部差异更新完成后,把旧的元素移动到最后(旧的最后),因为这里旧开始和新结束相同,
    在这里插入图片描述
    移动完后更新索引,移动到旧2的位置,新的会移动到前面(新5)
    在这里插入图片描述
    这是第三种情况,和前俩种不一样的是,这里会移动元素。

  • 3.第四种情况
    旧结束节点和新开始结束节点的比较
    和刚刚差不多,sameVnode,如果是patchVnode,更新完后把旧移动到旧的最前面
    在这里插入图片描述
    如果上述四种都不满足,说明开始和结束节点都不相同,
    这个时候就在旧数组中依次查找是否有相同的新节点
    过程:首先遍历新开始节点,在旧数组中查找是否有相同key值的旧节点,如果没有找到,就说明是新节点,此时要创建新的dom元素,并插入到旧的最前面
    在这里插入图片描述
    如果找到具有相同的key值节点,判断sel是否相同,不相同就新建dom,插入到最前面的位置,情况2,如果sel和key相同,找到的这个相同节点会赋值给elmToMove这个变量,然后调用patchVnode对比差异更新后放入最前
    在这里插入图片描述
    到这里整个同级别的比较就结束了。
    循环结束后的情况在这里插入图片描述
    情况1.老节点个数少于新节点个数,老节点先遍历完,新节点剩余()
    情况2.老的大于新的,老节点剩余

情况1.答案
在这里插入图片描述
比较过程
先比较旧和新开始节点,如果相同,后移继续比较
在这里插入图片描述
3不相同后开始节点的比较结束了,开始比较结束节点,如果相同就索引往前移动,
在这里插入图片描述
如果不相同,结束节点比较结束,进行其他操作
发现新节点中有三个剩余节点,那么这些剩余的就是需要新增的dom元素,最后插入老节点元素数组中的末尾
在这里插入图片描述
那么这种情况旧开始索引大于旧结束索引,第一种情况看完了。

  • 情况2答案
    在这里插入图片描述
    老节点个数大于新节点个数
    过程: 还是差不多,先比较开始节点,相同就后移索引,
    在这里插入图片描述

如果不同(4开始不同)就结束开始节点的比较,开始比较结束节点
在这里插入图片描述
先拿到旧节点的结束节点 然后再拿新节点的结束节点
在这里插入图片描述
如果相同,索引往前,不相同就停止比较结束节点。
最后执行其他操作,此时老节点剩余俩个节点,这俩个节点在新节点数组中不存在,所以这俩个对应的dom需要从dom树移除,这种情况新节点开始的索引大于旧节点开始的索引

updateChildren

源码分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 第一部分,定义变量
    在这里插入图片描述
  • 第二部分,循环
    对比同级,循环条件是都没有对比完成
    在这里插入图片描述
    1先判断新旧节点开始的值是否为null,如果是相同节点处理完后,会对新旧节点重新赋值,此时赋值可能会null
    在这里插入图片描述
    判断完后会进行四种情况的判断

    如果不相同,会对比旧的结束和新的开始,然后把旧的elm移动到旧的elm后面,然后再改变对应索引
    在这里插入图片描述

如果这俩不是相同节点,那么会比较旧结束和新开始,如果一样就把旧的elm结束移动到旧的开始节点之前,然后改变索引
在这里插入图片描述
到这 四种情况就结束了


遍历新节点,用新节点key,在老节点中找到相同key的节点,如果找到了就移动对应dom到合适的位置。

首先判断是否是undefined,这个对象存储老节点的key,值是老节点的索引,根据key找索引,在这里插入图片描述

没有就初始化(创建方法循环老节点,拿到对应key作为键,索引作为值,返回map)

在这里插入图片描述
拿到新节点的key去找旧节点index存到idxInOld变量中,然后判断是否存在(因为新的节点在老的节点中不存在例如新插入的值可能 有不存在的情况),在这里插入图片描述
所以会判断是否有值,没有就代表没有对应元素,创建对应dom插入到旧的开始前。新的节点处理完后,新的索引++,获取下一个作为开始节点。
在这里插入图片描述
上面是找不到的情况。

找到的话,取老节点赋值给elmToMove,2比较sel如果不一样,创建新元素插入老节点开始前
在这里插入图片描述
sel相同的情况,触发patchVnode对比更新,然后把老节点对应索引设置为undefined,因为这个节点被处理过了,然后移动老dom元素到老dom开始之前,最后又继续索引++
在这里插入图片描述
到此对比同级节点就结束了。


收尾工作,确保新旧节点个数相同都比较完了,如果其中一个为true那么代表某数据没处理完。
情况1,老节点先完成。在这里插入图片描述
在这里插入图片描述
情况2,新节点先完成的情况。
参数34记录剩余节点的位置
在这里插入图片描述
那么到这里updateChildren就看完了,这就是diff的核心

调试updateChildren

在这里插入图片描述

断点
在这里插入图片描述
f11继续断点
在这里插入图片描述
此时的key是undefined sel都是ul,那么sameVnode就认为是同节点。
进入patchVnode
在这里插入图片描述
在这里插入图片描述
对比新旧节点是否有值,有值进入下一个分支对比是否相同节点,因为新节点是新建的,此时不相等触发updatechildren
在这里插入图片描述
进入updatechildren
在这里插入图片描述
while这里判断是否为null,首次可以跳过,因为可以获取到值,只有后面赋值的时候可能会出现null的情况,所以这里断点到第一次调用sameVnode位置
在这里插入图片描述
在这里插入图片描述
此时新旧开始节点的sel和key都是相同的,f10进入if分支,触发patchVnode对比差异,并更新dom,进入patchVnode

对比第一个子节点 打个断点
在这里插入图片描述
此时vnode.text有值,进入 else if 分支
在这里插入图片描述
进入elseif后判断旧节点text和新的是否相同
在这里插入图片描述
此时都是相同的 不会执行任何操作。
patchV对比结束,触发postPatch
在这里插入图片描述


继续回到(执行完后)patchVnode 获取下一个节点 ++index,此时第二个节点作为了下次循环的开始节点。
在这里插入图片描述
继续跳到设置的断点 sameVnode中
在这里插入图片描述
在这里插入图片描述
相同进入patchVnode,有值进入elseif,此时值不相等,判断oldch,此时为undefined,更新dom(注意setTextContent执行完后页面才更新数据)
在这里插入图片描述
执行postpatch 执行完后又回到patchVnode 索引++
在这里插入图片描述
此时指向第三个节点,第三节点作为开始节点循环。
下次循环又进入sameVnode判断
在这里插入图片描述
在这里插入图片描述
和刚才一样


此时新老节点的个数是相同的,updatechildren执行结束
在这里插入图片描述
调试结束
以上是没有设置key的情况

调试带key的情况

有key的相同的情况会交互,不会重新创建节点,例如 key是 旧b 和新b 虽然顺序对应不上,但遇到key一样的会对换位置。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后updatechildren内 isUndef打断点
在这里插入图片描述
旧节点
在这里插入图片描述
新节点
在这里插入图片描述
进入下一个循环,点击下一个断点直接到sameVnode


不同的情况
在这里插入图片描述

在这里插入图片描述
注意,因为这里key不一样,这里有和不设置key不一样的地方,sameVnode失败 进入下一个sameVnode 比较老结束和新开始是否相同
在这里插入图片描述
此时老是c 新是b,不相同
继续进入第三个分支 老开始和新结束对比
在这里插入图片描述
在这里插入图片描述
此时相同进入patchVnode,进入下一轮updatechildren,此时只有text有,而且相同 不做任何操作
这就是不设置key不一样的地方,如果没有key那么key都相同,会重新设置文本内容
对比完跳出patchVnode,执行api插入老开始节点对应的dom元素插到老结束节点后
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实时更新,执行代码完后立即更新,老节点上修改 。


第三轮
在这里插入图片描述
在这里插入图片描述
key和text都一样 不做任何操作。
收尾工作,因为新老节点个数都相同不做任何工作。

Key 的意义

vue中的key和dsnabom中的是一样的,都是在vnode中比较是否相同节点,不设置key最大程度重用元素(有问题)
问题演示
在这里插入图片描述
不带key
在这里插入图片描述

在这里插入图片描述
这里出问题了选中的是1单更新后变成100
updatechildren对比:新开和旧开都一样所以重用了,继续对比text发现不一样更新text
(没有key或者key一样的时候都有这种错误)


带key情况
在这里插入图片描述
问题解决
在这里插入图片描述
updatechildren对比新旧节点,发现key不一样老是1 新是100,key不同重新创建li
总之给相同父元素的子元素添加唯一key 不然渲染错误

todo 调试这段代码

snabdom了解即可,为后续vue源码做准备。


注意 这里新节点 视频位置换了一下,单key相同
在这里插入图片描述
在这里插入图片描述
进入patchVnode
在这里插入图片描述
判断是否定义,判断内存地址是否一样,此时不一样进入updateChildren
在这里插入图片描述
这里新旧节点的key是相同的,节点相同进入patchVnode
在这里插入图片描述
发现这里的过程和之前没设置key是一样的。
索引++对比下一个节点

在这里插入图片描述
在这里插入图片描述
注意此时执行不一样了,不是相同节点了,不进入patchVnode

对比老的结束节点和新的结束节点
在这里插入图片描述
这里会一直对比到最后一个节点
key都不相同

继续对比
在这里插入图片描述
在这里插入图片描述
这里的key是相同的,进入patchVnode
在这里插入图片描述
判断是否有值后 对比新旧的text,这里相同 不做任何事情 结束。

在这里插入图片描述
执行完了后交换了位置
在这里插入图片描述
在这里插入图片描述
这里把老的开始节点 移动到了旧的结束节点。

继续指向下一个元素
在这里插入图片描述
旧的开始节点 微博 和新的开始节点 微博 他们的key是一样的 进入patchVnode,不更新dom,所以性能比之前好。

所有节点对比完毕。


另一种带key的位置优化情况
当新旧开始节点 四种情况都不满足的时候

使用新开始节点的key 在老节点数组中获取对应节点的索引,如何取得对应老节点
在这里插入图片描述
如果找到对应的老节点后
在这里插入图片描述
而且老节点的sel是相同的,执行patchVnode后 进行移动,不会调用createElm方法创建 对应对应元素,减少dom渲染过程在这里插入图片描述
所以这里也是key性能提升的位置
这里和vue的for是一样的 带key的情况,vue内部就是使用了这段代码优化的,去避免重复渲染相同项

模块源码

在这里插入图片描述
在这里插入图片描述

定义接口
在这里插入图片描述

initHOOk后create触发时可以给dom注册样式或事件

modules

导出钩子
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面是新节点情况

看看旧节点的属性情况
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值