翼度科技»论坛 编程开发 JavaScript 查看内容

一文了解Vue 3 的 generate 是这样生成 render&n

9

主题

9

帖子

27

积分

新手上路

Rank: 1

积分
27
前言

在之前的 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?文章中讲了
  1. transform
复制代码
阶段处理完v-for、v-model等指令后,会生成一棵javascript AST抽象语法树。这篇文章我们来接着讲
  1. generate
复制代码
阶段是如何根据这棵javascript AST抽象语法树生成render函数字符串的,本文中使用的vue版本为
  1. 3.4.19
复制代码


看个demo

还是一样的套路,我们通过debug一个demo来搞清楚render函数字符串是如何生成的。demo代码如下:
  1. <template>
  2.   <p>{{ msg }}</p>
  3. </template>
  4. <script setup lang="ts">
  5. import { ref } from "vue";
  6. const msg = ref("hello world");
  7. </script>
复制代码
上面这个demo很简单,使用p标签渲染一个msg响应式变量,变量的值为"hello world"。我们在浏览器中来看看这个demo生成的render函数是什么样的,代码如下:
  1. import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
  2. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  3.   return _openBlock(), _createElementBlock(
  4.     "p",
  5.     null,
  6.     _toDisplayString($setup.msg),
  7.     1
  8.     /* TEXT */
  9.   );
  10. }
复制代码
上面的render函数中使用了两个函数:
  1. openBlock
复制代码
  1. createElementBlock
复制代码
。在之前的vue3早已具备抛弃虚拟DOM的能力了文章中我们已经讲过了这两个函数:

    1. openBlock
    复制代码
    的作用为初始化一个全局变量
    1. currentBlock
    复制代码
    数组,用于收集dom树中的所有动态节点。
    1. createElementBlock
    复制代码
    的作用为生成根节点p标签的虚拟DOM,然后将收集到的动态节点数组
    1. currentBlock
    复制代码
    塞到根节点p标签的
    1. dynamicChildren
    复制代码
    属性上。
render函数的生成其实很简单,经过
  1. transform
复制代码
阶段处理后会生成一棵
  1. javascript AST抽象语法树
复制代码
,这棵树的结构和要生成的render函数结构是一模一样的。所以在
  1. generate
复制代码
函数中只需要递归遍历这棵树,进行字符串拼接就可以生成render函数啦!
加我微信heavenyjj0012回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
  1. generate
复制代码
函数
首先给
  1. generate
复制代码
函数打个断点,
  1. generate
复制代码
函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。
然后启动一个debug终端,在终端中执行
  1. yarn dev
复制代码
(这里是以vite举例)。在浏览器中访问 http://localhost:5173/ ,此时断点就会走到
  1. generate
复制代码
函数中了。在我们这个场景中简化后的
  1. generate
复制代码
函数是下面这样的:
  1. function generate(ast) {
  2.   const context = createCodegenContext();
  3.   const { push, indent, deindent } = context;
  4.   const preambleContext = context;
  5.   genModulePreamble(ast, preambleContext);
  6.   const functionName = `render`;
  7.   const args = ["_ctx", "_cache"];
  8.   args.push("$props", "$setup", "$data", "$options");
  9.   const signature = args.join(", ");
  10.   push(`function ${functionName}(${signature}) {`);
  11.   indent();
  12.   push(`return `);
  13.   genNode(ast.codegenNode, context);
  14.   deindent();
  15.   push(`}`);
  16.   return {
  17.     ast,
  18.     code: context.code,
  19.   };
  20. }
复制代码
  1. generate
复制代码
中主要分为四部分:

  • 生成
    1. context
    复制代码
    上下文对象。
  • 执行
    1. genModulePreamble
    复制代码
    函数生成:
    1. import { xxx } from "vue";
    复制代码
  • 生成render函数中的函数名称和参数,也就是
    1. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
    复制代码
  • 生成render函数中return的内容
  1. context
复制代码
上下文对象
  1. context
复制代码
上下文对象是执行
  1. createCodegenContext
复制代码
函数生成的,将断点走进
  1. createCodegenContext
复制代码
函数。简化后的代码如下:
  1. function createCodegenContext() {
  2.   const context = {
  3.     code: ``,
  4.     indentLevel: 0,
  5.     helper(key) {
  6.       return `_${helperNameMap[key]}`;
  7.     },
  8.     push(code) {
  9.       context.code += code;
  10.     },
  11.     indent() {
  12.       newline(++context.indentLevel);
  13.     },
  14.     deindent(withoutNewLine = false) {
  15.       if (withoutNewLine) {
  16.         --context.indentLevel;
  17.       } else {
  18.         newline(--context.indentLevel);
  19.       }
  20.     },
  21.     newline() {
  22.       newline(context.indentLevel);
  23.     },
  24.   };
  25.   function newline(n) {
  26.     context.push("\n" + `  `.repeat(n));
  27.   }
  28.   return context;
  29. }
复制代码
为了代码具有较强的可读性,我们一般都会使用换行和锁进。
  1. context
复制代码
上下文中的这些属性和方法作用就是为了生成具有较强可读性的render函数。
  1. code
复制代码
属性:当前生成的render函数字符串。

    1. indentLevel
    复制代码
    属性:当前的锁进级别,每个级别对应两个空格的锁进。
    1. helper
    复制代码
    方法:返回render函数中使用到的vue包中export导出的函数名称,比如返回
    1. openBlock
    复制代码
    1. createElementBlock
    复制代码
    等函数
    1. push
    复制代码
    方法:向当前的render函数字符串后插入字符串code。
    1. indent
    复制代码
    方法:插入换行符,并且增加一个锁进。
    1. deindent
    复制代码
    方法:减少一个锁进,或者插入一个换行符并且减少一个锁进。
    1. newline
    复制代码
    方法:插入换行符。
生成
  1. import {xxx} from "vue"
复制代码
我们接着来看
  1. generate
复制代码
函数中的第二部分,生成
  1. import {xxx} from "vue"
复制代码
。将断点走进
  1. genModulePreamble
复制代码
函数,在我们这个场景中简化后的
  1. genModulePreamble
复制代码
函数代码如下:
  1. function genModulePreamble(ast, context) {
  2.   const { push, newline, runtimeModuleName } = context;
  3.   if (ast.helpers.size) {
  4.     const helpers = Array.from(ast.helpers);
  5.     push(
  6.       `import { ${helpers
  7.         .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
  8.         .join(", ")} } from ${JSON.stringify(runtimeModuleName)}
  9. `,
  10.       -1 /* End */
  11.     );
  12.   }
  13.   genHoists(ast.hoists, context);
  14.   newline();
  15.   push(`export `);
  16. }
复制代码
其中的
  1. ast.helpers
复制代码
是在
  1. transform
复制代码
阶段收集的需要从vue中import导入的函数,无需将vue中所有的函数都import导入。在debug终端看看
  1. helpers
复制代码
数组中的值如下图:

从上图中可以看到需要从vue中import导入
  1. toDisplayString
复制代码
  1. openBlock
复制代码
  1. createElementBlock
复制代码
这三个函数。
在执行
  1. push
复制代码
方法之前我们先来看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到此时生成的render函数字符串还是一个空字符串,执行完push方法后,我们来看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到此时的render函数中已经有了
  1. import {xxx} from "vue"
复制代码
了。
这里执行的
  1. genHoists
复制代码
函数就是前面 搞懂 Vue 3 编译优化:静态提升的秘密文章中讲过的静态提升的入口。

生成render函数中的函数名称和参数

执行完
  1. genModulePreamble
复制代码
函数后,已经生成了一条
  1. import {xxx} from "vue"
复制代码
了。我们接着来看
  1. generate
复制代码
函数中render函数的函数名称和参数是如何生成的,代码如下:
  1. const functionName = `render`;
  2. const args = ["_ctx", "_cache"];
  3. args.push("$props", "$setup", "$data", "$options");
  4. const signature = args.join(", ");
  5. push(`function ${functionName}(${signature}) {`);
复制代码
上面的代码很简单,都是执行
  1. push
复制代码
方法向render函数中添加code字符串,其中
  1. args
复制代码
数组就是render函数中的参数。我们在来看看执行完上面这块代码后的render函数字符串是什么样的,如下图:

从上图中可以看到此时已经生成了render函数中的函数名称和参数了。

生成render函数中return的内容

接着来看
  1. generate
复制代码
函数中最后一块代码,如下:
  1. indent();
  2. push(`return `);
  3. genNode(ast.codegenNode, context);
复制代码
首先调用
  1. indent
复制代码
方法插入一个换行符并且增加一个锁进,然后执行
  1. push
复制代码
方法添加一个
  1. return
复制代码
字符串。
接着以根节点的
  1. codegenNode
复制代码
属性为参数执行
  1. genNode
复制代码
函数生成return中的内容,在我们这个场景中
  1. genNode
复制代码
函数简化后的代码如下:
  1. function genNode(node, context) {
  2.   switch (node.type) {
  3.     case NodeTypes.SIMPLE_EXPRESSION:
  4.       genExpression(node, context)
  5.       break
  6.     case NodeTypes.INTERPOLATION:
  7.       genInterpolation(node, context);
  8.       break;
  9.     case NodeTypes.VNODE_CALL:
  10.       genVNodeCall(node, context);
  11.       break;
  12.   }
  13. }
复制代码
这里涉及到
  1. SIMPLE_EXPRESSION
复制代码
  1. INTERPOLATION
复制代码
  1. VNODE_CALL
复制代码
三种AST抽象语法树node节点类型:

    1. INTERPOLATION
    复制代码
    :表示当前节点是双大括号节点,我们这个demo中就是:
    1. {{msg}}
    复制代码
    这个文本节点。
    1. SIMPLE_EXPRESSION
    复制代码
    :表示当前节点是简单表达式节点,在我们这个demo中就是双大括号节点
    1. {{msg}}
    复制代码
    中的更里层节点
    1. msg
    复制代码
    1. VNODE_CALL
    复制代码
    :表示当前节点是虚拟节点,比如我们这里第一次调用
    1. genNode
    复制代码
    函数传入的
    1. ast.codegenNode
    复制代码
    (根节点的
    1. codegenNode
    复制代码
    属性)就是虚拟节点。
  1. genVNodeCall
复制代码
函数
由于当前节点是虚拟节点,第一次进入
  1. genNode
复制代码
函数时会执行
  1. genVNodeCall
复制代码
函数。在我们这个场景中简化后的
  1. genVNodeCall
复制代码
函数代码如下:
  1. const OPEN_BLOCK = Symbol(`openBlock`);
  2. const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);
  3. function genVNodeCall(node, context) {
  4.   const { push, helper } = context;
  5.   const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
  6.   if (isBlock) {
  7.     push(`(${helper(OPEN_BLOCK)}(${``}), `);
  8.   }
  9.   const callHelper = CREATE_ELEMENT_BLOCK;
  10.   push(helper(callHelper) + `(`, -2 /* None */, node);
  11.   genNodeList(
  12.     // 将参数中的undefined转换成null
  13.     genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
  14.     context
  15.   );
  16.   push(`)`);
  17.   if (isBlock) {
  18.     push(`)`);
  19.   }
  20. }
复制代码
首先判断当前节点是不是block节点,由于此时的node为根节点,所以
  1. isBlock
复制代码
为true。将断点走进
  1. helper
复制代码
方法,我们来看看
  1. helper(OPEN_BLOCK)
复制代码
返回值是什么。
  1. helper
复制代码
方法的代码如下:
  1. const helperNameMap = {
  2.   [OPEN_BLOCK]: `openBlock`,
  3.   [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  4.   [TO_DISPLAY_STRING]: `toDisplayString`,
  5.   // ...省略
  6. };
  7. helper(key) {
  8.   return `_${helperNameMap[key]}`;
  9. }
复制代码
  1. helper
复制代码
方法中的代码很简单,这里的
  1. helper(OPEN_BLOCK)
复制代码
返回的就是
  1. _openBlock
复制代码

将断点走到第一个
  1. push
复制代码
方法,代码如下:
  1. push(`(${helper(OPEN_BLOCK)}(${``}), `);
复制代码
执行完这个
  1. push
复制代码
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到,此时render函数中增加了一个
  1. _openBlock
复制代码
函数的调用。
将断点走到第二个
  1. push
复制代码
方法,代码如下:
  1. const callHelper = CREATE_ELEMENT_BLOCK;
  2. push(helper(callHelper) + `(`, -2 /* None */, node);
复制代码
同理
  1. helper(callHelper)
复制代码
方法返回的是
  1. _createElementBlock
复制代码
,执行完这个
  1. push
复制代码
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到,此时render函数中增加了一个
  1. _createElementBlock
复制代码
函数的调用。
继续将断点走到
  1. genNodeList
复制代码
部分,代码如下:
  1. genNodeList(
  2.   genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
  3.   context
  4. );
复制代码
其中的
  1. genNullableArgs
复制代码
函数功能很简单,将参数中的
  1. undefined
复制代码
转换成
  1. null
复制代码
。比如此时的
  1. props
复制代码
就是
  1. undefined
复制代码
,经过
  1. genNullableArgs
复制代码
函数处理后传给
  1. genNodeList
复制代码
函数的
  1. props
复制代码
就是
  1. null
复制代码
  1. genNodeList
复制代码
函数
继续将断点走进
  1. genNodeList
复制代码
函数,在我们这个场景中简化后的代码如下:
  1. function genNodeList(nodes, context, multilines = false, comma = true) {
  2.   const { push } = context;
  3.   for (let i = 0; i < nodes.length; i++) {
  4.     const node = nodes[i];
  5.     if (shared.isString(node)) {
  6.       push(node);
  7.     } else {
  8.       genNode(node, context);
  9.     }
  10.     if (i < nodes.length - 1) {
  11.       comma && push(", ");
  12.     }
  13.   }
  14. }
复制代码
我们先来看看此时的
  1. nodes
复制代码
参数,如下图:

这里的
  1. nodes
复制代码
就是调用
  1. genNodeList
复制代码
函数时传的数组:
  1. [tag, props, children, patchFlag, dynamicProps]
复制代码
,只是将数组中的
  1. undefined
复制代码
转换成了
  1. null
复制代码


    1. nodes
    复制代码
    数组中的第一项为字符串p,表示当前节点是p标签。
  • 由于当前p标签没有props,所以第二项为null的字符串。
  • 第三项为p标签子节点:{{msg}}
  • 第四项也是一个字符串,标记当前节点是否是动态节点。
在讲
  1. genNodeList
复制代码
函数之前,我们先来看一下如何使用
  1. h
复制代码
函数生成一个
  1. <p>{{ msg }}</p>
复制代码
标签的虚拟DOM节点。根据vue官网的介绍,
  1. h
复制代码
函数定义如下:
  1. // 完整参数签名
  2. function h(
  3.   type: string | Component,
  4.   props?: object | null,
  5.   children?: Children | Slot | Slots
  6. ): VNode
复制代码
  1. h
复制代码
函数接收的第一个参数是标签名称或者一个组件,第二个参数是props对象或者null,第三个参数是子节点。
所以我们要使用
  1. h
复制代码
函数生成demo中的p标签虚拟DOM节点代码如下:
  1. h("p", null, msg)
复制代码
  1. h
复制代码
函数生成虚拟DOM实际就是调用的
  1. createBaseVNode
复制代码
函数,而我们这里的
  1. createElementBlock
复制代码
函数生成虚拟DOM也是调用的
  1. createBaseVNode
复制代码
函数。两者的区别是
  1. createElementBlock
复制代码
函数多接收一些参数,比如
  1. patchFlag
复制代码
  1. dynamicProps
复制代码

现在我想你应该已经反应过来了,为什么调用
  1. genNodeList
复制代码
函数时传入的第一个参数
  1. nodes
复制代码
为:
  1. [tag, props, children, patchFlag, dynamicProps]
复制代码
。这个数组的顺序就是调用
  1. createElementBlock
复制代码
函数时传入的参数顺序。
所以在
  1. genNodeList
复制代码
中会遍历
  1. nodes
复制代码
数组生成调用
  1. createElementBlock
复制代码
函数需要传入的参数。
先来看第一个参数
  1. tag
复制代码
,这里
  1. tag
复制代码
的值为字符串"p"。所以在for循环中会执行
  1. push(node)
复制代码
,生成调用
  1. createElementBlock
复制代码
函数的第一个参数"p"。在debug终端看看此时的render函数,如下图:

从上图中可以看到
  1. createElementBlock
复制代码
函数的第一个参数"p"
接着来看
  1. nodes
复制代码
数组中的第二个参数:
  1. props
复制代码
,由于p标签中没有
  1. props
复制代码
属性。所以第二个参数
  1. props
复制代码
的值为字符串"null",在for循环中同样会执行
  1. push(node)
复制代码
,生成调用
  1. createElementBlock
复制代码
函数的第二个参数"null"。在debug终端看看此时的render函数,如下图:

从上图中可以看到
  1. createElementBlock
复制代码
函数的第二个参数
  1. null
复制代码
接着来看
  1. nodes
复制代码
数组中的第三个参数:
  1. children
复制代码
,由于
  1. children
复制代码
是一个对象,所以以当前children节点作为参数执行
  1. genNode
复制代码
函数。
这个
  1. genNode
复制代码
函数前面已经执行过一次了,当时是以根节点的
  1. codegenNode
复制代码
属性作为参数执行的。回顾一下
  1. genNode
复制代码
函数的代码,如下:
  1. function genNode(node, context) {
  2.   switch (node.type) {
  3.     case NodeTypes.SIMPLE_EXPRESSION:
  4.       genExpression(node, context)
  5.       break
  6.     case NodeTypes.INTERPOLATION:
  7.       genInterpolation(node, context);
  8.       break;
  9.     case NodeTypes.VNODE_CALL:
  10.       genVNodeCall(node, context);
  11.       break;
  12.   }
  13. }
复制代码
前面我们讲过了
  1. NodeTypes.INTERPOLATION
复制代码
类型表示当前节点是双大括号节点,而我们这次执行
  1. genNode
复制代码
函数传入的p标签children,刚好就是{{msg}}双大括号节点。所以代码会走到
  1. genInterpolation
复制代码
函数中。
  1. genInterpolation
复制代码
函数
将断点走进
  1. genInterpolation
复制代码
函数中,
  1. genInterpolation
复制代码
代码如下:
  1. function genInterpolation(node, context) {
  2.   const { push, helper } = context;
  3.   push(`${helper(TO_DISPLAY_STRING)}(`);
  4.   genNode(node.content, context);
  5.   push(`)`);
  6. }
复制代码
首先会执行
  1. push
复制代码
方法向render函数中插入一个
  1. _toDisplayString
复制代码
函数调用,在debug终端看看执行完这个
  1. push
复制代码
方法后的render函数,如下图:

从上图中可以看到此时
  1. createElementBlock
复制代码
函数的第三个参数只生成了一半,调用
  1. _toDisplayString
复制代码
函数传入的参数还没生成。
接着会以
  1. node.content
复制代码
作为参数执行
  1. genNode(node.content, context);
复制代码
生成
  1. _toDisplayString
复制代码
函数的参数,此时代码又走回了
  1. genNode
复制代码
函数。
将断点再次走进
  1. genNode
复制代码
函数,看看此时的node是什么样的,如下图:

从上图中可以看到此时的node节点是一个简单表达式节点,表达式为:
  1. $setup.msg
复制代码
。所以代码会走进
  1. genExpression
复制代码
函数。
  1. genExpression
复制代码
函数
接着将断点走进
  1. genExpression
复制代码
函数中,
  1. genExpression
复制代码
函数中的代码如下:
  1. function genExpression(node, context) {
  2.   const { content, isStatic } = node;
  3.   context.push(
  4.     isStatic ? JSON.stringify(content) : content,
  5.     -3 /* Unknown */,
  6.     node
  7.   );
  8. }
复制代码
由于当前的
  1. msg
复制代码
变量是一个
  1. ref
复制代码
响应式变量,所以
  1. isStatic
复制代码
  1. false
复制代码
。所以会执行
  1. push
复制代码
方法,将
  1. $setup.msg
复制代码
插入到render函数中。
执行完
  1. push
复制代码
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到此时的render函数基本已经生成了,剩下的就是调用
  1. push
复制代码
方法生成各个函数的右括号")"和右花括号"}"。将断点逐层走出,直到
  1. generate
复制代码
函数中。代码如下:
  1. function generate(ast) {
  2.   // ...省略
  3.   genNode(ast.codegenNode, context);

  4.   deindent();
  5.   push(`}`);
  6.   return {
  7.     ast,
  8.     code: context.code,
  9.   };
  10. }
复制代码
执行完最后一个
  1. push
复制代码
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到此时的render函数终于生成啦!

总结

这是我画的我们这个场景中
  1. generate
复制代码
生成render函数的流程图:


  • 执行
    1. genModulePreamble
    复制代码
    函数生成:
    1. import { xxx } from "vue";
    复制代码
  • 简单字符串拼接生成render函数中的函数名称和参数,也就是
    1. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
    复制代码
  • 以根节点的
    1. codegenNode
    复制代码
    属性为参数调用
    1. genNode
    复制代码
    函数生成render函数中return的内容。

    • 此时传入的是虚拟节点,执行
      1. genVNodeCall
      复制代码
      函数生成
      1. return _openBlock(), _createElementBlock(
      复制代码
      和调用
      1. genNodeList
      复制代码
      函数,生成
      1. createElementBlock
      复制代码
      函数的参数。
    • 处理p标签的
      1. tag
      复制代码
      标签名和
      1. props
      复制代码
      ,生成
      1. createElementBlock
      复制代码
      函数的第一个和第二个参数。此时render函数return的内容为:
      1. return _openBlock(), _createElementBlock("p", null
      复制代码
    • 处理p标签的children也就是
      1. {{msg}}
      复制代码
      节点,再次调用
      1. genNode
      复制代码
      函数。此时node节点类型为双大括号节点,调用
      1. genInterpolation
      复制代码
      函数。
      1. genInterpolation
      复制代码
      函数中会先调用
      1. push
      复制代码
      方法,此时的render函数return的内容为:
      1. return _openBlock(), _createElementBlock("p", null, _toDisplayString(
      复制代码
      。然后以
      1. node.content
      复制代码
      为参数再次调用
      1. genNode
      复制代码
      函数。
      1. node.content
      复制代码
      1. $setup.msg
      复制代码
      ,是一个简单表达式节点,所以在
      1. genNode
      复制代码
      函数中会调用
      1. genExpression
      复制代码
      函数。执行完
      1. genExpression
      复制代码
      函数后,此时的render函数return的内容为:
      1. return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg
      复制代码
    • 调用push方法生成各个函数的右括号")"和右花括号"}",生成最终的render函数

到此这篇关于原来 Vue 3 的 generate 是这样生成 render 函数的的文章就介绍到这了,更多相关原来vue3中template使用ref无需.value是因为这个内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/javascript/3223916bn.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具