likes
comments
collection
share

构建你的React【上】前言 文章翻译作者 pomb 的博客 构建你的React 我们将从头开始重写 React。一步一

作者站长头像
站长
· 阅读数 37

前言

文章翻译作者 pomb 的博客

构建你的React

我们将从头开始重写 React。一步一步。遵循真实 React 代码的架构,但没有所有优化和非必要的功能。

从头开始,这些是我们将逐一添加到 React 版本中的所有内容:

  • 步骤一:createElement 函数
  • 步骤二:render 函数
  • 步骤三:Concurrent Mode 并发模式
  • 步骤四:Fiber
  • 步骤五:Reconciliation
  • 步骤六:Function Components
  • 步骤七:Hooks

步骤 0 :回顾

但首先让我们回顾一些基本概念。如果你已经对 React、JSX 和 DOM 元素的工作原理有很好的了解,则可以跳过此步骤。

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById('root')
ReactDOM.render(element, container)

我们将使用这个 React 应用程序,只需三行代码。第一个定义了一个 React 元素。下一个从 DOM 获取一个节点。最后一个将 React 元素渲染到容器中。

让我们删除所有特定于 React 的代码,并将其替换为普通的 JavaScript。

在第一行,我们有元素,用 JSX 定义。它甚至不是有效的 JavaScript,所以为了用普通 JS 替换它,首先我们需要用有效的 JS 替换它。

JSX 通过 Babel 等构建工具转换为 JS。转换通常很简单:将标签内的代码替换为对 createElement 的调用,将标签名称、props 和 children 作为参数传递。

const element = React.createElement('h1', { title: 'foo' }, 'Hello')

const container = document.getElementById("root")
ReactDOM.render(element, container)

React.createElement 从其参数创建一个对象。除了一些验证之外,这就是它所做的全部。因此,我们可以安全地将函数调用替换为其输出。

const element = React.createElement('h1', { title: 'foo' }, 'Hello')

// 省略之前到代码

这就是 element ,一个具有两个属性的对象:typeprops(嗯,它有更多,但我们只关心这两个)。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

// 省略之前到代码

type 是一个字符串,它指定了我们想要创建的 DOM 节点的类型,它是你在创建 HTML 元素时传递给 document.createElementtagName。它也可以是一个函数,但我们将它留给步骤七。

props 是另一个对象,它包含来自 JSX 属性的所有键和值。它还有一个特殊属性:children

在这种情况下,children 是一个字符串,但它通常是具有更多元素的数组。这就是为什么元素也是树的原因。

我们需要替换的另一段 React 代码是对 ReactDOM.render 的调用。

render 是 React 更改 DOM 的地方,所以让我们自己进行更新。

// 省略之前到代码

ReactDOM.render(element, container)

首先,我们使用元素类型创建一个 node*,在本例中为 h1

然后我们将所有元素 props 分配给该节点。这里只是标题。

* 为避免混淆,我将使用“element”来指代 React 元素,使用“node”来指代 DOM 元素。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)// 新增
node["title"] = element.props.title// 新增

然后我们为子节点创建节点。我们只有一个字符串作为子节点,因此我们创建了一个 text 节点。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title


const text = document.createTextNode("")// 新增
text["nodeValue"] = element.props.children// 新增

使用 textNode 而不是设置 innerText 将允许我们稍后以相同的方式处理所有元素。还要注意我们如何像对 h1 标题所做的那样设置 nodeValue,这几乎就像字符串有 props:{nodeValue: “你好”}

最后,我们将 textNode 附加到 h1 上,将 h1 附加到容器中。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

node.appendChild(text)// 新增
container.appendChild(node)// 新增

现在我们拥有与以前相同的应用程序,但没有使用 React。

步骤 1:createElement 函数

让我们从另一个应用程序开始(另一个结构的 JSX)。这一次,我们将用我们自己的 React 版本替换 React 代码。

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById('root')
ReactDOM.render(element, container)

我们将从编写自己的 createElement 开始。

让我们将 JSX 转换为 JS,以便我们可以看到 createElement 调用。

正如我们在上一步中看到的,元素是具有 typeprops 的对象。我们的函数唯一需要做的就是创建该对象。

const element = React.createElement(
  'div',
  { id: 'foo' },
  React.createElement('a', null, 'bar'),
  React.createElement('b')
)

// 省略之前到代码

我们对 props 使用 展开运算符,对 children 使用 rest 参数语法,这样 children prop 将始终是一个数组。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children
    }
  }
}

例如,createElement("div") 返回:

{
  "type": "div",
  "props": { "children": [] }
}

createElement("div", null, a) 返回:

{
  "type": "div",
  "props": { "children": [a] }
}

createElement("div", null, a, b) 返回:

{
  "type": "div",
  "props": { "children": [a, b] }
}

children 数组还可以包含原始值,如字符串或数字。因此,我们将所有不是对象的东西包装在它自己的元素中,并为它们创建一个特殊类型:TEXT_ELEMENT。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => // 新增判断
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

function createTextElement(text) {// 新增函数
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

// 省略之前的代码

React 不会在没有子项时包装原始值或创建空数组,但我们这样做是因为它会简化我们的代码,对于我们的库,我们更喜欢简单的代码而不是高性能代码。

我们仍在使用 React 的 createElement

// 省略之前的代码

const element = React.createElement(
  'div',
  { id: 'foo' },
  React.createElement('a', null, 'bar'),
  React.createElement('b')
)

为了替换它,让我们为我们的库命名。我们需要一个听起来像 React 但又暗示其教学目的的名字。

我们将其称为 Didact。

// 省略之前的代码

const Didact = {
  createElement,
}

const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)

// 省略之前的代码

但我们仍然希望在这里使用 JSX。我们如何告诉 babel 使用 Didact 的 createElement 而不是 React 的?

如果我们有这样的注释,当 babel 转译 JSX 时,它将使用我们定义的函数。

// 省略之前的代码

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

// 省略之前的代码

步骤 1 完整代码

const Didact = {
  createElement,
}

const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => // 新增判断
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

function createTextElement(text) {// 新增函数
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

const container = document.getElementById('root')

console.log(element)

// 省略render

步骤 2:render 函数

接下来,我们需要编写我们版本的 ReactDOM.render 函数。

// 省略之前的代码

ReactDOM.render(element, container)

目前,我们只关心向 DOM 添加内容。我们稍后会处理更新和删除。

// 省略之前的代码

function render(element, container) {
  // TODO create dom nodes
}

const Didact = {
  createElement,
  render,
}

// 省略之前的代码

Didact.render(element, container)

我们首先使用 element 类型创建 DOM 节点,然后将新节点附加到容器中。

// 省略之前的代码

function render(element, container) {
  const dom = document.createElement(element.type)

  container.appendChild(dom)
}

// 省略之前的代码

我们递归地为每个孩子做同样的事情。

// 省略之前的代码

function render(element, container) {
  const dom = document.createElement(element.type)

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

// 省略之前的代码

我们还需要处理文本元素,如果元素类型为 TEXT_ELEMENT 我们创建一个文本节点而不是常规节点。

// 省略之前的代码

function render(element, container) {

  // 新增
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

// 省略之前的代码

我们在这里需要做的最后一件事是将 element props 分配给节点。

// 省略之前的代码

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

// 新增
  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

// 省略之前的代码

就是这样。我们现在有一个可以将 JSX 渲染到 DOM 的库。

codesandbox 上试一试。

步骤 2 完整代码

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

const Didact = {
  createElement,
  render,
}

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
Didact.render(element, container)

步骤 3:Concurrent Mode 并发模式

但。。。在我们开始添加更多代码之前,我们需要一个重构。

这个递归调用有问题。

// 省略之前的代码

function render(element, container) {
  // 有问题
  element.props.children.forEach((child) => render(child, dom))
}

// 省略之前的代码

一旦我们开始渲染,我们不会停止,直到我们渲染了完整的元素树。如果元素树很大,则可能会阻塞主线程太久。如果浏览器需要执行高优先级操作,例如处理用户输入或保持动画流畅,则必须等到渲染完成。

因此,我们将工作分解为小单元,完成每个单元后,如果有其他需要完成的事情,我们将让浏览器中断渲染。

// 省略之前的代码

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false

  // 如果有下一个单元 并且 可以继续工作
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)

    // 时间大于1的时候可以继续工作
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

// 省略之前的代码

我们使用 requestIdleCallback 来构建一个循环。你可以将 requestIdleCallback 看作是一个 setTimeout,但浏览器会在主线程空闲时运行回调,而不是我们告诉它何时运行。

// 省略之前的代码
function workLoop(deadline) {
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)
// 省略之前的代码

React 不再使用 requestIdleCallback。现在它使用 scheduler 包。但对于这个用例,它在概念上是相同的。

requestIdleCallback 还为我们提供了一个 deadline 参数。我们可以使用它来检查浏览器需要再次控制之前我们还有多少时间。

// 省略之前的代码

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    // 省略之前的代码
  }
  requestIdleCallback(workLoop)
}

// 省略之前的代码

截至 2019 年 11 月,Concurrent 模式在 React 中还不稳定。循环的稳定版本看起来更像这样:

while (nextUnitOfWork) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}

要开始使用循环,我们需要设置第一个工作单元,然后编写一个 performUnitOfWork 函数,该函数不仅执行工作,还返回下一个工作单元。

// 省略之前的代码

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
}

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

// 省略之前的代码

步骤 3 完整代码

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === 'object' ? child : createTextElement(child)
      )
    }
  }
}

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

function render(element, container) {
  const dom =
    element.type == 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(element.type)
  const isProperty = (key) => key !== 'children'
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = element.props[name]
    })
  element.props.children.forEach((child) => render(child, dom)) // 这里的写法有问题,应该靠workloop来完成,后面章节会修改
  container.appendChild(dom)
}

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

const Didact = {
  createElement,
  render
}

/** @jsx Didact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
)

const container = document.getElementById('root')
Didact.render(element, container)

步骤 4:Fibers

要组织工作单元,我们需要一个数据结构:fiber 树。

构建你的React【上】前言 文章翻译作者 pomb 的博客 构建你的React 我们将从头开始重写 React。一步一

让我给你看一个例子。

假设我们想要渲染一个像这样的元素树:

Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

渲染中,我们将创建根 fiber 并将其设置为 nextUnitOfWork。其余工作将在 performUnitOfWork 函数上进行,我们将为每个 fiber 执行三项操作:

  1. 将元素添加到 DOM
  2. 为元素的 children 创建 fiber
  3. 选择下一个工作单元

此数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个纤程都有一个指向其第一个子项、下一个兄弟级和父级的链接。

当我们完成对 fiber 执行工作时,如果它有一个子项(child),则该 fiber 将是下一个工作单元。

从我们的示例中,当我们完成 div fiber 的工作时,下一个工作单元将是 h1 fiber。

如果 fiber 没有子节点(child),我们将同级(sibling)节点用作下一个工作单元。

例如,p fiber 没有 child,因此我们在完成 p fiber 后移动到 a fiber。

如果 fiber 没有孩子也没有兄弟姐妹,我们就去找 “叔叔”:父母的兄弟姐妹。就像示例中的 a 和 h2 fiber。

此外,如果父级没有兄弟姐妹,我们会继续向上浏览父级,直到找到有兄弟姐妹的父级 或直到我们到达根 root。如果我们到达了根,则意味着我们已经完成了此渲染的所有工作。

现在让我们将其放入代码中。

首先,让我们从 render 函数中删除此代码。

// 省略之前的代码

function render(element, container) {
  // 这里的内容即将删掉
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

let nextUnitOfWork = null

// 省略之前的代码

我们将创建 DOM 节点的部分保留在它自己的函数中,我们稍后会使用它。

// 省略之前的代码

function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)

  const isProperty = key => key !== "children"
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name]
    })

  return dom
}

function render(element, container) {
  // TODO set next unit of work
}

let nextUnitOfWork = null

// 省略之前的代码

render 函数中,我们将 nextUnitOfWork 设置为 fiber 树的根。

// 省略之前的代码

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}

let nextUnitOfWork = null

// 省略之前的代码

然后,当浏览器准备好时,它将调用我们的 workLoop,我们将开始在根目录下工作。

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(fiber) {
  // TODO add dom node
  // TODO create new fibers
  // TODO return next unit of work
}

首先,我们创建一个新节点并将其附加到 DOM。

我们在 fiber.dom 属性中跟踪 DOM 节点。

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  // TODO create new fibers
  // TODO return next unit of work
}

然后,我们为每个孩子创建一个新的 fiber。

// 省略之前的代码

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
  }

  // TODO return next unit of work
}

// 省略之前的代码

然后我们将其添加到 fiber 树中,将其设置为子项或同级项,具体取决于它是否是第一个子项。

// 省略之前的代码

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }

    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

  // TODO return next unit of work
}

// 省略之前的代码

最后,我们搜索下一个工作单元。我们首先尝试与 child 一起尝试,然后与兄弟姐妹一起尝试,然后与叔叔一起尝试,依此类推。

// 省略之前的代码

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }

    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

 if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }

}

// 省略之前的代码

这就是我们的 performUnitOfWork。

步骤 4 完整代码

// JSX转对象
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === 'object' ? child : createTextElement(child)
      )
    }
  }
}

// JSX转对象时处理文本节点
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

// 单独的创建dom的函数,但是没有container挂载
function createDom(fiber) {
  const dom =
    fiber.type == 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type)
  const isProperty = (key) => key !== 'children'
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = fiber.props[name]
    })
  return dom
}

// ReactDOM.render 把JSX转换后到对象变成dom,并渲染到页面
// element就是fiber树,相当于传了fiber树进去当第一个工作单元
function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element]
    }
  }
}

// 下一个工作单元
let nextUnitOfWork = null

// 并发更新
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 执行工作单元
// 1. 创建dom
// 2. 创建子节点 fiber树 或者说创建新的fiber
// 3. 返回下一个工作单元
function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  // 只有当前级处理,但由于在workLoop里是递归,所以在递归时候会处理完所有子级 fiber
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  // 从根开始生成fiber
  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null
    }

    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

// 自己定义的 React 对象
const Didact = {
  createElement,
  render
}

/** @jsx Didact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
)

const container = document.getElementById('root')
Didact.render(element, container)
转载自:https://juejin.cn/post/7411435643290632243
评论
请登录