React16源码: React中commitAllHostEffects内部的commitWork的源码实现

【论文精读】Formal Mathematical Reasoning: A New Frontier in AI 注:本文根据师兄汇报内容总结。 阅读详情

commitWork


1 )概述

  • 在 react commit 阶段的 commitRoot 第二个while循环中
  • 调用了 commitAllHostEffects,这个函数不仅仅处理了新增节点
  • 若一个节点已经存在,当它有新的内容要更新或者是它的attributes要更新
  • 这个时候,就需要调用 commitWork

2 )源码

定位到 packages/react-reconciler/src/ReactFiberCommitWork.js#L1033

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  if (!supportsMutation) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case MemoComponent:
      case SimpleMemoComponent: {
        commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
        return;
      }
    }

    commitContainer(finishedWork);
    return;
  }

  // 首先,根据不同的tag类型来执行不同的操作
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
      return;
    }
    // ClassComponent 不在这里的执行范围内,所以它就return了
    case ClassComponent: {
      return;
    }
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      // // 如果instance不等于null,就代表 instance 对应的节点已经创建了
      if (instance != null) {
        // Commit the work prepared earlier.
        const newProps = finishedWork.memoizedProps;
        // For hydration we reuse the update path but we treat the oldProps
        // as the newProps. The updatePayload will contain the real change in
        // this case.
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // TODO: Type the updateQueue to be specific to host components.
        // 获取了一个 updatePayload 等于 finishedWork.updateQueue
        // 在 completeWork 的时候,对于 HostComponent 进行了新老 props 的一个对比
        // 这个对比的结果就是返回一个叫做 updatePayload 这么一个数组
        // 这个数组是以2为单位,就是说一个key一个value, 它的第一位是key,第二位是value,然后这样的形式一个一个往后面堆叠
        // 拿到这么一个 updatePayload 之后,我们就知道在commit阶段要去执行怎么样的一个更新操作
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        finishedWork.updateQueue = null;
        // 存在 updatePayload 则执行 commitUpdate
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }
    case HostText: {
      invariant(
        finishedWork.stateNode !== null,
        'This should have a text node initialized. This error is likely ' +
          'caused by a bug in React. Please file an issue.',
      );
      const textInstance: TextInstance = finishedWork.stateNode;
      const newText: string = finishedWork.memoizedProps;
      // For hydration we reuse the update path but we treat the oldProps
      // as the newProps. The updatePayload will contain the real change in
      // this case.
      const oldText: string =
        current !== null ? current.memoizedProps : newText;
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }
    case HostRoot: {
      return;
    }
    case Profiler: {
      return;
    }
    case SuspenseComponent: {
      let newState: SuspenseState | null = finishedWork.memoizedState;

      let newDidTimeout;
      let primaryChildParent = finishedWork;
      if (newState === null) {
        newDidTimeout = false;
      } else {
        newDidTimeout = true;
        primaryChildParent = finishedWork.child;
        if (newState.timedOutAt === NoWork) {
          // If the children had not already timed out, record the time.
          // This is used to compute the elapsed time during subsequent
          // attempts to render the children.
          newState.timedOutAt = requestCurrentTime();
        }
      }

      if (primaryChildParent !== null) {
        hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
      }
      return;
    }
    case IncompleteClassComponent: {
      return;
    }
    default: {
      invariant(
        false,
        'This unit of work tag should not have side-effects. This error is ' +
          'likely caused by a bug in React. Please file an issue.',
      );
    }
  }
}
  • 进入 commitUpdate

    // packages/react-dom/src/client/ReactDOMHostConfig.js#L324
    export function commitUpdate(
      domElement: Instance,
      updatePayload: Array<mixed>,
      type: string,
      oldProps: Props,
      newProps: Props,
      internalInstanceHandle: Object,
    ): void {
      // Update the props handle so that we know which props are the ones with
      // with current event handlers.
      updateFiberProps(domElement, newProps); // 将新的props挂载到新的属性上
      // Apply the diff to the DOM node.
      updateProperties(domElement, updatePayload, type, oldProps, newProps);
    }
    
    // packages/react-dom/src/client/ReactDOMComponentTree.js#L85
    export function updateFiberProps(node, props) {
      node[internalEventHandlersKey] = props;
    }
    
    // packages/react-dom/src/client/ReactDOMComponent.js#L775
    // Apply the diff.
    export function updateProperties(
      domElement: Element,
      updatePayload: Array<any>,
      tag: string,
      lastRawProps: Object,
      nextRawProps: Object,
    ): void {
      // Update checked *before* name.
      // In the middle of an update, it is possible to have multiple checked.
      // When a checked radio tries to change name, browser makes another radio's checked false.
      // 像是 radio 可以设置 value 绑定来设置
      // 对于radio标签,设置通过 value 的绑定来进行是否选中的控制, 相当于是要把value转化成哪一个radio标签,它的checked属性是等于true的这么一种情况
      // 这是为什么当初在执行 diff property 的时候,判断如果这个标签是input,它的 updatePayload 直接等于了一个空数组
      // 也就是说,即便我们后续 updatePayload 的里面没有放任何的内容,但它返回的仍然是一个空数组,仍然要执行到我们这个 updateProperties 方法里面
      // 因为最终我们要去根据一些不同的情况来更新,像radio标签,checkbox 标签 的一个 checked 相关的一些状态
      if (
        tag === 'input' &&
        nextRawProps.type === 'radio' &&
        nextRawProps.name != null
      ) {
        ReactDOMInput.updateChecked(domElement, nextRawProps);
      }
    
      const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
      const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
      // Apply the diff.
      // 更新 dom 节点的 properties
      updateDOMProperties(
        domElement,
        updatePayload,
        wasCustomComponentTag,
        isCustomComponentTag,
      );
    
      // TODO: Ensure that an update gets scheduled if any of the special props
      // changed.
      // 表单标签的处理
      // 其实就是把这些 input, textarea, select 标签, 特殊的一些属性,一些 attribute 给它进行一个更新的一个过程
      switch (tag) {
        case 'input':
          // Update the wrapper around inputs *after* updating props. This has to
          // happen after `updateDOMProperties`. Otherwise HTML5 input validations
          // raise warnings and prevent the new value from being assigned.
          ReactDOMInput.updateWrapper(domElement, nextRawProps);
          break;
        case 'textarea':
          ReactDOMTextarea.updateWrapper(domElement, nextRawProps);
          break;
        case 'select':
          // <select> value update needs to occur after <option> children
          // reconciliation
          ReactDOMSelect.postUpdateWrapper(domElement, nextRawProps);
          break;
      }
    }
    
    // packages/react-dom/src/client/ReactDOMInput.js#L128
    export function updateChecked(element: Element, props: Object) {
      const node = ((element: any): InputWithWrapperState);
      const checked = props.checked;
      if (checked != null) {
        // dom操作
        DOMPropertyOperations.setValueForProperty(node, 'checked', checked, false);
      }
    }
    
    // packages/react-dom/src/shared/isCustomComponent.js#L10
    function isCustomComponent(tagName: string, props: Object) {
      // 它根据标签名字上,是否有 - 的存在, 并且 props.is 是 string 则是
      if (tagName.indexOf('-') === -1) {
        return typeof props.is === 'string';
      }
      // 有 - 的,屏蔽自有的一些,排除之后其他都算 自定义
      switch (tagName) {
        // These are reserved SVG and MathML elements.
        // We don't mind this whitelist too much because we expect it to never grow.
        // The alternative is to track the namespace in a few places which is convoluted.
        // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
        case 'annotation-xml':
        case 'color-profile':
        case 'font-face':
        case 'font-face-src':
        case 'font-face-uri':
        case 'font-face-format':
        case 'font-face-name':
        case 'missing-glyph':
          return false;
        default:
          return true;
      }
    }
    
    // packages/react-dom/src/client/ReactDOMComponent.js#L326
    function updateDOMProperties(
      domElement: Element,
      updatePayload: Array<any>,
      wasCustomComponentTag: boolean,
      isCustomComponentTag: boolean,
    ): void {
      // TODO: Handle wasCustomComponentTag
      // 遍历这个updatepaylow的这个数组,然后记住这边的下标增加是每次加2,而不是加1了
      for (let i = 0; i < updatePayload.length; i += 2) {
        // i 对应的就是 propKey, 然后i加1对应的就是 propValue
        const propKey = updatePayload[i];
        const propValue = updatePayload[i + 1];
        // 如果是style,会对每一个style的属性来执行
        if (propKey === STYLE) {
          // 里面会遍历这个style对象里面的每一个style属性,然后去设置对应的值
          // 我们拿到一个对象,最终如何给它转换成真正的CSS属性给它设置上去的一个过程
          // 其中还要考虑一些属性是删除的操作
          CSSPropertyOperations.setValueForStyles(domElement, propValue);
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
          setInnerHTML(domElement, propValue);
        } else if (propKey === CHILDREN) {
          setTextContent(domElement, propValue);
        } else {
          // 对于其他属性的处理
          DOMPropertyOperations.setValueForProperty(
            domElement,
            propKey,
            propValue,
            isCustomComponentTag,
          );
        }
      }
    }
    // packages/react-dom/src/client/ReactDOMHostConfig.js#L343
    export function commitTextUpdate(
      textInstance: TextInstance,
      oldText: string,
      newText: string,
    ): void {
      textInstance.nodeValue = newText;
    }
    
  • 对于 HostComponent 来说

    • 因为更新的过程,有一大部分是放在 completeUnitOfWork 里面去做了
    • 在 completeWork 的时候会获得这个 updatePayload
    • 这个 payload 已经告诉我们具体要做哪些属性的操作了
    • 它里面也包含了我们有一些 attribute 要删除的情况
    • 就是说它的 value 为 null 的情况, 标示着这个属性是要被删除的
    • 所以, 那一部分工作做完了之后,到这里其实已经比较的简单了
    • 只需要根据 updatePayload 里面相关的内容进行一个整体的更新就可以了
  • 对于 HostText 它也要进行一个更新,它调用了一个 commitTextUpdate

  • 以上就是整个 commitWork 里面做的事情, 具体的细节写在代码注释里

项目报告:基于Sentaurus TCAD的多沟道长度NMOS器件仿真、SPICE模型提取与环形振荡器电路验证 本项目基于Sentaurus TCAD工具,完成了从器件仿真到电路验证的全流程研究。首先构建了45nm、65nm和90nm三种沟道长度的NMOS器件,通过TCAD仿真分析了其电学特性,观察到明显的短沟道效应。随后提取了BSIM4 SPICE模型参数,并生成Hspice可用的模型文件。最后利用该模型设计了11级环形振荡器进行电路验证,证明了模型的实用性。研究结果表明,随着沟道长度缩短,器件性能虽有所提升,但短沟道效应导致关态电流显著增加,影响整体能效。本项目为纳米尺度MOSFET的建模与电路设计提供了完整的技 阅读详情

相关推荐

数控弯管机xyz转换ybc的算法_数控弯管机技术的更新和应用.pdf

您所在位置:网站首页 > 海量文档&nbsp>&nbsp汽车/机械/制造&nbsp>&nbsp机械/模具设计数控弯管机技术的更新和应用.pdf3页本文档一共被下载:次,您可全文免费在线阅读后下载本文档。 下载提示1.本站不保证该用户上传的文档完整性,不预览、不比对内容而直接下载产生的反悔问题本站不予受理。2.该文档所...

weixin_31493101的博客 1523

Fix signatures do not match the previously installed version

报错信息FAILURE: Build failed with an exception.* What went wrong: Execution failed for task ':app:installDebug'. > com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: Fai

Wang的专栏 2万+

EastWave应用案例:机箱屏蔽效能仿真

如何使用EastWave对机箱的电磁屏蔽效能进行仿真

2201_76034687的博客 1395

在react中 实现 base64的图片转换成formdata格式并实现上传功能

如果不想要服务端实现处理base64格式的图片的转换,那么在浏览器端来实现,部分属于伪代码,仅作为开发参考,请勿直接拷贝使用

Wang的专栏 8836

告别Qt Creator?手把手教你用VSCode+MinGW调试QT项目(附完整launch.json配置)

本文详细介绍了如何在VSCode中配置完整的QT开发环境,特别是针对使用MinGW编译器的用户。从环境搭建到调试配置,包括生成.pro文件、Makefile,以及配置task.json和launch.json文件,帮助开发者高效调试QT项目。

weixin_27180827的博客 187

智能体在车联网中的应用:第9天 核心工具链与仿真世界:SUMO交通仿真入门——从安装到构建你的第一个虚拟十字路口

摘要 SUMO是一款开源微观交通仿真工具,为车联网和自动驾驶算法开发提供高效安全的测试环境。文章详细介绍了SUMO的核心概念、安装方法及实战应用。在Ubuntu或Windows系统安装SUMO后,可通过netedit工具构建十字路口路网,包括创建节点、连接边、设置交通信号灯等步骤。随后通过编写.rou.xml文件定义车辆类型、路线和车流,实现动态交通场景模拟。SUMO支持与外部程序交互,能复现真实场景、进行极端测试和算法验证,是车联网研究的理想数字沙盘。

IT深耕十余载,大道之简 1455

快速解决gradle下载慢的问题的小技巧

在ionic run 或build 或clean命令的时候,如果没有下载过gradle, 需要重新下载一次,而通过CLI的方式下载很慢,即使科学上网后,下载还是存在一些问题。这里总结一下,快速的下载安装方式。 步骤 本地安装 http-server 包, 通过 $ npm i -g http-server 的方式安装到全局 在官网上下载自己的版本,目前自带的版本是 2.14.1,其他版本可能在执行过

Wang的专栏 6714

仿真学习系列(四十九):Sigrity仿真之电容库制作

在高速电路设计中,电源完整性(PI)是确保系统稳定、可靠运行的关键因素。随着电子产品对性能的要求不断提升,PDN(电源分配网络)的设计与优化变得尤为重要。为了确保电源网络在高速工作状态下不会引发电压波动和信号干扰,我们需要在设计中精确地选择电容器进行优化,而电容库的制作则是这一过程的核心。 OptimizePI作为一个强大的电源完整性优化工具,能够帮助设计师根据不同的目标,批量进行PDN仿真,以得出最佳电容选择。

案例文章,高速信号 228

React 实现 2D 画板功能

在移动端或者PC端我们来模拟画板的实现, 支持清除和导出功能,此处简单实现,仅作开发参考。 效果演示 主要代码演示 /* 此模块用于实现签名绘图功能 */ // 需要用到的变量定义 let clickX = new Array(); let clickY = new Array(); let clickDrag = new Array(); let paint; let point...

Wang的专栏 4325

机械式激光雷达实现原理以及特性总结

本文介绍了机械式激光雷达的技术原理和核心特性。该技术通过机械旋转结构实现360°三维扫描,采用飞行时间法测距并生成点云数据。核心优势包括全向扫描、长距高精度和成熟度高,但存在体积大、成本高、可靠性受限等局限。典型应用于自动驾驶测试车、测绘建模等领域。与固态激光雷达相比,机械式在视场角和探测距离方面有优势,但在体积、成本和可靠性方面处于劣势。该技术目前主要应用于对成本不敏感的专业场景。

start_up_go的博客 263

制作你自己的yolov5数据集并进行训练

项目地址:make-your-yolov5_dataset 一、数据集标注软件 labelImg labelMe 精灵标注助手 更多的标注工具你可以去看:深度学习图像标注工具汇总 、 十个最常用深度学习图像/视频数据标注工具 、 百度一下 在这里我选择使用labelImg 二、数据集标注 下载labelImg 解压,直接打开labelimg.exe labelImg标注数据 标注完成后即可获得VOC格式(生成.xml文件)的数据 目录结构: dataset │ ├─Annotations .

年少的勇气已经用完,剩下的就是三思而后行 1万+

React16源码: React中LegacyContext的源码实现

对于在 beginWork 中的 context 操作,可以看上述判断成立的条件下的代码,即便有符合跳过更新的操作,依然 需要 push 和 pop 操作。如果我们可以跳出组件的更新,也就是代表着当前这个 classComponent,它的state它的props都应该是没有任何变化的。对于子组件来说,它的所有子树所获取到的context肯定是经过子组件,和上层的父组件合并的 context 了, 也就是。所以,先 push 一个老的值进去再说, 然后到后面,如果发现这个组件它是要更新的,就调用这个。

Wang的专栏 2803

【数据集】米级别热红外卫星合集:SDGSAT-1、HotSat-1等

米级别热红外卫星合集:SDGSAT-1、HotSat-1等

WW、forever的博客 1259

Fix babel-preset-react-native TransformError

错误环境新版本存在不兼容,在运行 react-native run-android 的时候,发生的错误分析: babel-preset-react-native version : @3.0.0 $ react-native -v :react-native-cli: 2.0.1 react-native: 0.47.1$node -v : v7.8.0 $ npm -v :4.4.4 解决方案

Wang的专栏 2800

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

React18原理: 再聊Fiber架构下的时间分片

react的任务可以被打断,其实就是基于时间分片的,人眼最高能识别的帧数不超过30帧,电影的帧数差不多是在24,浏览器的帧率一般来说是60帧,也就是每秒60个画面, 平均一个画面大概是16.5毫秒左右,浏览器正常的工作流程是运算渲染,运算,渲染运算渲染,在浏览器里面一个运算,加上一个渲染就是一帧

Wang的专栏 2796

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

在react项目中修改webpack中本地服务器架设的目录

一个产品的上线往往会有很多的问题,现在就出现一个需求,我们是在服务器下的某个目录进行发布,而不是根服务器,服务端不想为一个项目配置ip地址,而在react项目中,默认是走的根路径,现在我们就开始修改一下它。 前提说明 如果我们需要修改手动配置webpack, 那么根据现在的版本, 我们只能执行 npm run eject, 之后就可以着手修改了。 webpack中与需求相关的...

Wang的专栏 2397

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

React18源码: React调度中的3种优先级类型和Lane的位运算

可以看得出来lane&-lane的结果是SyncLane,所以通过lane&-lane就能分离出最高优先级的任务。即A(cpu)>B(IO)>C(cpu).此时的需求需要将任务B从group中分离出来,先处理cpu任务A和C。在新版本的React中,还有一个新特性,就是render阶段可能被中断,在这个期间会产生一个更高优先级的任务。如上就是通过 lanes & -lanes 分离出最高优先级的任务的,我们来看一下具体的流程。也即是说lane的代表的数值越小,此次更新的优先级就越大。

Wang的专栏 2394

React18原理: 核心包结构与两大工作循环

react中的工作循环,这是它的一个最顶层的一个流程图,在这张图里面,有两个核心的大循环,第一大循环是 react-reconciler 提供的,它的核心是构建fiber树,生成任务,第二大循环是 scheduler 提供的, 它的核心是任务调度(处理任务的优先级),react的原理实际上就是两大工作循环

Wang的专栏 2356

React18原理: 生命周期中特别注意事项

生命周期就是一个组件从诞生到销毁的全过程(包含错误捕获,这里暂且不聊这个)react 在组件的生命周期中注册了一系列的钩子函数支持开发者在其中嵌入代码,并在适当的时机运行生命周期本质上就是组件中的钩子函数,主要有三个主要的钩子挂载更新卸载。

Wang的专栏 2244

React16源码: React中的beginWork的源码实现

定位到 packages/react-reconciler/src/ReactFiberBeginWork.js。整个文件 export 出去的只有 beginWork 方法。

Wang的专栏 2140

React16源码: React中的reconcileChildren的源码实现

在更新了一个节点之后,拿到它的props.children, 要根据这个children里面的 ReactElement 来去创建子树的所有的 fiber 对象, 要根据 props.children 来生成 fiber 子树,然后判断 fiber 对象它是否是可以复用的

Wang的专栏 2133

React16源码: React中的completeUnitOfWork的源码实现

各种不同类型组件的一个更新过程对应的是在执行 `performUnitOfWork` 里面 `beginWork` 阶段,它是去向下遍历一棵 fiber 树的一侧的子节点,然后遍历到叶子节点为止,以及 return 自己 child 的这种方式

Wang的专栏 2088

React18原理: 渲染与更新时的重点关注事项

react 在渲染过程中要做很多事情,所以不可能直接通过初始元素直接渲染还需要一个东西,就是虚拟节点,暂不涉及React Fiber的概念,将vDom树和Fiber 树统称为虚拟节点有了初始元素后,React 就会根据初始元素和其他可以生成虚拟节点的东西生成虚拟节点React一定是通过虚拟节点来进行渲染的对于首次挂载阶段需要了解React的渲染流程通过书写的初始元素和一些其他可以生成虚拟节点的东西来生成虚拟节点然后针对不同的节点类型去做不同的事情,最终将真实DOM挂载到页面上。

Wang的专栏 2069

21st.dev 上手教程:12000+ React 组件库怎么用,6 步从挑样式到接进 Cursor

21st.dev 收录 12000+ 个 React + Tailwind 组件、297 套整站模板与 979 套主题。本文是手把手流程版:按页面用途找组件、用复制或 shadcn CLI 落地、接入 MCP 让 Cursor 自己取代码、用整站模板开局,附 10 张真实组件截图和 4 个常见坑。

懿轩的技术小窝 233

成为全栈·Vue 管理后台篇·Naive UI 主题对齐:React 后台配色与明暗模式

本文根据 React 管理后台的蓝紫主色与浅/深色语义令牌,分析 Vue 后台如何用 CSS custom properties、Naive UI `NConfigProvider`、theme overrides、Pinia UI store 和系统媒体查询完成主题对齐。文章说明 CSS class 与 Naive UI darkTheme 必须同步,Markdown 编辑器/预览需要单独接入 resolvedTheme,并通过主题/侧栏持久化区分非敏感偏好与认证凭证。

FungLeo's Blog 273

手搓 AI Agent 02:让模型第一次“动手“——Agent Loop 与 ReAct

本系列第2篇实现Agent核心循环——ReAct模式,让模型从“被动回答”变为“主动执行”。通过LLMClient工具调用能力,构建think → act → observe闭环,首次实现模型自主调用计算器、读取结果并继续推理。代码支持安全算术、步数限制与错误隔离,升级终端REPL为可观察的智能体交互界面。每步操作清晰可见,为后续多工具、规划与协作打下基础。

快乐来自程序里 131
上一篇: React16源码: React中commitAllHostEffects内部的commitPlacement的源码实现
下一篇: React16源码: React中commitAllHostEffects内部的commitDeletion的源码实现
Wang's Blog
博客等级 码龄12年 7438粉丝 2421原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

Wang's Blog

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

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

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

打赏作者

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

抵扣说明:

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

余额充值