【譯文】在這個(gè)人為例子下镰惦,MobX——閃耀迷守,Redux——不適合

原文:https://hackernoon.com/an-artificial-example-where-mobx-really-shines-and-redux-is-not-really-suited-for-it-1a58313c0c70
譯者:SunnyEver0

首先,我要申明一下旺入,這個(gè)標(biāo)題并不是引戰(zhàn)的兑凿,也不是說MobX是比Redux更好的狀態(tài)管理庫。
Redux和MobX我都在使用眨业,這兩個(gè)我都很喜歡急膀,如果有需要,我會再次使用它們龄捡。
這個(gè)文章下的例子卓嫂,我們可以看到如果想要MobX和Redux都在同一個(gè)呈現(xiàn)水平的話,我們使用MobX可以愉悅輕松地開發(fā)但使用Redux時(shí)我們卻步步維艱聘殖。如果我不清楚晨雳,這是一個(gè)不公平的比較。
因?yàn)檫@篇文章也并不是將這兩個(gè)進(jìn)行綜合的比較也不沒有完全考慮在一個(gè)真實(shí)的境況下奸腺。所以請不要將這個(gè)作為一個(gè)基準(zhǔn)說:”讓我們使用它吧餐禁,它更加快速⊥徽眨”
我不相信這個(gè)世界上存在最好的工具帮非。我相信任何工具都有其用武之地的地方,也有其不適合的地方。
好末盔,一切是時(shí)候了筑舅。我做這個(gè)實(shí)驗(yàn)是為可更好地理解這兩個(gè)工具,當(dāng)我們需要使用這個(gè)工具的時(shí)候陨舱,我能夠選擇正確的工具做正確的事翠拣。
好,我們現(xiàn)在繼續(xù)游盲。

使用版本

為什么要列出版本误墓?因?yàn)樵贘avaScript這個(gè)世界里面,隨著不斷的優(yōu)化益缎,我相信這篇文章會很快過時(shí)谜慌。

  • react@15.4.1, react-dom@15.4.1
  • redux@3.6.0, react-redux@5.0.0-rc.1
  • mobx@2.6.4, mobx-react@4.0.3

課題:pixel paint

我通過React構(gòu)建了一個(gè)"pixel paint"的應(yīng)用,它通過canvas渲染了一個(gè)可繪制像素的128*128格子莺奔。
你可以通過鼠標(biāo)漂浮在上面繪制任意的像素畦娄。
我們將會并排渲染兩個(gè)canvas畫板,而且這兩個(gè)畫板共享一樣的圖片弊仪。否則熙卡,我們可以自己通過component自身state進(jìn)行狀態(tài)管理,而不需要任何狀態(tài)容器庫進(jìn)行管理励饵。

pixel畫板.gif

每一個(gè)像素點(diǎn)通過<div>進(jìn)行展示驳癌,所以一共有1281282=32768個(gè) DOM節(jié)點(diǎn)去渲染和更新。
這個(gè)實(shí)驗(yàn)在進(jìn)行的過程中非常慢役听。
注意:所以的測試均是通過發(fā)布包進(jìn)行颓鲜。

MobX 版

這是容器store(我避免使用了裝飾器語法,因?yàn)樵趯戇@篇文章的時(shí)候典予,該語法還為待定提案)甜滨。

const store = observable({
  pixels: asMap({ }),
  isActive (i, j) {
    return !!store.pixels.get(i + ',' + j)
  },
  toggle: action(function toggle (i, j) {
    store.pixels.set(i + ',' + j, !store.isActive(i, j))
  })
})

渲染每一個(gè)像素點(diǎn)的canvas

function MobXCanvas () {
  const items = [ ]
  for (let i = 0; i < 128; i++) {
    for (let j = 0; j < 128; j++) {
      items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
    }
  }
  return <div>{items}</div>
}

管理每一個(gè)像素state的狀態(tài)容器

const PixelContainer = observer(function PixelContainer ({ i, j }) {
  return <Pixel
    i={I}
    j={j}
    active={store.isActive(i, j)}
    onToggle={() => store.toggle(i, j)}
  />
})

這里是演示結(jié)果。在屏幕的右上角瘤袖,有mobx-react-devtools可以使用衣摩。
以下為演示結(jié)果:

mobx-profiler.png

通過統(tǒng)計(jì)的餅狀圖,我們可以看到scripting只占到一小部分百分比捂敌。大多數(shù)時(shí)間是花在了rendering和painting上面艾扮。
所以表明MobX做得很棒!

Redux 版(初次)

這是reducer:

const store = createStore((state = Immutable.Map(), action) => {
  if (action.type === 'TOGGLE') {
    const key = action.i + ',' + action.j
    return state.set(key, !state.get(key))
  }
  return state
})

selector:

const selectActive = (state, i, j) => state.get(i + ',' + j)

action creator:

const toggle = (i, j) => ({ type: 'TOGGLE', i, j })

Redux store已經(jīng)準(zhǔn)備就緒占婉。
給每一個(gè) pixel提供的canvas store:

function ReduxCanvas () {
  const items = [ ]
  for (let i = 0; i < 128; i++) {
    for (let j = 0; j < 128; j++) {
      items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
    }
  }
  return <Provider store={store}>
    <div>
      {items}
    </div>
  </Provider>
}

每一個(gè)pixel connect store:

const PixelContainer = connect(
  (state, ownProps) => ({
    active: selectActive(state, ownProps.i, ownProps.j)
  }),
  (dispatch, ownProps) => ({
    onToggle: () => dispatch(toggle(ownProps.i, ownProps.j))
  })
)(Pixel)

這是演示結(jié)果

你可以使用 redux-devtools-extension 來檢查store state泡嘴,actions 和一直耗時(shí)追蹤∧婕茫可以看出這種方式下性能大大低于了MobX版酌予。讓我們一起看下它的圖形結(jié)果:

redux1-result

大量的時(shí)間損耗在了執(zhí)行JS上磺箕。接近50%的比例.這是不好的,為什么會執(zhí)行這么長時(shí)間呢抛虫?t
我們一起來剖析一下:


redux-analysis.png

這個(gè)展示了Redux的訂閱模式是怎么工作的滞磺。

在以上的例子中,每一個(gè)pixel都connectstore莱褒,也意味著它對store的數(shù)據(jù)的數(shù)據(jù)進(jìn)行了subscribe。只要store發(fā)生了變化涎劈,每一個(gè)訂閱者都會進(jìn)行檢查是否需要進(jìn)行更新渲染广凸。

這個(gè)也意味著,我們只改變一個(gè)pixel蛛枚,則Redux會通知所有的32768個(gè)觀察者

這個(gè)同Angular 1的臟檢測類似谅海。通過這個(gè)也建議在使用Redux時(shí):不要在屏幕上渲染太多視圖。

通過Redux蹦浦,你只能對整個(gè)store的狀態(tài)進(jìn)行訂閱扭吁,因?yàn)樗膕ubtree是一個(gè)普通而陳舊的JavaScript對象,我們不能夠訂閱它盲镶。

但是使用MobX侥袜,每一塊的state都是有自己內(nèi)部進(jìn)行觀察。在我們的MobX版本中溉贿,每一個(gè)pixel都訂閱了自己本身的state subtree枫吧。這也是它效率高的原因。

第二次嘗試:單一subscriber

所以宇色,太多的觀察者是一個(gè)問題九杂。這一次,我將保證這里只有一個(gè)subscriber宣蠕。
這里例隆,我們創(chuàng)建了一個(gè)Canvas組件,它可以訂閱整個(gè)store并渲染所有的pixels抢蚀。

function ReduxCanvas () {
  return <Provider store={store}><Canvas /></Provider>
}

const Canvas = connect(
  (state) => ({ state }),
  (dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function Canvas ({ state, onToggle }) {
  const items = [ ]
  for (let i = 0; i < 128; i++) {
    for (let j = 0; j < 128; j++) {
      items.push(<PixelContainer
        i={I}
        j={j}
        active={selectActive(state, i, j)}
        onToggle={onToggle}
        key={i + ',' + j}
      />)
    }
  }
  return <div>{items}</div>
})

PixelContainer組件將其從Canvas組件中獲取的props傳遞給Pixel

class PixelContainer extends React.PureComponent {
  constructor (props) {
    super(props)
    this.handleToggle = this.handleToggle.bind(this)
  }
  handleToggle () {
    this.props.onToggle(this.props.i, this.props.j)
  }
  render () {
    return <Pixel
      i={this.props.i}
      j={this.props.j}
      active={this.props.active}
      onToggle={this.handleToggle}
    />
  }
}

這里是結(jié)果

redux-single-subscriber.png

從這個(gè)版本可以看出我們上次的嘗試是有多么的糟糕镀层。
讓我們看看發(fā)生了什么。

redux-single-subscriber-analysis.png

問題好像出在了我們的繪圖Canvas上皿曲。它是唯一一個(gè)訂閱store的觀察者鹿响,并對16384個(gè)pixels的狀態(tài)進(jìn)行管理。
每次store進(jìn)行dispatch action谷饿,它需要傳遞正確的props給16384個(gè)pixels進(jìn)行渲染惶我。
這個(gè)意味著在每個(gè)canvas中,React會對React.createElement會對16384次調(diào)用博投,并嘗試去調(diào)和16384個(gè)子組件绸贡。這不是一件好事。
我們可以做得更好。

第三次嘗試:平衡的狀態(tài)樹

Redux的一個(gè)主要優(yōu)勢在于它不可變的state tree(它開啟可一些酷的功能听怕,比如無痛熱加載和time-traveling)
它證明了我們構(gòu)建數(shù)據(jù)的方式和我們的觀點(diǎn)并不是一成不變的捧挺。

一個(gè)不可變的狀態(tài)樹儲存于一個(gè)平衡的樹中
,這是最好的尿瞭。我在這篇文章中討論了這個(gè)觀點(diǎn):
immutable-js-persistent-data-structures-and-structural-sharing
譯者注:這是文章的主題是 Why use Immutable.js instead of normal Javascript object? 有興趣的朋友可以了解一下闽烙。

所以,我們在這里也這樣做吧!

我們可以把我們的canvas分為一下四個(gè)象限:


quadrants_1.png

當(dāng)我們需要去改變一個(gè)pixel

quadrants_2.png

我們只需更新這個(gè)區(qū)塊声搁,不用去考慮其他區(qū)塊黑竞。

quadrants_3.png

與重新渲染所有的16384 pixels,我們只需重新渲染 64×64=4096個(gè) pixels疏旨。我們提升了75%的效率很魂。
但是4096仍然是一個(gè)較大的數(shù)量。所以我們可以繼續(xù)遞歸地分割的我們的渲染區(qū)塊直到最后的1×1 pixel檐涝。

quadrants_4.png

為了通過這種當(dāng)時(shí)來更新組件遏匆,我們需要通過同樣的方式來構(gòu)造我們的state,當(dāng)state改變時(shí)谁榜,我們可以直接用===來判斷區(qū)塊的state是否已經(jīng)改變

下面是初始化state的代碼(recursively)

const generateInitialState = (size) => (size === 1
  ? false
  : Immutable.List([
    generateInitialState(size / 2),
    generateInitialState(size / 2),
    generateInitialState(size / 2),
    generateInitialState(size / 2)
  ])
)

現(xiàn)在我們的state是一個(gè)遞歸嵌套的樹幅聘,每個(gè)pixel也不是由這樣的坐標(biāo)----(58, 52)進(jìn)行標(biāo)識,而是使用這樣的路徑---- (1, 3, 3, 2, 0, 2, 1) 去標(biāo)識窃植。

但要在屏幕上展示喊暖,我們需要通過這個(gè)path能找到它的坐標(biāo)。

function keyPathToCoordinate (keyPath) {
  let i = 0
  let j = 0
  for (const quadrant of keyPath) {
    i <<= 1
    j <<= 1
    switch (quadrant) {
      case 0: j |= 1; break
      case 2: i |= 1; break
      case 3: i |= 1; j |= 1; break
      default:
    }
  }
  return [ i, j ]
}

// 譯者注:如果這種二進(jìn)制構(gòu)造不好理解撕瞧,可以用下面這種迭代還原法

function keyPathToCoordinate(keyPath) {
  let i = 0, j = 0;
  for (let index = 0; index < keyPath.length; index++) {
    let path = keyPath[index];
    let power = keyPath.length - index - 1;
    if (path === 0) {
      j += Math.pow(2, power);
    } else if (path === 2) {
      i += Math.pow(2, power);
    } else if (path === 3) {
      i += Math.pow(2, power);
      j += Math.pow(2, power);
    }
  }
  return [i ,j];
}

我們同樣需要能夠通過坐標(biāo)獲得path:

function coordinateToKeyPath (i, j) {
  const keyPath = [ ]
  for (let threshold = 64; threshold > 0; threshold >>= 1) {
    keyPath.push(i < threshold
      ? j < threshold ? 1 : 0
      : j < threshold ? 2 : 3
    )
    i %= threshold
    j %= threshold
  }
  return keyPath
}

現(xiàn)在我們需要更新我們的reducer變成這樣:

const store = createStore(
  function reducer (state = generateInitialState(128), action) {
    if (action.type === 'TOGGLE') {
      const keyPath = coordinateToKeyPath(action.i, action.j)
      return state.updateIn(keyPath, (active) => !active)
      //           |
      //           This is why I use Immutable.js:
      //           So that I can use this method.
    }
    return state
  }
)

然后我們構(gòu)造一個(gè)組件來遍歷這個(gè)樹并將所有的內(nèi)容放置在這里陵叽。GridContainer與store相連接并對最外層的Grid進(jìn)行渲染。

function ReduxCanvas () {
  return <Provider store={store}><GridContainer /></Provider>
}

const GridContainer = connect(
  (state, ownProps) => ({ state }),
  (dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function GridContainer ({ state, onToggle }) {
  return <Grid keyPath={[ ]} state={state} onToggle={onToggle} />
})

然后每個(gè)Grid遞歸渲染一個(gè)較小的版本丛版,直到它到達(dá)一個(gè)葉子(白色/黑色1x1像素畫布)

class Grid extends React.PureComponent {
  constructor (props) {
    super(props)
    this.handleToggle = this.handleToggle.bind(this)
  }
  shouldComponentUpdate (nextProps) {
    // Required since we construct a new `keyPath` every render
    // but we know that each grid instance will be rendered with
    // a constant `keyPath`. Otherwise we need to memoize the
    // `keyPath` for each children we render to remove this
    // "escape hatch."
    return this.props.state !== nextProps.state
  }
  handleToggle () {
    const [ i, j ] = keyPathToCoordinate(this.props.keyPath)
    this.props.onToggle(i, j)
  }
  render () {
    const { keyPath, state } = this.props
    if (typeof state === 'boolean') {
      const [ i, j ] = keyPathToCoordinate(keyPath)
      return <Pixel
        i={I}
        j={j}
        active={state}
        onToggle={this.handleToggle}
      />
    } else {
      return <div>
        <Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 0 ]} state={state.get(0)} />
        <Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 1 ]} state={state.get(1)} />
        <Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 2 ]} state={state.get(2)} />
        <Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 3 ]} state={state.get(3)} />
      </div>
    }
  }
}

這里是結(jié)果

quardants_result.png

哇哦巩掺,我們終于恢復(fù)了速度!它同之前的MobX版本一樣快页畦,而且擁有熱加載和時(shí)間追蹤功能胖替。
我們的DOM樹看起來也更像是樹了:


quadrants_dom_tree.png

對比所有之前的方法:


quadrants_previous.png

終極優(yōu)化方案

因?yàn)樗鼘?shí)用性不高,我沒有進(jìn)行編碼豫缨。
怎么做呢:針對每一個(gè)pixel創(chuàng)建一個(gè)Redux Store独令。我沒有進(jìn)行測試因?yàn)槲掖_信這種方法是Redux最快的方式。
但你使用這種方式時(shí)好芭,Redux的很多優(yōu)點(diǎn)都會被丟棄燃箭。舉個(gè)例子,Redux DevTools 可能閃退舍败。而且時(shí)間追蹤對于么一個(gè)pixel不是那么有用招狸,不是嗎敬拓?

更好的方式?

以上即為所有我所考慮到的地方裙戏。
如果你有更好的優(yōu)雅解決方案乘凸,還請聯(lián)系我。

Updates:
  • Dan Abramov 提交了他的版本累榜,它的性能比V1的要好但低于V3的性能而且比較簡單易懂营勤。
    譯者注:Dan Abramov為Redux的作者

結(jié)論

這是一個(gè)有趣的實(shí)驗(yàn)。
我們大多數(shù)在優(yōu)化命令式算法方面擁有扎實(shí)的知識壹罚,但對于在不可變數(shù)據(jù)上的應(yīng)用層面葛作,如果我們不懂性能影響,優(yōu)化它會變成一個(gè)挑戰(zhàn)渔嚷。
一旦我們優(yōu)化了Redux版本,我們可以看到性能優(yōu)化后導(dǎo)致了代碼的可讀性降低稠曼。上面寫的代碼真的有點(diǎn)糟糕形病!
就像Dan Abramov說的一樣,Redux提供了一種折中的方案(MobX也是)霞幅。所以你會在不失去熱加載和時(shí)間追蹤功能的前提下漠吻,用代碼的清晰度和可讀性去交換性能嗎?


在我的midi-instrument項(xiàng)目下司恳,因?yàn)樗鼤贛obileSafari下運(yùn)行途乃,所以性能是很重要的,特別是當(dāng)一個(gè)instrument包含了幾百個(gè)buttons的時(shí)候扔傅。
我同樣也希望能夠在使用不可變數(shù)據(jù)時(shí)快速創(chuàng)建新的原型耍共,而不用去擔(dān)心性能影響。
我也發(fā)現(xiàn)hot-reloading 和 time-traveling在這個(gè)項(xiàng)目中并不是那么適用猎塞。大多數(shù)state只持續(xù)了短暫的幾秒鐘试读,我的項(xiàng)目也足夠的小我可以自己手動(dòng)刷新界面。
所以我最后很開心地使用了MobX荠耽。


在我正在做的這個(gè)旋律游戲--Bemuse中钩骇,我能感覺到使用不可變數(shù)據(jù)能夠幫助我寫一些簡單易用的代碼。
我并不擔(dān)心不確定的state突然變更铝量,因?yàn)檫@里并不會有倘屹。
這里不會有大量的數(shù)據(jù)需要渲染,所以我可能不需要像上面的例子一樣去優(yōu)化它慢叨。
使用Redux DevTools纽匙,讓所有state更新都集中顯示中一個(gè)固定地方,也讓我受益匪淺拍谐,這里哄辣,Redux盡情發(fā)揮了它的價(jià)值请梢。
所以我很開心地在這個(gè)項(xiàng)目中使用了Redux。

一個(gè)不公平的性能比較

這個(gè)比較一開始就是不公平的力穗,當(dāng)我使用函數(shù)式方法(Redux)同命令式方法(MobX)進(jìn)行比較時(shí)毅弧。
在1996年,Chris Okasaki在它140頁的論文中得出了結(jié)論--“純函數(shù)數(shù)據(jù)結(jié)構(gòu)”当窗,如下:

無論編譯器怎么進(jìn)步够坐,只要命令式的算法優(yōu)于函數(shù)式的算法,函數(shù)式程序絕不會比它的同行--命令式更快崖面。

In that thesis (now available as a book), he tried to make data structures in functional programming as efficient its imperative counterpart.
在那篇論文中(現(xiàn)在已經(jīng)發(fā)版出書)元咙,他嘗試通過構(gòu)造數(shù)據(jù)結(jié)構(gòu)讓函數(shù)式編程比命令式編程更有效。

本文提供了大量的函數(shù)式數(shù)據(jù)結(jié)構(gòu)巫员,它可以漸近地同命令式編程一樣有效庶香。

我不會停止函數(shù)式編程因?yàn)樗粫衩钍剿惴敲纯臁?br> 這完全取決于利弊的權(quán)衡。這是為什么我不會說"讓我們使用Redux/MobX做所有事情吧简识!"這也是為什么當(dāng)人們問我“2017年了案站,我應(yīng)該使用MobX還是Redux该肴?”卻沒有給出特定的場景,我也不會給出確定答案的原因。這也是我寫這篇文章的原因富岳。

謝謝閱讀进副!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末民轴,一起剝皮案震驚了整個(gè)濱河市解滓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌立由,老刑警劉巖轧钓,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锐膜,居然都是意外死亡聋迎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門枣耀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霉晕,“玉大人,你說我怎么就攤上這事捞奕∥撸” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵颅围,是天一觀的道長伟葫。 經(jīng)常有香客問我,道長院促,這世上最難降的妖魔是什么筏养? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任斧抱,我火速辦了婚禮,結(jié)果婚禮上渐溶,老公的妹妹穿的比我還像新娘辉浦。我一直安慰自己,他們只是感情好茎辐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布宪郊。 她就那樣靜靜地躺著,像睡著了一般拖陆。 火紅的嫁衣襯著肌膚如雪弛槐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天依啰,我揣著相機(jī)與錄音乎串,去河邊找鬼。 笑死速警,一個(gè)胖子當(dāng)著我的面吹牛叹誉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坏瞄,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼桂对,長吁一口氣:“原來是場噩夢啊……” “哼甩卓!你這毒婦竟也來了鸠匀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤逾柿,失蹤者是張志新(化名)和其女友劉穎缀棍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體机错,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡爬范,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弱匪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片青瀑。...
    茶點(diǎn)故事閱讀 39,764評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萧诫,靈堂內(nèi)的尸體忽然破棺而出斥难,到底是詐尸還是另有隱情,我是刑警寧澤帘饶,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布哑诊,位于F島的核電站,受9級特大地震影響及刻,放射性物質(zhì)發(fā)生泄漏镀裤。R本人自食惡果不足惜竞阐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暑劝。 院中可真熱鬧骆莹,春花似錦、人聲如沸铃岔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毁习。三九已至智嚷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纺且,已是汗流浹背盏道。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留载碌,地道東北人猜嘱。 一個(gè)月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像嫁艇,于是被迫代替她去往敵國和親朗伶。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評論 2 354

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