升级异步rendering(Update on Async Rendering)

原文地址 March 27, 2018 by Brian Vaughn

一年多了,React小组致力于实现异步rendering。在上个月的JSConf Iceland上,Dan公布了一些关于异步rendering的令人兴奋的新可能性解锁了
现在我们想分享在工作中使用这些特性,已经学到的一些经验和方法,以帮助你的组件为异步rendering发布做好准备。

我们学到的一个最大的经验是,一些旧的组件生命周期会变成不安全的代码实践。它们是:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

这些生命周期方法经常被误解和巧妙地滥用;而且,这些可能的滥用在异步rendering中会更有问题。因此,我们将在最近的版本中为这些生命周期加上“UNSAFE_”前缀。(“unsafe”不是指安全性,而是说使用这些生命周期在将来的React版本中更容易有bugs,尤其是一旦启用了异步rendering)


渐进的迁移(升级)路线

React遵循semantic versioning,因此此改变是渐进的。我们目前计划如下:

  • 16.3:介绍不安全生命周期的别名:UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(此版本旧名称和新别名都可以使用)
  • 某个16.x版本:开启弃用警告:componentWillMount, componentWillReceiveProps 和 componentWillUpdate。(此版本旧名称和新别名可以同时使用,但是旧名称会在开发模式打印警告)
  • 17.0:移除componentWillMount,componentWillReceiveProps 和 componentWillUpdate。(这以后,只有新的“UNSAFE_”生命周期名字可以使用)

注意如果你是个React应用开发者,你目前不需要为旧方法做什么。即将到来的16.3版本的主要目的是,允许开源项目维护人员借助弃用警告提前升级它们的库。这些警告直到将来某个16.x版本才会启用

我们在Facebook维护超过50000个React组件,并且我们不打算全部立即重写。我们知道升级需要时间。我们将与React社区的成员采用渐进的升级路线。


迁移旧的生命周期

如果你想开始使用React 16.3中新的组件APIs(或者你是个维护人员,想提前升级你的库),这里有少许例子希望帮到你,开始以不同的方式思考组件。以后,我们会在文档中继续添加其他“食谱”,展示如何避免使用问题生命周期。

在进入正题之前,看下16.3版本的生命周期变动概览:

  • 添加以下生命周期别名:UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(旧生命周期和新别名都可使用)
  • 两个新生命周期,静态getDerivedStateFromProps和getSnapshotBeforeUpdate

新生命周期:getDerivedStateFromProps

class Example extends React.Component {
  static getDerivedStateFromProps(props, state) {
    // ...
  }
}

新的静态生命周期getDerivedStateFromProps,在组件实例化后和每次re-rendered之前执行。它可以返回一个对象来更新state,或者返回null表示不需要更新state。

通过和componentDidUpdate一起使用,这个新生命周期应该涵盖了所有旧的componentWillReceiveProps使用情况。

Note:
旧componentWillReceiveProps和新getDerivedStateFromProps方法都增加组件复杂性。这经常引起bugs。请参考derived state简单替代方案,让组件可预见可维护。

新生命周期:getSnapshotBeforeUpdate

class Example extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // ...
  }
}

新生命周期getSnapshotBeforeUpdate在制造突变之前被调用(例如DOM被更新之前)。此生命周期返回值会作为componentDidUpdate的第三个参数。(这个生命周期不经常使用,但是在某些情况很有用,譬如在rerenders时候手动保存滚动位置)

通过和componentDidUpdate一起使用,这个新生命周期应该会涵盖所有旧的componentWillUpdate使用情况。
你可以在这个要点中找到他们的类型签名。

我们来看看下面的例子中是如何使用这俩生命周期的。


例子

  • Initializing state
  • Fetching external data
  • Adding event listeners (or subscriptions)
  • Updating state based on props
  • Invoking external callbacks
  • Side effects on props change
  • Fetching external data when props change
  • Reading DOM properties before an update

Note
简单起见,下面的例子们使用了实验性质的class,但不用也是一样的迁移策略。

初始化state

这个例子显示了一个在componentWillMount中调用setState的组件

// Before
class ExampleComponent extends React.Component {
  state = {};

  componentWillMount() {
    this.setState({
      currentColor: this.props.defaultColor,
      palette: 'rgb',
    });
  }
}

对这种组件最简单的重构是将state初始化移动到constructor或属性初始化器,例如:

// After
class ExampleComponent extends React.Component {
  state = {
    currentColor: this.props.defaultColor,
    palette: 'rgb',
  };
}
获取外部数据

这是一个组件使用componentWillMount来获取外部数据的例子:

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentWillMount() {
    this._asyncRequest = loadMyAsyncData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

上面的例子是有问题的:对服务器rendering(外部数据不会被使用的);对即将到来的异步rendering(请求可能被发起两次)。

大多数情况下,推荐把获取数据移到componentDidMount。

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._asyncRequest = loadMyAsyncData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

一种常见的误解是在componentWillMount中获取可以避免第一次rendering空state。实际上这观点永远是错的,因为React一直都是在componentWillMount后直接执行render。如果在componentWillMount执行时候没有数据,第一次render将仍然显示loading state,才不会管你在哪发起的获取动作。所以大多数情况,把获取数据动作移到componentDidMount没啥影响。

Note:
一些高级用法(譬如Realy之类的库)想更快预获取异步数据。这里有一个如何实现这一点的示例
长远来看,React组件获取数据的规范方式是基于JSConf Iceland上介绍的“suspense”API。无论是简单的数据获取,还是Apollo和Relay这样的库,都可以在底层使用它。它比上述任何一种解决方案都要简单,但是可能不会在16.3版本中实现。
如果是服务器端rendering,目前来看提供同步数据是必要的,componentWillMount经常被用于这个目的,但是也可以使用constructor。即将到来的suspense APIs将完全有可能使客户端和服务器端redndering都可以异步获取数据。

添加事件监听(或订阅)

这个组件挂载时候订阅了一个外部的事件分发

// Before
class ExampleComponent extends React.Component {
  componentWillMount() {
    this.setState({
      subscribedValue: this.props.dataSource.value,
    });

    // This is not safe; it can leak!
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

很不幸的是:服务器端渲染的话,这样可能引起内存泄漏(因为componentWillUnmount永远不会被调用);而且异步rendering也会(因为rendering可能在结束前被打断,导致componentWillUnmount不会被调用)。

大家经常认为componentWillMount和componentWillUnmount是成对出现,但其实不一定。只有componentDidMount已经调用了,React才保证以后会调用componentWillUnmount,你才可以使用它来清理东西。

因此,建议使用componentDidMount生命周期添加监听/订阅。

// After
class ExampleComponent extends React.Component {
  state = {
    subscribedValue: this.props.dataSource.value,
  };

  componentDidMount() {
    // Event listeners are only safe to add after mount,
    // So they won't leak if mount is interrupted or errors.
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );

    // External values could change between render and mount,
    // In some cases it may be important to handle this case.
    if (
      this.state.subscribedValue !==
      this.props.dataSource.value
    ) {
      this.setState({
        subscribedValue: this.props.dataSource.value,
      });
    }
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

有时为了响应属性变化,更新订阅器很重要。如果你使用了类似Redux或者MobX的库,其容器组件可能帮你处理了。对于应用作者,我们创建了一个小库create-subscription来帮助你。它将和React 16.3一起发布。

我们可以通过create-subscription传递订阅值,而不是像上面例子那样传递数据源订阅属性。

import {createSubscription} from 'create-subscription';

const Subscription = createSubscription({
  getCurrentValue(sourceProp) {
    // Return the current value of the subscription (sourceProp).
    return sourceProp.value;
  },

  subscribe(sourceProp, callback) {
    function handleSubscriptionChange() {
      callback(sourceProp.value);
    }

    // Subscribe (e.g. add an event listener) to the subscription (sourceProp).
    // Call callback(newValue) whenever a subscription changes.
    sourceProp.subscribe(handleSubscriptionChange);

    // Return an unsubscribe method.
    return function unsubscribe() {
      sourceProp.unsubscribe(handleSubscriptionChange);
    };
  },
});

// Rather than passing the subscribable source to our ExampleComponent,
// We could just pass the subscribed value directly:
<Subscription source={dataSource}>
  {value => <ExampleComponent subscribedValue={value} />}
</Subscription>;

注意
像Realy/Apollo之类的库,应该各自使用了和create-subscription同样的技巧在底层管理订阅(参考这里)。

基于props更新state

注意
旧的componentWillReceiveProps和新的getDerivedStateFromProps方法都会给组件带来复杂性。经常导致bugs。请考虑简单的替代方案,让组件可预见可维护。

这个组件使用了旧的componentWillReceiveProps生命周期,来基于新props值更新state。

// Before
class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
  };

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      this.setState({
        isScrollingDown:
          nextProps.currentRow > this.props.currentRow,
      });
    }
  }
}

即使上面的代码本身没什么问题,但componentWillReceiveProps生命周期经常被错误使用,带来问题。因此,这个方法将被弃用。

基于16.3版本,响应props变化更新state的推荐方式是使用:新静态getDerivedStateFromProps生命周期。(这个生命周期在组件创建后和接受新props时候被调用)(译者:还有更新state,forUpdate等时候

// After
class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

你可能注意到上面例子中props.currentRow被映射到state中(state.lastRow)。这允许getDerivedStateFromProps像componentWillReceiveProps一样读取上个props值。

你可能会想为什么我们不直接把上一个props当成参数传给getDerivedStateFromProps。我们设计这个API时候考虑到了,但是最终决定反对这样,因为两个原因:

  • 一个prevProps参数在第一次getDerivedStateFromProps被调用时候可能是null(实例化后),需要在prevProps存取时候添加if-not-null检查。
  • 不传递上一个props给这个函数是为了将来:在未来的React版本中释放内存。(如果React不需要传递上一个props给生命周期,那么他不需要保存上一个props对象在内存中)

注意
如果你在写一个共享组件,react-lifecycles-compat polyfill 允许在旧版React中使用getDerivedStateFromProps生命周期。后面有如何使用

执行外部回调

当内部state变化,此组件调用了一个外部函数

// Before
class ExampleComponent extends React.Component {
  componentWillUpdate(nextProps, nextState) {
    if (
      this.state.someStatefulValue !==
      nextState.someStatefulValue
    ) {
      nextProps.onChange(nextState.someStatefulValue);
    }
  }
}

有时候人们把componentWillUpdate用错地方,是因为怕随着componentDidUpdate触发,更新其他组件state“太晚了”。不是这样的。React保证任何一个 componentDidMount 和 componentDidUpdate 中的setState调用,在用户看到更新后的UI前,都会被排放(flushed)。一般来说,避免这样串联升级会更好,但是有些情况是必须的(例如,如果你需要在测量rendered DOM元素后定位一个提示)。

无论哪种方式,在异步模式下用componentWillUpdate都是不安全的,因为一次更新中外部调用可能会被调用两次。相反,应该使用componentDidUpdate生命周期,因为可以保证一次更新只被调用一次。
我没看懂这部分,在用户看到更新后的UI前,都会被排放(flushed)是什么意思?一次更新中外部调用可能会被调用两次怎么复现?

/ After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.someStatefulValue !==
      prevState.someStatefulValue
    ) {
      this.props.onChange(this.state.someStatefulValue);
    }
  }
}
有副作用的props改变

类似上面的例子,有时候props改变,会带来副作用。

// Before
class ExampleComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (this.props.isVisible !== nextProps.isVisible) {
      logVisibleChange(nextProps.isVisible);
    }
  }
}

像componentWillUpdate,componentWillReceiveProps可能在一次更新中被调用多次。因此,避免在这两个方法中有副作用很重要。相反,应该使用componentDidUpdate:因为它可以保证一次更新中只被调用一次。
同上,上面看不懂,这个肯定就不理解了

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.isVisible !== prevProps.isVisible) {
      logVisibleChange(this.props.isVisible);
    }
  }
}
当props更新时候获取外部数据

此组件在props改变时候获取外部数据

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.id !== this.props.id) {
      this.setState({externalData: null});
      this._loadAsyncData(nextProps.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = loadMyAsyncData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

建议将数据更新移到componentDidUpdate。你也可以使用新的getDerivedStateFromProps生命周期在rendering新props之前清除旧数据。

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(props, state) {
    // Store prevId in state so we can compare when props change.
    // Clear out previously-loaded data (so we don't render stale stuff).
    if (props.id !== state.prevId) {
      return {
        externalData: null,
        prevId: props.id,
      };
    }

    // No state update necessary
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadAsyncData(this.props.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = loadMyAsyncData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

注意
如果你使用一个支持取消动作的HTTP库,例如axios,那么在卸载时候取消一个正在进行的请求很容易。对于原生Promises,你可以使用这个方法

更新前读取DOM属性

此组件在更新前读取DOM属性,为了在列表中保持滚动位置。

class ScrollingList extends React.Component {
  listRef = null;
  previousScrollOffset = null;

  componentWillUpdate(nextProps, nextState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (this.props.list.length < nextProps.list.length) {
      this.previousScrollOffset =
        this.listRef.scrollHeight - this.listRef.scrollTop;
    }
  }

  componentDidUpdate(prevProps, prevState) {
    // If previousScrollOffset is set, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    if (this.previousScrollOffset !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight -
        this.previousScrollOffset;
      this.previousScrollOffset = null;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}>
        {/* ...contents... */}
      </div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

在上例中,componentWillUpdate用来读取DOM属性。然而异步rendering,可能在“render”阶段生命周期(例如componentWillUpdate 和 render)和“commit”阶段生命周期(例如componentDidUpdate)之间有延迟。如果用户在这期间做了一些操作例如改变窗口,componentWillUpdate中读取的scrollHeight值就过时了。
不是很理解这部分,没遇到实际场景

这个问题的解决方案是使用新的“commit”阶段生命周期,getSnapshotBeforeUpdate。这个方法在制造突变前(例如DOM更新前)会被直接调用。它可以返回一个值给React,作为突变后被直接调用的componentDidUpdate的一个参数。
这俩生命周期可以这样一起使用:

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}>
        {/* ...contents... */}
      </div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

注意
如果你在写一个共享组件,react-lifecycles-compat polyfill允许旧版React使用新的getSnapshotBeforeUpdate生命周期。后面有个例子

其他场景

我们在本文努力覆盖最常见的使用情况,我们认识到我们可能会遗漏一些。如果你用到了componentWillMount, componentWillUpdate, or componentWillReceiveProps的其他情况,并且不确定如何迁移这些旧生命周期,请file a new issue against our documentation提供你的代码和尽可能多的背景信息。当有新的替代模式时候,我们会更新这个文档。

开源项目维护人员

开源项目维护人员可能会想这些改动对共享组件意味什么。如果你实现上面的建议,有新静态getDerivedStateFromProps生命周期的组件会发生什么?你是否必须发布一个新主版并且放弃对16.2及更老版本的兼容?

幸运的是,不需要。

当React 16.3发布,我们同时发布一个新npm包,react-lifecycles-compat。这个polyfill可以让老版本React使用新 getDerivedStateFromProps 和 getSnapshotBeforeUpdate生命周期(0.14.9+)。

为了使用这个polyfill,首先添加一个依赖:

# Yarn
yarn add react-lifecycles-compat

# NPM
npm install react-lifecycles-compat --save

下一步,升级你的组件使用新的生命周期(像上面的描述)。

最后,使用polyfill兼容旧版React。

import React from 'react';
import {polyfill} from 'react-lifecycles-compat';

class ExampleComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {
    // Your state update logic here ...
  }
}

// Polyfill your component to work with older versions of React:
polyfill(ExampleComponent);

export default ExampleComponent;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容