探索 Redux4.0 版本迭代 論基礎(chǔ)談?wù)雇▽?duì)比 React context)

DJ Snake on libe

Redux 在幾天前(2018.04.18)發(fā)布了新版本,6 commits 被合入 master。從誕生起朽们,到如今 4.0 版本,Redux 保持了使用層面的平滑過(guò)渡酥郭。同時(shí)前不久华坦, React 也從 15 升級(jí)到 16 版本愿吹,開發(fā)者并不需要作出太大的變動(dòng)不从,即可“無(wú)痛升級(jí)”。但是在版本迭代的背后很多有趣的設(shè)計(jì)值得了解犁跪。Redux 此次升級(jí)同樣如此椿息。

本文將從此次版本升級(jí)展開,從源代碼改動(dòng)入手坷衍,進(jìn)行分析寝优。通過(guò)后文內(nèi)容,相信讀者能夠在 JavaScript 基礎(chǔ)層面有更深認(rèn)識(shí)枫耳。

本文支持前端初學(xué)者學(xué)習(xí)乏矾,同時(shí)更適合有 Redux 源碼閱讀經(jīng)驗(yàn)者,核心源碼并不會(huì)重復(fù)分析迁杨,更多將聚焦在升級(jí)改動(dòng)上钻心。

改動(dòng)點(diǎn)總覽

這次升級(jí)改動(dòng)點(diǎn)一共有 22 處,最主要體現(xiàn)在 TypeScript 使用铅协、CommonJS 和 ES 構(gòu)建捷沸、關(guān)于 state 拋錯(cuò)三方面上。對(duì)于工程和配置的改動(dòng)狐史,我們不再多費(fèi)筆墨痒给。主要從代碼細(xì)節(jié)入手说墨,基礎(chǔ)入手,著重分析以下幾處改動(dòng):

  • 中間件 API dispatch 參數(shù)處理苍柏;
  • applyMiddleware 改動(dòng)尼斧;
  • bindActionCreators 對(duì) this 透明化處理;
  • dispatching 時(shí)试吁,對(duì) state 的凍結(jié)突颊;
  • Plain Object 類型判斷;

話不多說(shuō)潘悼,我們直接進(jìn)入正題律秃。

applyMiddleware 參數(shù)處理

這項(xiàng)改動(dòng)由 Asvarox 提出。熟悉 Redux 源碼中 applyMiddleware.js 設(shè)計(jì)的讀者一定對(duì) middlewareAPI 并不陌生:對(duì)于每個(gè)中間件治唤,都可以感知部分 store棒动,即 middlewareAPI。這里簡(jiǎn)單展開一下:

 const middlewareAPI = {
   getState: store.getState,
   dispatch: (action) => dispatch(action)
 };
 chain = middlewares.map(middleware => middleware(middlewareAPI));
 dispatch = compose(...chain)(store.dispatch)

創(chuàng)建一個(gè)中間件 store:

let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);

我們看宾添,applyMiddleware 是個(gè)三級(jí) curry 化的函數(shù)船惨。它將陸續(xù)獲得了三個(gè)參數(shù),第一個(gè)是 middlewares 數(shù)組粱锐,[mid1, mid2, mid3, ...],第二個(gè)是 Redux 原生的 createStore扛邑,最后一個(gè)是 reducer怜浅;

applyMiddleware 利用 createStore 和 reducer 創(chuàng)建了一個(gè) store,然后 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量蔬崩。middlewares 數(shù)組通過(guò) map 方法讓每個(gè) middleware 帶著 middlewareAPI 這個(gè)參數(shù)分別執(zhí)行一遍恶座。執(zhí)行完后,獲得 chain 數(shù)組沥阳,[f1, f2, ... , fx, ...,fn]跨琳,接著 compose 將 chain 中的所有匿名函數(shù),[f1, f2, ... , fx, ..., fn]桐罕,組裝成一個(gè)新的函數(shù)脉让,即新的 dispatch,當(dāng)新 dispatch 執(zhí)行時(shí)功炮,[f1, f2, ... , fx, ..., fn] 將會(huì)從右到左依次執(zhí)行溅潜。以上解釋改動(dòng)自:pure render 專欄

好了死宣,把中間件機(jī)制簡(jiǎn)要解釋之后伟恶,我們看看這次改動(dòng)。故事源于 Asvarox 設(shè)計(jì)了一個(gè)自定義的中間件毅该,這個(gè)中間件接收的 dispatch 需要兩個(gè)參數(shù)博秫。他的“杰作”就像這樣:

const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));

對(duì)比傳統(tǒng)編寫中間件的套路:

const middleware = store => next => action => {...}

我們能清晰地看到他的這種編寫方式會(huì)有什么問(wèn)題:在原有 Redux 源碼基礎(chǔ)上潦牛,actionCreator 參數(shù)后面的 args 將會(huì)丟失。因此他提出的改動(dòng)點(diǎn)在:

     const middlewareAPI = {
       getState: store.getState,
-      dispatch: (action) => dispatch(action)
+      dispatch: (...args) => dispatch(...args)
     }

如果你好奇他為什么會(huì)這樣設(shè)計(jì)自己的中間件挡育,可以參考 #2501 號(hào) issue巴碗。我個(gè)人認(rèn)為對(duì)于需求來(lái)說(shuō),他的這種“奇葩”方式即寒,可以通過(guò)其他手段來(lái)規(guī)避橡淆;但是對(duì)于 Redux 庫(kù)來(lái)說(shuō),將 middlewareAPI.dispatch 參數(shù)展開母赵,確實(shí)是更合適的做法逸爵。

此項(xiàng)改動(dòng)我們點(diǎn)到為止,不再鉆牛角尖凹嘲。應(yīng)該學(xué)到:基于 ES6 的不定參數(shù)與展開運(yùn)算符的妙用师倔。雖然一直在說(shuō),一直在提周蹭,但在真正開發(fā)程序時(shí)趋艘,我們?nèi)匀灰獣r(shí)刻注意,并養(yǎng)成良好習(xí)慣凶朗。

基于此瓷胧,同樣的改動(dòng)也體現(xiàn)在:

   export default function applyMiddleware(...middlewares) {
  -  return (createStore) => (reducer, preloadedState, enhancer) => {
  -  const store = createStore(reducer, preloadedState, enhancer)
  +  return (createStore) => (...args) => {
  +  const store = createStore(...args)
     let dispatch = store.dispatch
     let chain = []

這項(xiàng)改動(dòng)由 jimbolla 提出。

bindActionCreators 對(duì) this 透明化處理

Redux 中的 bindActionCreators棚愤,達(dá)到 dispatch 將 action 包裹起來(lái)的目的搓萧。這樣通過(guò) bindActionCreators 創(chuàng)建的方法,可以直接調(diào)用 dispatch(action) (隱式調(diào)用)遇八∶妫可能很多開發(fā)者并不常用,所以這里稍微展開刃永,在 action.js 文件中, 我們定義了兩個(gè) action creators:

function action1(){
  return {
   type:'type1'
  }
}
function action2(){
  return {
   type:'type2'
  }
}

在另一文件 SomeComponent.js 中羊精,我們便可以直接使用:

import { bindActionCreators } from 'redux';
import * as oldActionCreator from './action.js'

class C1 extends Component {
  constructor(props) { 
    super(props);

    const {dispatch} = props;
    this.boundActionCreators = bindActionCreators(oldActionCreator, dispatch);
  }

  componentDidMount() {
    // 由 react-redux 注入的 dispatch:
    let { dispatch } = this.props;
    let action = TodoActionCreators.addTodo('Use Redux');
    dispatch(action);
  }

  render() {
    // ...
    let { dispatch } = this.props;
    let newAction = bindActionCreators(oldActionCreator, dispatch)
    return <Child {...newAction}></child>
  }
}

這樣一來(lái)斯够,我們?cè)谧咏M件 Child 中,直接調(diào)用 newAction.action1 就相當(dāng)于調(diào)用 dispatch(action1)喧锦,如此做的好處在于:沒(méi)有 store 和 dispatch 的組件读规,也可以進(jìn)行動(dòng)作分發(fā)。

一般這個(gè) API 應(yīng)用不多燃少,至少筆者不太常用束亏。因此上面做一個(gè)簡(jiǎn)單介紹。有經(jīng)驗(yàn)的開發(fā)中一定不難猜出 bindActionCreators 源碼做了什么阵具,連帶著這次改動(dòng):

function bindActionCreator(actionCreator, dispatch) {
-  return (...args) => dispatch(actionCreator(...args))
+  return function() { return dispatch(actionCreator.apply(this, arguments)) }
 }

我們看這次改動(dòng)碍遍,對(duì) actionCreator 使用 apply 方法定铜,明確地進(jìn)行 this 綁定。那么這樣做的意義在哪里呢怕敬?

我舉一個(gè)例子揣炕,想象我們對(duì)原始的 actionCreator 進(jìn)行 this 綁定,并使用 bindActionCreators 方法:

const uniqueThis = {};
function actionCreator() {
  return { type: 'UNKNOWN_ACTION', this: this, args: [...arguments] }
};
const action = actionCreator.apply(uniqueThis,argArray);
const boundActionCreator = bindActionCreators(actionCreator, store.dispatch);
const boundAction = boundActionCreator.apply(uniqueThis,argArray);

我們應(yīng)該期望 boundAction 和 action 一致东跪;且 boundAction.this 和 uniqueThis 一致畸陡,都等同于 action.this。這如此的期望下虽填,這樣的改動(dòng)無(wú)疑是必須的丁恭。

對(duì) state 的凍結(jié)

Dan Abramov 認(rèn)為,在 reducer 中使用 getState() 和 subscribe() 方法是一種反模式斋日。store.getState 的調(diào)用會(huì)使得 reducer 不純涩惑。事實(shí)上,原版已經(jīng)在 reducer 執(zhí)行過(guò)程中桑驱,禁用了 dispatch 方法竭恬。源碼如下:

  function dispatch(action) {
    // ...

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    var listeners = currentListeners = nextListeners
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]()
    }

    return action
  }

同時(shí),這次修改在 getState 方法以及 subscribe熬的、unsubscribe 方法中進(jìn)行了同樣的凍結(jié)處理:

 if (isDispatching) {
  throw new Error(
    'You may not call store.subscribe() while the reducer is executing. ' +
      'If you would like to be notified after the store has been updated, subscribe from a ' +
      'component and invoke store.getState() in the callback to access the latest state. ' +
      'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
  )
}

筆者認(rèn)為痊硕,這樣的做法毫無(wú)爭(zhēng)議。顯式拋出異常無(wú)意是合理的押框。

Plain Object 類型判斷

Plain Object 是一個(gè)非常有趣的概念岔绸。這次改動(dòng)圍繞判斷 Plain Object 的性能進(jìn)行了激烈的討論。最終將引用 lodash isPlainObject 的判斷方法改為 ./utils/isPlainObject 中自己封裝的做法:

- import isPlainObject from 'lodash/isPlainObject';
+ import isPlainObject from './utils/isPlainObject'

簡(jiǎn)單來(lái)說(shuō)橡伞,Plain Object:

指的是通過(guò)字面量形式或者new Object()形式定義的對(duì)象盒揉。

Redux 這次使用了以下代碼來(lái)進(jìn)行判斷:

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

如果讀者對(duì)上述代碼不理解,那么需要補(bǔ)一下原型兑徘、原型鏈的知識(shí)刚盈。簡(jiǎn)單來(lái)說(shuō),就是判斷 obj 的原型鏈有幾層挂脑,只有一層就返回 true藕漱。如果還不理解,可以參考下面示例代碼:

function Foo() {}

// obj 不是一個(gè) plain object
var obj = new Foo();

console.log(typeof obj, obj !== null);

let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
  proto = Object.getPrototypeOf(proto)
}

// false
var isPlain = Object.getPrototypeOf(obj) === proto;
console.log(isPlain);

而 loadash 的實(shí)現(xiàn)為:

function isPlainObject(value) {
  if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
    return false
  }
  if (Object.getPrototypeOf(value) === null) {
    return true
  }
  let proto = value
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }
  return Object.getPrototypeOf(value) === proto
}

export default isPlainObject

isObjectLike 源碼:

function isObjectLike(value) {
  return typeof value == 'object' && value !== null
}

baseGetTag 源碼:

const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  if (!(symToStringTag && symToStringTag in Object(value))) {
    return toString.call(value)
  }
  const isOwn = hasOwnProperty.call(value, symToStringTag)
  const tag = value[symToStringTag]
  let unmasked = false
  try {
    value[symToStringTag] = undefined
    unmasked = true
  } catch (e) {}

  const result = toString.call(value)
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag
    } else {
      delete value[symToStringTag]
    }
  }
  return result
}

根據(jù) timdorr 給出的對(duì)比結(jié)果崭闲,dispatch 方法中:

master: 4690.358ms
nodash: 82.821ms

這一組 benchmark 引發(fā)的討論自然少不了肋联,也引出來(lái)了 Dan Abramov。筆者對(duì)此不發(fā)表任何意見(jiàn)刁俭,感興趣的同學(xué)可自行研究橄仍。從結(jié)果上來(lái)看,摒除了部分對(duì) lodash 的依賴,在性能表現(xiàn)上說(shuō)服力增強(qiáng)侮繁。

展望和總結(jié)

提到 Redux 發(fā)展虑粥,自然離不開 React,React 新版本一經(jīng)推出鼎天,極受追捧舀奶。尤其是 context 這樣的新 API,某些開發(fā)者認(rèn)為將逐漸取代 Redux斋射。

筆者認(rèn)為育勺,圍繞 React 開發(fā)應(yīng)用,數(shù)據(jù)狀態(tài)管理始終是一個(gè)極其重要的話題罗岖。但是 React context 和 Redux 并不是完全對(duì)立的涧至。

首先 React 新特性 context 在大型數(shù)據(jù)應(yīng)用的前提下,并不會(huì)減少模版代碼桑包。而其 Provider 和 Consumer 的一一對(duì)應(yīng)特性南蓬,即 Provider 和 Consumer 必須來(lái)自同一次 React.createContext 調(diào)用(可以用 hack 方式解決此“局限”),仿佛 React 團(tuán)隊(duì)對(duì)于此特性的發(fā)展方向設(shè)計(jì)主要體現(xiàn)在小型狀態(tài)管理上哑了。如果需要實(shí)現(xiàn)更加靈活和直接的操作赘方,Redux 也許會(huì)是更好的選擇。

其次弱左,Redux 豐富的生態(tài)以及中間件等機(jī)制窄陡,決定了其在很大程度上具有不可替代性。畢竟拆火,已經(jīng)使用 Redux 的項(xiàng)目跳夭,遷移成本也將是極大的,至少需要開發(fā)中先升級(jí) React 以支持新版 context 吧们镜。

最后币叹,Redux 作為一個(gè)“發(fā)布訂閱系統(tǒng)”,完全可以脫離 React 而單獨(dú)存在模狭,這樣的基因也決定了其后天與 React 本身 context 不同的性征颈抚。

我認(rèn)為,新版 React context 是對(duì) React 本身“短板”的長(zhǎng)線補(bǔ)充和完善胞皱,未來(lái)大概率也會(huì)有所打磨調(diào)整邪意。Redux 也會(huì)進(jìn)行一系列迭代,但就如同這次版本升級(jí)一樣反砌,將趨于穩(wěn)定,更多的是細(xì)節(jié)上調(diào)整萌朱。

退一步講宴树,React context 的確也和 Redux 有千絲萬(wàn)縷的聯(lián)系。任何類庫(kù)或者框架都具有其短板晶疼,Redux 同樣也如此酒贬。我們完全可以使用新版 React context又憨,在使用層面來(lái)規(guī)避 Redux 的一些劣勢(shì),模仿 Redux 所能做到的一切锭吨。如同 didierfranc 的 react-waterfall蠢莺,國(guó)內(nèi)@方正的 Rectx,都是基于新版 React context 的解決方案零如。

最后躏将,我很贊同@誠(chéng)身所說(shuō):
選擇用什么樣的工具從來(lái)都不是決定一個(gè)開發(fā)團(tuán)隊(duì)成敗的關(guān)鍵,根據(jù)業(yè)務(wù)場(chǎng)景選擇恰當(dāng)?shù)墓ぞ呖祭伲⒗霉ぞ叻催^(guò)來(lái)約束開發(fā)者祸憋,最終達(dá)到控制整體項(xiàng)目復(fù)雜度的目的,才是促進(jìn)一個(gè)開發(fā)團(tuán)隊(duì)不斷提升的核心動(dòng)力肖卧。

沒(méi)錯(cuò)蚯窥,真正對(duì)項(xiàng)目起到?jīng)Q定性作用的還是是開發(fā)者本身,完善基礎(chǔ)知識(shí)塞帐,提升開發(fā)技能拦赠,讓我們從 Redux 4.0 的改動(dòng)看起吧。

廣告時(shí)間:
如果你對(duì)前端發(fā)展葵姥,尤其對(duì) React 技術(shù)棧感興趣:我的新書中荷鼠,也許有你想看到的內(nèi)容。關(guān)注作者 Lucas HC牌里,新書出版將會(huì)有送書活動(dòng)颊咬。

Happy Coding!

PS: 作者 Github倉(cāng)庫(kù)知乎問(wèn)答鏈接 歡迎各種形式交流!

我的其他幾篇關(guān)于React技術(shù)棧的文章:

從setState promise化的探討 體會(huì)React團(tuán)隊(duì)設(shè)計(jì)思想

React 應(yīng)用設(shè)計(jì)之道 - curry 化妙用

組件復(fù)用那些事兒 - React 實(shí)現(xiàn)按需加載輪子

通過(guò)實(shí)例牡辽,學(xué)習(xí)編寫 React 組件的“最佳實(shí)踐”

React 組件設(shè)計(jì)和分解思考

從 React 綁定 this喳篇,看 JS 語(yǔ)言發(fā)展和框架設(shè)計(jì)

做出Uber移動(dòng)網(wǎng)頁(yè)版還不夠 極致性能打造才見(jiàn)真章**

React+Redux打造“NEWS EARLY”單頁(yè)應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市态辛,隨后出現(xiàn)的幾起案子麸澜,更是在濱河造成了極大的恐慌,老刑警劉巖奏黑,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炊邦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡熟史,警方通過(guò)查閱死者的電腦和手機(jī)馁害,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蹂匹,“玉大人碘菜,你說(shuō)我怎么就攤上這事。” “怎么了忍啸?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵仰坦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我计雌,道長(zhǎng)悄晃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任凿滤,我火速辦了婚禮妈橄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸭巴。我一直安慰自己眷细,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布鹃祖。 她就那樣靜靜地躺著溪椎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪恬口。 梳的紋絲不亂的頭發(fā)上校读,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音祖能,去河邊找鬼歉秫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛养铸,可吹牛的內(nèi)容都是我干的雁芙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼钞螟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兔甘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起鳞滨,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洞焙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拯啦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澡匪,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年褒链,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唁情。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甫匹,死狀恐怖荠瘪,靈堂內(nèi)的尸體忽然破棺而出夯巷,到底是詐尸還是另有隱情赛惩,我是刑警寧澤哀墓,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站喷兼,受9級(jí)特大地震影響篮绰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜季惯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一吠各、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧勉抓,春花似錦贾漏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至隐圾,卻和暖如春伍掀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暇藏。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工蜜笤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盐碱。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓把兔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瓮顽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子县好,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容