同事问我 Fiber 是什么,我直接甩给他这篇文章

在 react 16 版本中,官方对内部代码进行了大面积的重写,其中 fiber 就是最重要的一部分,那什么是 fiber 呢?

其实 fiber 就是一种新的 dom 比对的新的算法,fiber 就是这种算法的名字。在 react 16 版本之前,dom 对比的算法叫 stack(堆栈)。

那为什么 react 官方要重写 dom 的比对算法,而采用 filber 呢?

那是因为 stack 对更新虚拟 dom 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵虚拟 dom 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。从而使应用无法及时响应用户的输入或其他高优先级任务,页面就会产生卡顿, 非常的影响用户体验。

核心问题:递归无法中断,执行重任务耗时长。 JavaScript 又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿,用户体验差。

结构体

fiber 其实就是 javascript 对象,它是 Virtual DOM 对象演变而来的,在 fiber 对象有很多属性,这里我就挑出几个比较重要的属性放在了笔记中

type Fiber = {
  /************************  DOM 实例相关  *****************************/
  
  // 标记不同的组件类型, 值详见 WorkTag
  tag: WorkTag,

  // 组件类型 div、span、组件构造函数
  type: any,

  // 实例对象, 如类组件的实例、原生 dom 实例, 而 function 组件没有实例, 因此该属性是空
  stateNode: any,
 
	/************************  构建 Fiber 树相关  ***************************/
  
  // 指向自己的父级 Fiber 对象
  return: Fiber | null,

  // 指向自己的第一个子级 Fiber 对象
  child: Fiber | null,
  
  // 指向自己的下一个兄弟 iber 对象
  sibling: Fiber | null,
  
  // 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
  // 我们称他为 current <==> workInProgress
  // 在渲染完成之后他们会交换位置
  // alternate 指向当前 Fiber 在 workInProgress 树中的对应 Fiber
	alternate: Fiber | null,
		
  /************************  状态数据相关  ********************************/
  
  // 即将更新的 props
  pendingProps: any, 
  // 旧的 props
  memoizedProps: any,
  // 旧的 state
  memoizedState: any,
		
  /************************  副作用相关 ******************************/

  // 该 Fiber 对应的组件产生的状态更新会存放在这个队列里面 
  updateQueue: UpdateQueue<any> | null,
  
  // 用来记录当前 Fiber 要执行的 DOM 操作
  effectTag: SideEffectTag,

  // 存储第一个要执行副作用的子级 Fiber 对象
  firstEffect: Fiber | null,
  
  // 存储下一个要执行副作用的子级 Fiber 对象
  // 执行 DOM 渲染时要先通过 first 找到第一个, 然后通过 next 一直向后查找
  nextEffect: Fiber | null,
  
  // 存储 DOM 操作完后的副作用 比如调用生命周期函数或者钩子函数的调用
  lastEffect: Fiber | null,

  // 任务的过期时间
  expirationTime: ExpirationTime,
  
	// 当前组件及子组件处于何种渲染模式 详见 TypeOfMode
  mode: TypeOfMode,
};
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export type WorkTag =
  | 0
  | 1
  | 2
  | 3
  | 4
  | 5
  | 6
  | 7
  | 8
  | 9
  | 10
  | 11
  | 12
  | 13
  | 14
  | 15
  | 16
  | 17
  | 18
  | 19
  | 20
  | 21
  | 22
  | 23
  | 24
  | 25
  | 26
  | 27;

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

export type TypeOfMode = number;

// 同步渲染模式
export const NoMode = /*                         */ 0b0000000;
// TODO: Remove ConcurrentMode by reading from the root tag instead
// 异步渲染模式
export const ConcurrentMode = /*                 */ 0b0000001;
// 性能测试模式
export const ProfileMode = /*                    */ 0b0000010;
export const DebugTracingMode = /*               */ 0b0000100;
export const StrictLegacyMode = /*               */ 0b0001000;
export const StrictEffectsMode = /*              */ 0b0010000;
export const ConcurrentUpdatesByDefaultMode = /* */ 0b0100000;
export const NoStrictPassiveEffectsMode = /*     */ 0b1000000;

双缓存

如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,这样的话在帧画面替换的过程中就会节约非常多的时间,就不会出现白屏问题。这种在内存中构建并直接替换的技术叫做双缓存

React 使用双缓存技术完成 Fiber 树的构建与替换,实现DOM对象的快速更新。

在 React 中最多会同时存在两棵 Fiber 树,当前在屏幕中显示的内容对应的 Fiber 树叫做 current Fiber 树,当发生更新时,React 会在内存中重新构建一颗新的 Fiber 树,这颗正在构建的 Fiber 树叫做 workInProgress Fiber 树。

在双缓存技术中,workInProgress Fiber 树就是即将要显示在页面中的 Fiber 树,当这颗 Fiber 树构建完成后,React 会使用它直接替换 current Fiber 树达到快速更新 DOM 的目的,因为 workInProgress Fiber 树是在内存中构建的所以构建它的速度是非常快的。

一旦 workInProgress Fiber 树在屏幕上呈现,它就会变成 current Fiber 树。

双缓存树一个显著的特点就是两棵树之间会互相切换,通过alternate属性连接

currentFiber.alternate === workInProgressFiber;

workInProgressFiber.alternate === currentFiber;

构建 Fiber 树的过程

在调用 createRoot(并发模式)或者 ReactDOM.render(同步模式)时,会执行 createFiberRoot 方法。

export function createFiberRoot(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  initialChildren: ReactNodeList,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  // TODO: We have several of these arguments that are conceptually part of the
  // host config, but because they are passed in at runtime, we have to thread
  // them through the root constructor. Perhaps we should put them all into a
  // single type, like a DynamicHostConfig that is defined by the renderer.
  identifierPrefix: string,
  onRecoverableError: null | ((error: mixed) => void),
  transitionCallbacks: null | TransitionTracingCallbacks,
  formState: ReactFormState<any, any> | null,
): FiberRoot {
  
  // 生成 fiberRoot
  const root: FiberRoot = new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError,
    formState,
  )
 
  // ...

  // 生成 rootFiber
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );

  // 互相关联
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // .....

  return root;
}

createFiberRoot 创建一个了 fiberRoot,以及一个 rootFiber。它们的关系如下:

同事问我 Fiber 是什么,我直接甩给他这篇文章

在 React 中 fiberRoot 只有一个,而 rootFiber 可以有多个,因为 render 方法是可以调用多次的 fiberRoot 会记录应用的更新信息,比如协调器在完成工作后,会将工作成果存储在 fiberRoot 中。

render 阶段

render

/**
 * 渲染入口
 * element 要进行渲染的 ReactElement, createElement 方法的返回值
 * container 渲染容器 <div id="root"></div>
 * callback 渲染完成后执行的回调函数
 */
export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
  // 检测 container 是否是符合要求的渲染容器
  // 即检测 container 是否是真实的DOM对象
  // 如果不符合要求就报错
  if (!isValidContainerLegacy(container)) {
    throw new Error('Target container is not a DOM element.');
  }

  return legacyRenderSubtreeIntoContainer(
    // 父组件 初始渲染没有父组件 传递 null 占位
    null,
    element,
    container,
    // 是否为服务器端渲染 false 不是服务器端渲染 true 是服务器端渲染
    false,
    callback,
  );
}

isValidContainerLegacy

/**
 * 判断 node 是否是符合要求的 DOM 节点
 * 1. node 可以是元素节点
 * 2. node 可以是 document 节点
 * 3. node 可以是 文档碎片节点
 * 4. node 可以是注释节点但注释内容必须是 react-mount-point-unstable
 * 		react 内部会找到注释节点的父级 通过调用父级元素的 insertBefore 方法, 将 element 插入到注释节点的前面
 */
export function isValidContainerLegacy(node: any): boolean {
  return !!(
    node &&
    (node.nodeType === ELEMENT_NODE ||
      node.nodeType === DOCUMENT_NODE ||
      node.nodeType === DOCUMENT_FRAGMENT_NODE ||
      (node.nodeType === COMMENT_NODE &&
        (node: any).nodeValue === ' react-mount-point-unstable '))
  );
}

legacyRenderSubtreeIntoContainer

/**
 * 将子树渲染到容器中 (初始化 Fiber 数据结构: 创建 fiberRoot 及 rootFiber)
 * parentComponent: 父组件, 初始渲染传入了 null
 * children: render 方法中的第一个参数, 要渲染的 ReactElement
 * container: 渲染容器
 * forceHydrate: true 为服务端渲染, false 为客户端渲染
 * callback: 组件渲染完成后需要执行的回调函数
 **/
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
  /**
   * 检测 container 是否已经是初始化过的渲染容器
   * react 在初始渲染时会为最外层容器添加 _reactRootContainer 属性
   * react 会根据此属性进行不同的渲染方式
   * maybeRoot 不存在 表示初始渲染
   * maybeRoot 存在 表示更新
   */
  // 获取 container 容器对象下是否有 _reactRootContainer 属性
  const maybeRoot = container._reactRootContainer;

  // 即将存储根 Fiber 对象
  let root: FiberRoot;

  if (!maybeRoot) {
    // 初始渲染
    // 初始化根 Fiber 数据结构
    root = legacyCreateRootFromDOMContainer(
      container,
      children,
      parentComponent,
      callback,
      forceHydrate,
    );
  } else {
    // 非初始化渲染 即更新
    // ...
  }

  // 返回 render 方法第一个参数的真实 DOM 对象作为 render 方法的返回值
  // 就是说渲染谁 返回谁的真实 DOM 对象
  return getPublicRootInstance(root);
}


function legacyCreateRootFromDOMContainer(
  container: Container,
  initialChildren: ReactNodeList,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
  isHydrationContainer: boolean,
): FiberRoot {
  if (isHydrationContainer) {
    // 服务端渲染
    // ...
  } 

  // 客户端渲染
  // First clear any existing content.
  clearContainer(container);

  /**
   * 改变 callback 函数中的 this 指向
   * 使其指向 render 方法第一个参数的真实 DOM 对象
   */
  // 如果 callback 参数是函数类型
  if (typeof callback === 'function') {
    const originalCallback = callback;
    callback = function () {
      const instance = getPublicRootInstance(root);
      originalCallback.call(instance);
    };
  }

  // 返回项目中只有唯一一个的 FiberRoot 节点
  const root = createContainer(
    container,
    LegacyRoot,
    null, // hydrationCallbacks
    false, // isStrictMode
    false, // concurrentUpdatesByDefaultOverride,
    '', // identifierPrefix
    noopOnRecoverableError, // onRecoverableError
    null, // transitionCallbacks
  );

  // 对 container._reactRootContainer 重新赋值,
  // 以后在调用 legacyRenderSubtreeIntoContainer 方法时不会再再进入创建方法中
  container._reactRootContainer = root;

  // 将DOM节点 container 标记为已被作为root使用过
  // 并通过一个属性指向到fiber节点:
  // container['__reactContainer$'] = root.current; // root为fiber类型的节点
  // 这里就形成了互相指向,root.containerInfo = container;
  markContainerAsRoot(root.current, container);

  // 获取container的真实element元素,若container是注释类型的元素,则使用其父级元素,否则直接使用container
  // 大概是因为注释节点无法挂载事件
  const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  
  // 绑定所有可支持的事件到 rootContainerElement 节点上
  listenToAllSupportedEvents(rootContainerElement);

  // Initial mount should not be batched.
  // 让 updateContainer 的更新变成同步,执行完 updateContainer 后再返回 root
  flushSync(() => {
    updateContainer(initialChildren, root, parentComponent, callback);
  });

  return root;
}
export function clearContainer(container: Container): void {
  const nodeType = container.nodeType;
  if (nodeType === DOCUMENT_NODE) {
    clearContainerSparingly(container);
  } else if (nodeType === ELEMENT_NODE) {
    switch (container.nodeName) {
      case 'HEAD':
      case 'HTML':
      case 'BODY':
        clearContainerSparingly(container);
        return;
      default: {
        container.textContent = '';
      }
    }
  }
}

function clearContainerSparingly(container: Node) {
  let node;
  let nextNode: ?Node = container.firstChild;
  if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) {
    nextNode = nextNode.nextSibling;
  }
  
  while (nextNode) {
    node = nextNode;
    nextNode = nextNode.nextSibling;
    switch (node.nodeName) {
      case 'HTML':
      case 'HEAD':
      case 'BODY': {
        const element: Element = (node: any);
        clearContainerSparingly(element);
        
        // 删除相关字段
        detachDeletedInstance(element);
        continue;
      }
      // Script tags are retained to avoid an edge case bug. Normally scripts will execute if they
      // are ever inserted into the DOM. However when streaming if a script tag is opened but not
      // yet closed some browsers create and insert the script DOM Node but the script cannot execute
      // yet until the closing tag is parsed. If something causes React to call clearContainer while
      // this DOM node is in the document but not yet executable the DOM node will be removed from the
      // document and when the script closing tag comes in the script will not end up running. This seems
      // to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified
      // behavior so it could change in future versions of browsers. While leaving all scripts is broader
      // than strictly necessary this is the least amount of additional code to avoid this breaking
      // edge case.
      //
      // Style tags are retained because they may likely come from 3rd party scripts and extensions
      case 'SCRIPT':
      case 'STYLE': {
        continue;
      }
      // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
      case 'LINK': {
        if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
          continue;
        }
      }
    }
    container.removeChild(node);
  }
  return;
}

Q: 为什么首次 render 时需要判断 maybeRoot?

A: legacyRenderSubtreeIntoContainer 除了在 render 方法里面调用还在其他方法中有调用。其他方法在调用时,root 可能已经存在,所以在调用时,需要先判断一下 container._reactRootContainer 是否存在。如果存在,就应该走 update 路线。

createContainer

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: (error: mixed) => void,
  transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
  const hydrate = false;
  const initialChildren = null;
  
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
    null,
  );
}

可以看到 createContainer 只做了一件事,返回执行 createFiberRoot 方法后的值,这个值就是一个项目中只有唯一一个的 FiberRoot 节点。

createFiberRoot

/*
  containerInfo: 容器信息,表示要在其中创建 Fiber Root 的容器。
  tag: 根节点的标签,通常是 RootTag 中的一个值,用于区分不同类型的根节点。
  hydrate: 是否进行混合(Hydrate)操作,通常在服务端渲染(SSR)时会用到。
  initialChildren: 初始子节点,通常是要在 Fiber Root 中渲染的 React 元素。
  hydrationCallbacks: 混合操作的回调函数,用于在混合操作过程中执行一些逻辑。
  isStrictMode: 是否启用严格模式。
  concurrentUpdatesByDefaultOverride: 是否默认启用并发更新。
  identifierPrefix: 标识符前缀,用于标识该 Fiber Root。
  onRecoverableError: 可恢复错误的回调函数,用于处理在渲染过程中发生的错误。
  transitionCallbacks: 过渡追踪的回调函数,用于追踪过渡动画的执行。
  formState: 表单状态,用于管理 React 表单组件的状态。
*/

export function createFiberRoot(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  initialChildren: ReactNodeList,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  // TODO: We have several of these arguments that are conceptually part of the
  // host config, but because they are passed in at runtime, we have to thread
  // them through the root constructor. Perhaps we should put them all into a
  // single type, like a DynamicHostConfig that is defined by the renderer.
  identifierPrefix: string,
  onRecoverableError: null | ((error: mixed) => void),
  transitionCallbacks: null | TransitionTracingCallbacks,
  formState: ReactFormState<any, any> | null,
): FiberRoot {
  // 创建 fiberRoot 根节点,在 react 项目中 fiberRoot 根节点只存在一个
  const root: FiberRoot = (new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError,
    formState,
  ): any);
  
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  if (enableTransitionTracing) {
    root.transitionCallbacks = transitionCallbacks;
  }

  // 创建 rootFiber,可以是多个
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  
  // 用 current 将 FiberRoot 和 RootFiber 相关联
  root.current = uninitializedFiber;
  // 使用 stateNode 关联了 FiberRoot 实例。
  // rootFiber 可以看做是 render 内容的根节点
  uninitializedFiber.stateNode = root;

  // ...

  // 为 fiber 对象添加 updateQueue 属性, 初始化 updateQueue 对象
  // updateQueue 用于存放 Update 对象
  // Update 对象用于记录组件状态的改变
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

在 createFiberRoot 里面,创建了一个 FiberRoot 并且还创建了一个 rootFiber。并将它们通过 current 和 stateNode 进行相互关联。这也就实现了最初我们看到的图。

在创建完成 fiberRoot 和 rootFiber 之后会

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      lanes: NoLanes, // 0b0000000000000000000000000000000
      hiddenCallbacks: null,
    },
    callbacks: null,
  };
  fiber.updateQueue = queue;
}

当 FiberRoot 创建好后,就会执行 updateContainer 方法:

function legacyCreateRootFromDOMContainer(
  container: Container,
  initialChildren: ReactNodeList,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
  isHydrationContainer: boolean,
): FiberRoot {
  // ...
  
  // 让 updateContainer 的更新变成同步,执行完 updateContainer 后再返回 root
  flushSync(() => {
    updateContainer(initialChildren, root, parentComponent, callback);
  });

  return root;
}

updateContainer

/**
 * 计算任务的过期时间
 * 再根据任务过期时间创建 Update 任务
 */
export function updateContainer(
  // element 要渲染的 ReactElement
  element: ReactNodeList,
  // container Fiber Root 对象
  container: OpaqueRoot,
  // parentComponent 父组件 初始渲染为 null
  parentComponent: ?React$Component<any, any>,
  // ReactElement 渲染完成执行的回调函数
  callback: ?Function,
): Lane {
  // 获取 rootFiber
  const current = container.current;
  const lane = requestUpdateLane(current);

  if (enableSchedulingProfiler) {
    markRenderScheduled(lane);
  }

  // 设置FiberRoot.context, 首次执行返回一个emptyContext, 是一个 {}
  const context = getContextForSubtree(parentComponent);

  // 初始渲染时 Fiber Root 对象中的 context 属性值为 null
  // 所以会进入到 if 中
  if (container.context === null) {
    // 初始渲染时将 context 属性值设置为 {}
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  // 创建一个待执行任务
  const update = createUpdate(lane);
  // 将要更新的内容挂载到更新对象中的 payload 中
  // 将要更新的组件存储在 payload 对象中, 方便后期获取
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    // 将 callback 挂载到 update 对象中
    // 其实就是一层层传递 方便 ReactElement 元素渲染完成调用
    // 回调函数执行完成后会被清除 可以在代码的后面加上 return 进行验证
    update.callback = callback;
  }


  /*
    下面这段代码总结起来就是将更新对象添加到当前 Fiber 节点的更新队列中,
    并在必要时调度更新,同时处理更新的过渡动画。
  */

  // 将更新对象添加到当前 Fiber 节点的更新队列中, 返回一个新的根 Fiber 节点
  const root = enqueueUpdate(current, update, lane);

  // 如果不为 null,则成功添加更新对象到更新队列中
  if (root !== null) {
    // 调度和更新 current 对象
    scheduleUpdateOnFiber(root, current, lane);
    // 处理更新的过渡,确保更新过程中的过渡动画能够正确地执行
    entangleTransitions(root, current, lane);
  }

  return lane;
}

通过 createUpate 创建一个更新。调用 enqueueUpdate 将这个更新加入到更新队列中。然后执行 scheduleUpdateOnFiber 开始调度。

scheduleUpdateOnFiber

export function scheduleUpdateOnFiber(
  // ...
) {

  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    // ...
  } else {
    // ...
    ensureRootIsScheduled(root, eventTime);
    // ...
  }
}

TODO….

原文链接:https://juejin.cn/post/7336887570976473127 作者:youwne

(0)
上一篇 2024年2月19日 下午4:32
下一篇 2024年2月19日 下午4:43

相关推荐

发表评论

登录后才能评论