React-Redux分析

React-Redux分析

Redux,作為大型React應用狀態管理最常用的工具,其概念理論和實踐都是很值得我們學習,分析然后在實踐中深入了解的,對前端開發者能力成長很有幫助。本篇計劃結合Redux容器組件和展示型組件的區別對比以及Redux與React應用最常見的連接庫,react-redux源碼分析,以期達到對Redux和React應用的更深層次理解。

前言

react-redux庫提供Provider組件通過context方式向應用注入store,然后可以使用connect高階方法,獲取并監聽store,然后根據store state和組件自身props計算得到新props,注入該組件,并且可以通過監聽store,比較計算出的新props判斷是否需要更新組件。

react與redux應用結構

Provider

首先,react-redux庫提供Provider組件將store注入整個React應用的某個入口組件,通常是應用的頂層組件。Provider組件使用context向下傳遞store:

// 內部組件獲取redux store的鍵
const storeKey = 'store'
// 內部組件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
  // 聲明context,注入store和可選的發布訂閱對象
  getChildContext() {
    return { [storeKey]: this[storeKey], [subscriptionKey]: null }
  }

  constructor(props, context) {
    super(props, context)
    // 緩存store
    this[storeKey] = props.store;
  }

  render() {
    // 渲染輸出內容
    return Children.only(this.props.children)
  }
}

Example

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './components/App'
import reducers from './reducers'

// 創建store
const store = createStore(todoApp, reducers)

// 傳遞store作為props給Provider組件;
// Provider將使用context方式向下傳遞store
// App組件是我們的應用頂層組件
render(
  <Provider store={store}>
    <App/>
  </Provider>, document.getElementById('app-node')
)

connect方法

在前面我們使用Provider組件將redux store注入應用,接下來需要做的是連接組件和store。而且我們知道Redux不提供直接操作store state的方式,我們只能通過其getState訪問數據,或通過dispatch一個action來改變store state。

這也正是react-redux提供的connect高階方法所提供的能力。

Example

container/TodoList.js

首先我們創建一個列表容器組件,在組件內負責獲取todo列表,然后將todos傳遞給TodoList展示型組件,同時傳遞事件回調函數,展示型組件觸發諸如點擊等事件時,調用對應回調,這些回調函數內通過dispatch actions來更新redux store state,而最終將store和展示型組件連接起來使用的是react-redux的connect方法,該方法接收

import {connect} from 'react-redux'
import TodoList from 'components/TodoList.jsx'

class TodoListContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {todos: null, filter: null}
  }
  handleUpdateClick (todo) {
    this.props.update(todo);  
  }
  componentDidMount() {
    const { todos, filter, actions } = this.props
    if (todos.length === 0) {
      this.props.fetchTodoList(filter);
    }
  render () {
    const { todos, filter } = this.props

    return (
      <TodoList 
        todos={todos}
        filter={filter}
        handleUpdateClick={this.handleUpdateClick}
        /* others */
      />
    )
  }
}

const mapStateToProps = state => {
  return {
    todos : state.todos,
    filter: state.filter
  }
}

const mapDispatchToProps = dispatch => {
  return {
    update : (todo) => dispatch({
      type : 'UPDATE_TODO',
      payload: todo
    }),
    fetchTodoList: (filters) => dispatch({
      type : 'FETCH_TODOS',
      payload: filters
    })
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoListContainer)

components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, handleUpdateClick }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} handleUpdateClick={handleUpdateClick} />
    ))}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.array.isRequired
  ).isRequired,
  handleUpdateClick: PropTypes.func.isRequired
}

export default TodoList

components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'

class Todo extends React.Component { 
  constructor(...args) {
    super(..args);
    this.state = {
      editable: false,
      todo: this.props.todo
    }
  }
  handleClick (e) {
    this.setState({
      editable: !this.state.editable
    })
  }
  update () {
    this.props.handleUpdateClick({
      ...this.state.todo
      text: this.refs.content.innerText
    })
  }
  render () {
    return (
      <li
        onClick={this.handleClick}
        style={{
          contentEditable: editable ? 'true' : 'false'
        }}
      >
        <p ref="content">{text}</p>
        <button onClick={this.update}>Save</button>
      </li>
    )
  }

Todo.propTypes = {
  handleUpdateClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

容器組件與展示型組件

在使用Redux作為React應用的狀態管理容器時,通常貫徹將組件劃分為容器組件(Container Components)和展示型組件(Presentational Components)的做法,

Presentational Components Container Components
目標 UI展示 (HTML結構和樣式) 業務邏輯(獲取數據,更新狀態)
感知Redux
數據來源 props 訂閱Redux store
變更數據 調用props傳遞的回調函數 Dispatch Redux actions
可重用 獨立性強 業務耦合度高

應用中大部分代碼是在編寫展示型組件,然后使用一些容器組件將這些展示型組件和Redux store連接起來。

connect()源碼分析

react-redux源碼邏輯
connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories;
selectorFactory = defaultSelectorFactory;
function connect (
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
  pure = true,
  areStatesEqual = strictEqual, // 嚴格比較是否相等
  areOwnPropsEqual = shallowEqual, // 淺比較
  areStatePropsEqual = shallowEqual,
  areMergedPropsEqual = shallowEqual,
  renderCountProp, // 傳遞給內部組件的props鍵,表示render方法調用次數
  // props/context 獲取store的鍵
  storeKey = 'store',
  ...extraOptions
  } = {}
) {
  const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
  const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
  
  // 調用connectHOC方法
  connectHOC(selectorFactory, {
    // 如果mapStateToProps為false,則不監聽store state
    shouldHandleStateChanges: Boolean(mapStateToProps),
    // 傳遞給selectorFactory
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,
    renderCountProp, // 傳遞給內部組件的props鍵,表示render方法調用次數
    // props/context 獲取store的鍵
    storeKey = 'store',
    ...extraOptions // 其他配置項
  });
}

strictEquall

function strictEqual(a, b) { return a === b }

shallowEquall

源碼

const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if (keysA.length !== keysB.length) return false

  for (let i = 0; i < keysA.length; i++) {
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}
shallowEqual({x:{}},{x:{}}) // false
shallowEqual({x:1},{x:1}) // true

connectAdvanced高階函數

源碼

function connectAdvanced (
  selectorFactory,
  {
    renderCountProp = undefined, // 傳遞給內部組件的props鍵,表示render方法調用次數
    // props/context 獲取store的鍵
    storeKey = 'store',
    ...connectOptions
  } = {}
) {
  // 獲取發布訂閱器的鍵
  const subscriptionKey = storeKey + 'Subscription';
  const contextTypes = {
    [storeKey]: storeShape,
    [subscriptionKey]: subscriptionShape,
  };
  const childContextTypes = {
    [subscriptionKey]: subscriptionShape,
  };
  
  return function wrapWithConnect (WrappedComponent) {
    const selectorFactoryOptions = {
      // 如果mapStateToProps為false,則不監聽store state
      shouldHandleStateChanges: Boolean(mapStateToProps),
      // 傳遞給selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      ...connectOptions,
      ...others
      renderCountProp, // render調用次數
      shouldHandleStateChanges, // 是否監聽store state變更
      storeKey,
      WrappedComponent
    }
    
    // 返回拓展過props屬性的Connect組件
    return hoistStatics(Connect, WrappedComponent)
  }
}

selectorFactory

selectorFactory函數返回一個selector函數,根據store state, 展示型組件props,和dispatch計算得到新props,最后注入容器組件,selectorFactory函數結構形如:

(dispatch, options) => (state, props) => ({
  thing: state.things[props.thingId],
  saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})

注:redux中的state通常指redux store的state而不是組件的state,另此處的props為傳入組件wrapperComponent的props。

源碼

function defaultSelectorFactory (dispatch, {
  initMapStateToProps,
  initMapDispatchToProps,
  initMergeProps,
  ...options
}) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)
  
  // pure為true表示selectorFactory返回的selector將緩存結果;
  // 否則其總是返回一個新對象
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 最終執行selector工廠函數返回一個selector
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  );
}

pureFinalPropsSelectorFactory

function pureFinalPropsSelectorFactory (
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
  
  // 返回合并后的props或state
  // handleSubsequentCalls變更后合并;handleFirstCall初次調用
  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
    : handleFirstCall(nextState, nextOwnProps)
  }  
}

handleFirstCall

function handleFirstCall(firstState, firstOwnProps) {
  state = firstState
  ownProps = firstOwnProps
  stateProps = mapStateToProps(state, ownProps) // store state映射到組件的props
  dispatchProps = mapDispatchToProps(dispatch, ownProps)
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps) // 合并后的props
  hasRunAtLeastOnce = true
  return mergedProps
}

defaultMergeProps

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  // 默認合并props函數
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

handleSubsequentCalls

function handleSubsequentCalls(nextState, nextOwnProps) {
  // shallowEqual淺比較
  const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
  // 深比較
  const stateChanged = !areStatesEqual(nextState, state)
  state = nextState
  ownProps = nextOwnProps

  // 處理props或state變更后的合并
  // store state及組件props變更
  if (propsChanged && stateChanged) return handleNewPropsAndNewState()
  if (propsChanged) return handleNewProps()
  if (stateChanged) return handleNewState()
  
  return mergedProps
}

計算返回新props

只要展示型組件自身props發生變更,則需要重新返回新合并props,然后更新容器組件,無論store state是否變更:

// 只有展示型組件props變更
function handleNewProps() {
  // mapStateToProps計算是否依賴于展示型組件props
  if (mapStateToProps.dependsOnOwnProps)
    stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps計算是否依賴于展示型組件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
  
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  
  return mergedProps
}
// 展示型組件props和store state均變更
function handleNewPropsAndNewState() {
  stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps計算是否依賴于展示型組件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
  
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  
  return mergedProps
}

計算返回stateProps

通常容器組件props變更由store state變更推動,所以只有store state變更的情況較多,而且此處也正是使用Immutable時需要注意的地方:不要在mapStateToProps方法內使用toJS()方法。

mapStateToProps兩次返回的props對象未有變更時,不需要重新計算,直接返回之前合并得到的props對象即可,之后在selector追蹤對象中比較兩次selector函數返回值是否有變更時,將返回false,容器組件不會觸發變更。

因為對比多次mapStateToProps返回的結果時是使用淺比較,所以不推薦使用Immutable.toJS()方法,其每次均返回一個新對象,對比將返回false,而如果使用Immutable且其內容未變更,則會返回true,可以減少不必要的重新渲染。

// 只有store state變更
function handleNewState() {
  const nextStateProps = mapStateToProps(state, ownProps)
  // 淺比較
  const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
  stateProps = nextStateProps

  // 計算得到的新props變更了,才需要重新計算返回新的合并props
  if (statePropsChanged) {
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  }

  // 若新stateProps未發生變更,則直接返回上一次計算得出的合并props;
  // 之后selector追蹤對象比較兩次返回值是否有變更時將返回false;
  // 否則返回使用mergeProps()方法新合并得到的props對象,變更比較將返回true
  return mergedProps
}

hoist-non-react-statics

類似Object.assign,將子組件的非React的靜態屬性或方法復制到父組件,React相關屬性或方法不會被覆蓋而是合并。

hoistStatics(Connect, WrappedComponent)

Connect Component

真正的Connect高階組件,連接redux store state和傳入組件,即將store state映射到組件props,react-redux使用Provider組件通過context方式注入store,然后Connect組件通過context接收store,并添加對store的訂閱:

class Connect extends Component {
  constructor(props, context) {
    super(props, context)

    this.state = {}
    this.renderCount = 0 // render調用次數初始為0
    // 獲取store,props或context方式
    this.store = props[storeKey] || context[storeKey]
    // 是否使用props方式傳遞store
    this.propsMode = Boolean(props[storeKey])

    // 初始化selector
    this.initSelector()
    // 初始化store訂閱
    this.initSubscription()
  }
  
  componentDidMount() {
    // 不需要監聽state變更
    if (!shouldHandleStateChanges) return
    // 發布訂閱器執行訂閱
    this.subscription.trySubscribe()
    // 執行selector
    this.selector.run(this.props)
    // 若還需要更新,則強制更新
    if (this.selector.shouldComponentUpdate) this.forceUpdate()
  }
  
  // 渲染組件元素
  render() {
    const selector = this.selector
    selector.shouldComponentUpdate = false; // 重置是否需要更新為默認的false

    // 將redux store state轉化映射得到的props合并入傳入的組件
    return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

addExtraProps()

給props添加額外的props屬性:

// 添加額外的props
addExtraProps(props) {
  const withExtras = { ...props }
  if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 調用次數
  if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription

  return withExtras
}

初始化selector追蹤對象initSelector

Selector,選擇器,根據redux store state和組件的自身props,計算出將注入該組件的新props,并緩存新props,之后再次執行選擇器時通過對比得出的props,決定是否需要更新組件,若props變更則更新組件,否則不更新。

使用initSelector方法初始化selector追蹤對象及相關狀態和數據:

// 初始化selector
initSelector() {
  // 使用selector工廠函數創建一個selector
  const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
  // 連接組件的selector和redux store state
  this.selector = makeSelectorStateful(sourceSelector, this.store)
  // 執行組件的selector函數
  this.selector.run(this.props)
}

makeSelectorStateful()

創建selector追蹤對象以追蹤(tracking)selector函數返回結果:

function makeSelectorStateful(sourceSelector, store) {
  // 返回selector追蹤對象,追蹤傳入的selector(sourceSelector)返回的結果
  const selector = {
    // 執行組件的selector函數
    run: function runComponentSelector(props) {
      // 根據store state和組件props執行傳入的selector函數,計算得到nextProps
      const nextProps = sourceSelector(store.getState(), props)
      // 比較nextProps和緩存的props;
      // false,則更新所緩存的props并標記selector需要更新
      if (nextProps !== selector.props || selector.error) {
        selector.shouldComponentUpdate = true // 標記需要更新
        selector.props = nextProps // 緩存props
        selector.error = null
      }  
    }
  }

  // 返回selector追蹤對象
  return selector
}

初始化訂閱initSubscription

初始化監聽/訂閱redux store state:

// 初始化訂閱
initSubscription() {
  if (!shouldHandleStateChanges) return; // 不需要監聽store state

  // 判斷訂閱內容傳遞方式:props或context,兩者不能混雜
  const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
  // 訂閱對象實例化,并傳入事件回調函數
  this.subscription = new Subscription(this.store, 
                                       parentSub,
                                       this.onStateChange.bind(this))
  // 緩存訂閱器發布方法執行的作用域
  this.notifyNestedSubs = this.subscription.notifyNestedSubs
    .bind(this.subscription)
}

訂閱類實現

組件訂閱store使用的訂閱發布器實現:

export default class Subscription {
  constructor(store, parentSub, onStateChange) {
    // redux store
    this.store = store
    // 訂閱內容
    this.parentSub = parentSub
    // 訂閱內容變更后的回調函數
    this.onStateChange = onStateChange
    this.unsubscribe = null
    // 訂閱記錄數組
    this.listeners = nullListeners
  }
  
  // 訂閱
  trySubscribe() {
    if (!this.unsubscribe) {
      // 若傳遞了發布訂閱器則使用該訂閱器訂閱方法進行訂閱
      // 否則使用store的訂閱方法
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.onStateChange)
        : this.store.subscribe(this.onStateChange)
 
      // 創建訂閱集合對象
      // { notify: function, subscribe: function }
      // 內部包裝了一個發布訂閱器;
      // 分別對應發布(執行所有回調),訂閱(在訂閱集合中添加回調)
      this.listeners = createListenerCollection()
    }
  }
  
  // 發布
  notifyNestedSubs() {
    this.listeners.notify()
  }
}

訂閱回調函數

訂閱后執行的回調函數:

onStateChange() {
  // 選擇器執行
  this.selector.run(this.props)

  if (!this.selector.shouldComponentUpdate) {
    // 不需要更新則直接發布
    this.notifyNestedSubs()
  } else {
    // 需要更新則設置組件componentDidUpdate生命周期方法
    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
    // 同時調用setState觸發組件更新
    this.setState(dummyState) // dummyState = {}
  }
}

// 在組件componentDidUpdate生命周期方法內發布變更
notifyNestedSubsOnComponentDidUpdate() {
  // 清除組件componentDidUpdate生命周期方法
  this.componentDidUpdate = undefined
  // 發布
  this.notifyNestedSubs()
}

其他生命周期方法

getChildContext () {
  // 若存在props傳遞了store,則需要對其他從context接收store并訂閱的后代組件隱藏其對于store的訂閱;
  // 否則將父級的訂閱器映射傳入,給予Connect組件控制發布變化的順序流
  const subscription = this.propsMode ? null : this.subscription
  return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 接收到新props
componentWillReceiveProps(nextProps) {
  this.selector.run(nextProps)
}

// 是否需要更新組件
shouldComponentUpdate() {
  return this.selector.shouldComponentUpdate
}

componentWillUnmount() {
  // 重置selector
}

參考閱讀

  1. React with redux
  2. Smart and Dumb Components
  3. React Redux Container Pattern
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容