Vue3 是如何通过编译优化提升框架性能的?

2023-02-2821:45:02WEB前端开发Comments776 views字数 7813阅读模式

Vue3 通过编译优化,极大的提升了它的性能。本文将深入探讨 Vue3 的编译优化的细节,了解它是如何提升框架性能的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

编译优化

编译优化指的是:编译器将模板编译为渲染函数的过程中,尽可能多地提取关键信息,用于指导生成最优代码的过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

编译优化的策略和具体实现,是由框架的设计思路所决定的,不同框架有不同思路,因此优化策略也是不同的文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

但优化方向基本一致,尽可能的区分动态内容和静态内容,针对不同的内容,采用不同的优化策略。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

优化策略

Vue 作为组件级的数据驱动框架,当数据变化时,Vue 只能知道具体的某个组件发生了变化,但不知道具体是哪个元素需要更新。因此还需要对比新旧两棵 VNode 树,一层层地遍历,找出变化的部分,并进行更新。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

但其实使用模板描述的 UI,结构是非常稳定的,例如以下代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div class="container">
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

在这段代码中,唯一会发生变化的,就只有 h2 元素,且只会是内容发生变化,它的 attr 也是不会变化的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

如果对比新旧两颗 VNode 树,会有以下步骤:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  1. 比对 div
  2. 比对 div 的 children,使用 Diff 算法,找出 key 相同的元素,并一一进行比对
    1. 比对 h1 元素
    2. 比对 h2 元素

在对比完之后,发现 h2 元素的文本内容改变了,然后 Vue 会对 h2 的文本内容进行更新操作。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

但实际上,只有 h2 元素会改变,我们如果可以只比对 h2 元素,然后找到它变化的内容,进行更新。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

更进一步,其实 h2 只有文本会改变,只比对 h2 元素的文本内容,然后进行更新,这样就可以极大提升性能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

标记元素变化的部分

为了对每个动态元素的变化内容进行记录,需要引入 patchFlag 的概念文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

patchFlag

patchFlag 用于标记一个元素中动态的内容,它是 VNode 中的一个属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

还是这个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

加入 patchFlag 后的 h2 VNode 为:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

{ type: 'h2', children: ctx.msg, patchFlag: 1 }

patchFlag 为 1,代表这个元素的 Text 部分,会发生变化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

注意:patchFlag 是一个 number 类型值,记录当前元素的变化的部分文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

而 PatchFlag 是 Typescript 的 Enum 枚举类型文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

下面是 PatchFlag 的部分枚举定义文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

export const enum PatchFlags {
  // 代表元素的 text 会变化
  TEXT = 1,
  // 代表元素的 class 会变化
  CLASS = 1 << 1,
  // 代表元素的 style 会变化
  STYLE = 1 << 2,
  // 代表元素的 props 会变化
  PROPS = 1 << 3,
  // ...
}

当 patchFlag === PatchFlags.TEXT,即 patchFlag === 1 时,代表元素的 Text 会变化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

patchFlag 使用二进制进行存储,每一位存储一个信息。如果 PatchFlag 第一位为 1,就说明 Text 是动态的,如果第二位为 1,就说明 Class 是动态的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

如果一个元素既有 Text 变化,又有 Class 变化,patchFlag 就为 3文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

即 PatchFlag.TEXT | PatchFlagCLASS1 | 2 ,1 二进制是 01,2 的二进制是 10,按位或的结果为 11,即十进制的 3。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

计算过程如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Vue3 是如何通过编译优化提升框架性能的?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

有了这样的设计,我们可以根据每一位是否为 1,决定是否决定执行对应内容的更新文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

使用按位与 & 进行判断,具体过程如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Vue3 是如何通过编译优化提升框架性能的?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

伪代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

function patchElement(n1, n2){
    if(n2.patchFlag > 0){
        // 有 PatchFlag,只需要更新动态部分
        if (patchFlag & PatchFlags.TEXT) {
            // 更新 class
        }

        if (patchFlag & PatchFlags.CLASS) {
            // 更新 class
        }

        if (patchFlag & PatchFlags.PROPS) {
            // 更新 class
        }

        ...

    } else {
        // 没有 PatchFlag,全量比对并更新
    }
}
  • 当元素有 patchFlag 时,就只更新 patchFlag 对应的部分即可。
  • 如果没有 patchFlag,则将新老 VNode 全量的属性进行比对,找出差异并更新

为了能生成 dynamicChildren 和 patchFlag,就需要编译器的配合,在编译时分析出动态的元素和内容文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

如何生成 patchFlag

由于模板结构非常稳定,很容易判断出模板的元素是否为动态元素,且能够判断出元素哪些内容是动态的文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

还是这个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

Vue 编译器会生成如下的代码(并非最终生成的代码):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

import { ref, createVNode } from 'vue'

const __sfc__ = {
    __name: 'App',
    setup() {

        const msg = ref('Hello World!')

        // 在 setup 返回编译后渲染函数
        return () => {
            return createVNode("div", { class: "container" }, [
                createVNode("h1", null, "hello"),
                createVNode("h2", null, msg.value, 1 /* TEXT */)
            ])
        }
    }
}

createVNode 函数,其实就是 Vue 提供的渲染函数 h,只不过它比 h 多传了 patchFlag 参数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

对于动态的元素,在创建 VNode 的时候,会多传一个 patchFlag 参数,这样生成的 VNode,也就有了 patchFlag 属性,就代表该 VNode 是动态的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

记录动态元素

从上一小节我们可以知道,有 patchFlag 的元素,就是动态的元素,那如何对它们进行收集和记录呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

为了实现上述目的,我们需要引入 Block(块)的概念文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Block

Block 是一种特殊的 VNode,它可以负责收集它内部的所有动态节点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Block 比普通的 VNode 多了 dynamicChildren 属性,用于存储内部所有动态子节点。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

还是这个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

h1 的 VNode 为:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

const h1 = { type: 'h1', children: 'hello' }

h2 的 VNode 为:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

const h2 = { type: 'h2', children: ctx.msg, patchFlag: 1 }

div 的 VNode 为:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

const vnode = {
    type: 'div',
    children: [
        h1,
        h2
    ],
    dynamicChildren: [
        h2    // 动态节点,会被存储在 dynamicChildren
    ],
}

这里的 div 就是 Block,实际上,Vue 会把组件内的第一个元素作为 Block文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Block 更新

动态节点的 VNode,会被按顺序存储 Block 的 dynamicChildren 中文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 存储在 dynamicChildren,是为了可以只对这些元素进行比对,跳过其他静态元素
  • dynamicChildren 只存储在 Block,不需要所有 VNode 都有 dynamicChildren,因为仅仅通过 Block dynamicChildren 就能找到其内部中所有的动态元素
  • 按顺序,即旧 VNode 的 dynamicChildren 和 新 VNode 的 dynamicChildren 的元素是一一对应的,这样的设计就不需要使用 Diff 算法,从新旧 VNode 这两个 children 数组中,找到对应(key 相同)的元素

那我们更新组件内元素的算法,可以是这样的:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

// 传入两个元素的旧 VNode:n1 和新 VNode n2,
// patch 是打补丁的意思,即对它们进行比较并更新
function patchElement(n1, n2){
    if (n2.dynamicChildren) {
        // 优化的路径
        // 直接比对 dynamicChildren 就行
        patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren)
    } else {
        // 全量比对
        patchChildren(n1, n2)
    }
}

patchBlockChildren 的大概实现如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

// 对比新旧 children(是一个 VNode 的数组),并进行更新
function patchBlockChildren(oldDynamicChildren, oldDynamicChildren){
    // 按顺序一一比对即可
    for (let i = 0; i < dynamicChildren.length; i++) {
        const oldVNode = oldDynamicChildren[i]
        const newVNode = dynamicChildren[i]
        // patch 传入新旧 VNode,然后进行比对更新
        patch(oldVNode, newVNode)
    }
}

直接按顺序比较 dynamicChildren,好像很厉害,但这样真的没问题吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

其实是有问题的,但是能解决。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

dynamicChildren 能按顺序进行比较的前提条件,是要新旧 VNode 中, dynamicChildren 的元素必须能够一一对应。那会不会存在不一一对应的情况呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

答案是会的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

例如 v-if ,我们稍微改一下前面的例子(在线体验地址):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1 v-if="!msg">hello</h1>
      <p v-else>
          <h2 >{{ msg }}</h2>
      </p>
  </div>
</template>

假如 msg 从 undefined 变成了 helloWorld文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

按我们上一小节所受的,旧的 VNode 的 dynamicChildren 为空(没有动态节点),新的 dynamicChildren 则是为h2文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

这种情况, v-if/v-else 让模板结构变得不稳定导致 dynamicChildren 不能一一对应。那要怎么办呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

解决办法也很简单,让 v-if/v-else 的元素也作为 Block,这样就会得到一颗 Block 树。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Vue3 是如何通过编译优化提升框架性能的?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Block 会作为动态节点,被 dynamicChildren 收集文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

例如:当 msg 为 undefined,组件内元素的 VNode 如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

const vnode = {
    type: 'div',
    key: 0,    // 这里新增了 key
    children: [
        h1
    ],
    dynamicChildren: [
        h1    // h1 是 Block(h1 v-if),会被存储在 dynamicChildren
    ],
}

当 msg 为不为空时,组件内元素的 VNode 如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

const vnode = {
    type: 'div',
    key: 0,  // 这里新增了 key
    children: [
        h1
    ],
    dynamicChildren: [
        p    // p 是 Block(p v-else),会被存储在 dynamicChildren
    ],
}

对于 Block(div) 来说,它的 dynamicChildren 是稳定的,里面的元素仍然是一一对应,因此可以快速找到对应的 VNode。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

v-if/v-else 创建子 Block 的时候,会为子 Block 生成不同 key。在该例子中, Block(h1 v-if) 和 Block(p v-else) 是对应的一组 VNode/Block,它们的 key 不同,因此在更新这两个 Block 时,Vue 会将之前的卸载,然后重新创建元素。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

这种解决方法,其核心思想为:将不稳定元素,限制在最小的范围,让外层 Block 变得稳定文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

这样做有以下好处:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 保证稳定外层 Block 能继续使用优化的更新策略,
  • 在不稳定的内层 Block 中实施降级策略,只进行全量更新比对。

同样的,v-for 也会引起模板不稳定的问题,解决思路,也是将 v-for 的内容单独作为一层 Block,以保证外部 dynamicChildren 的稳定性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

如何创建 Block

只需要把有 patchFlag 的元素收集到 dynamicChildren 数组中即可,但如何确定 VNode 收集到哪一个 Block 中呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

还是这个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

Vue 编译器会生成如下的代码(并非最终代码):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

import { ref, createVNode, openBlock } from 'vue'

const __sfc__ = {
    __name: 'App',
    setup() {

        const msg = ref('Hello World!')

        // 在 setup 返回编译后渲染函数
        return () => {
            return (
                // 新增了 openBlock
                openBlock(),
                // createVNode 改为了 createBlock
                createBlock("div", { class: "container" }, [
                    createVNode("h1", null, "hello"),
                    createVNode("h2", null, msg.value, 1 /* TEXT */)
                ]))
        }
    }
}

与上一小节相比,有以下不同:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 新增了 openBlock
  • createVNode 改为了 createBlock

由于 Block 是一个范围,因此需要 openBlock 和 closeBlock 去划定范围,不过我们看不到 closeBlock ,是因为 closeBlock 直接在 createBlock 函数内被调用了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

处于 openBlock 和 closeBlock(或者 createBlock) 之间的元素,都会被收集到当前的 Block 中文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

我们来看一下 render 函数的执行顺序:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  1. openBlock初始化 currentDynamicChildren 数组
  2. createVNode,创建 h1 的 VNode
  3. createVNode,创建 h2 的 VNode,这个是动态元素,将 VNode push 到 currentDynamicChildren
  4. createBlock,创建 div 的 VNode,将 currentDynamicChildren 设置为 dynamicChildren
    • 在 createBlock 中调用 closeBlock

值得注意的是,内层的 createVNode 是先执行, createBlock 是后执行的,因此能收集 openBlock 和 closeBlock 之间的动态元素 VNode文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

其中 openBlock 和 closeBlock 的实现如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

// block 可能会嵌套,当发生嵌套时,用栈保存上一层收集的内容
// 然后 closeBlock 时恢复上一层的内容
const dynamicChildrenStack = []

// 用于存储当前范围中的动态元素的 VNode
let currentDynamicChildren = null

function openBlock(){
    currentDynamicChildren = []
    dynamicChildrenStack.push(currentDynamicChildren)
}

// 在 createBlock 中被调用
function closeBlock(){``
    currentDynamicChildren = dynamicChildrenStack.pop()
}

因为 Block 可以发生嵌套,因此要用栈存起来。openBlock 的时候初始化并推入栈,closeBlock 的时候恢复上一层的 dynamicChildren文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

createVnode 的代码大致如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

function createVnode(tag, props, children, patchFlags){
    const key = props && props.key
    props && delete props.key

    const vnode = {
        tag,
        props,
        children,
        key,
        patchFlags
    }

    // 如果有 patchFlags,那就记录该动态元素的 Vnode
    if(patchFlags){
        currentDynamicChildren.push(vnode)
    }

    return vnode
}

createBlock 的代码大致如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

function createBlock(tag, props, children){
    // block 本质也是一个 VNode
    const vnode = createVNode(tag, props, children)

    vnode.dynamicChildren = currentDynamicChildren

    closeBlock()
    // 当前 block 也会收集到上一层 block 的 dynamicChildren 中
    currentDynamicChildren.push(vnode)
    return vnode
}

其他编译优化手段

静态提升

仍然是这个例子(在线预览):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

实际上会编译成下图:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

Vue3 是如何通过编译优化提升框架性能的?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

与我们前面小节不同的是,编译后的代码,会将静态元素的 createVNode 提升,这样每次更新组件的时候,就不会重新创建 VNode,因此每次拿到的 VNode 的引用相同,Vue 渲染器就会直接跳过其渲染文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

预字符串化

在线例子预览文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

<template>
  <div>
      <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
  </div>
</template>

Vue3 是如何通过编译优化提升框架性能的?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

如果模板中包含大量连续静态的标签节点,会将这些静态节点序列化为字符串,并生成一个 Static 的 VNode文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

这样的好处是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 大块静态资源可以直接通过 innerHTML 设置,性能更佳
  • 减少创建大量的 VNode
  • 减少内存消耗

编译优化能用于 JSX 吗

目前 JSX 没有编译优化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

我在《浅谈前端框架原理》中谈到过:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 模板基于 HTML 语法进行扩展,其灵活性不高,但这也意味着容易分析
  • 而 JSX 是一种基于 ECMAScript 的语法糖,扩充的是 ECMAScript 的语法,但 ECMAScript 太灵活了,难以实现静态分析

例如:js 的对象可以复制、修改、导入导出等,用 js 变量存储的 jsx 内容,无法判断是否为静态内容,因为可能在不知道哪个地方就被修改了,无法做静态标记。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

但也并不是完全没有办法,例如可以通过约束 JSX 的灵活性,使其能够被静态分析,例如 SolidJS。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

总结

在本文中,我们首先讨论了编译优化的优化方向:尽可能的区分动态内容和静态内容文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

然后具体到 Vue 中,就是从模板语法中,分离出动态和静态的元素,并标记动态的元素,以及其动态的部分文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

当我们标记动态的内容后,Vue 就可以配合渲染器,快速找到并更新动态的内容,从而提升性能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/31017.html

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/31017.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定