Redux源碼

在進(jìn)入正題前,我們首先來看一下在項(xiàng)目中是如何使用 Redux 的掰担,根據(jù)使用步驟來講解源碼怒炸。

// 首先把多個 reducer 通過 combineReducers 組合在一起
const appReducer = combineReducers({
    user: UserReducer,
    goods: GoodsReducer,
    order: OrdersReducer,
    chat: ChatReducer
});
// 然后將 appReducer 傳入 createStore阅羹,并且通過 applyMiddleware 使用了中間件 thunkMiddleware
// 然后在需要的地方發(fā)起 dispatch(action) 引起 state 改變
export default function configureStore() {
    const store = createStore(
        rootReducer,
        compose(
            applyMiddleware(thunkMiddleware),
            window.devToolsExtension ? window.devToolsExtension() : f => f
        )
    );
    return store;
}

介紹完了使用步驟捏鱼,接下來進(jìn)入正題。

源碼解析

首先讓我們來看下 combineReducers 函數(shù)

// 傳入一個 object
export default function combineReducers(reducers) {
 // 獲取該 Object 的 key 值
    const reducerKeys = Object.keys(reducers)
    // 過濾后的 reducers
    const finalReducers = {}
    // 獲取每一個 key 對應(yīng)的 value
    // 在開發(fā)環(huán)境下判斷值是否為 undefined
    // 然后將值類型是函數(shù)的值放入 finalReducers
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]

        if (process.env.NODE_ENV !== 'production') {
            if (typeof reducers[key] === 'undefined') {
                warning(`No reducer provided for key "${key}"`)
            }
        }

        if (typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key]
        }
    }
    // 拿到過濾后的 reducers 的 key 值
    const finalReducerKeys = Object.keys(finalReducers)

    // 在開發(fā)環(huán)境下判斷,保存不期望 key 的緩存用以下面做警告
    let unexpectedKeyCache
    if (process.env.NODE_ENV !== 'production') {
        unexpectedKeyCache = {}
    }

    let shapeAssertionError
    try {
    // 該函數(shù)解析在下面
        assertReducerShape(finalReducers)
    } catch (e) {
        shapeAssertionError = e
    }
// combineReducers 函數(shù)返回一個函數(shù)猿诸,也就是合并后的 reducer 函數(shù)
// 該函數(shù)返回總的 state
// 并且你也可以發(fā)現(xiàn)這里使用了閉包狡忙,函數(shù)里面使用到了外面的一些屬性
    return function combination(state = {}, action) {
        if (shapeAssertionError) {
            throw shapeAssertionError
        }
        // 該函數(shù)解析在下面
        if (process.env.NODE_ENV !== 'production') {
            const warningMessage = getUnexpectedStateShapeWarningMessage(
                state,
                finalReducers,
                action,
                unexpectedKeyCache
            )
            if (warningMessage) {
                warning(warningMessage)
            }
        }
        // state 是否改變
        let hasChanged = false
        // 改變后的 state
        const nextState = {}
        for (let i = 0; i < finalReducerKeys.length; i++) {
        // 拿到相應(yīng)的 key
            const key = finalReducerKeys[i]
            // 獲得 key 對應(yīng)的 reducer 函數(shù)
            const reducer = finalReducers[key]
            // state 樹下的 key 是與 finalReducers 下的 key 相同的
            // 所以你在 combineReducers 中傳入的參數(shù)的 key 即代表了 各個 reducer 也代表了各個 state
            const previousStateForKey = state[key]
            // 然后執(zhí)行 reducer 函數(shù)獲得該 key 值對應(yīng)的 state
            const nextStateForKey = reducer(previousStateForKey, action)
            // 判斷 state 的值,undefined 的話就報錯
            if (typeof nextStateForKey === 'undefined') {
                const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
            // 然后將 value 塞進(jìn)去
            nextState[key] = nextStateForKey
            // 如果 state 改變
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        // state 只要改變過北专,就返回新的 state
        return hasChanged ? nextState : state
    }
}

combineReducers 函數(shù)總的來說很簡單旬陡,總結(jié)來說就是接收一個對象描孟,將參數(shù)過濾后返回一個函數(shù)。該函數(shù)里有一個過濾參數(shù)后的對象 finalReducers场航,遍歷該對象溉痢,然后執(zhí)行對象中的每一個 reducer 函數(shù),最后將新的 state 返回。

接下來讓我們來看看 combinrReducers 中用到的兩個函數(shù)

// 這是執(zhí)行的第一個用于拋錯的函數(shù)
function assertReducerShape(reducers) {
// 將 combineReducers 中的參數(shù)遍歷
    Object.keys(reducers).forEach(key => {
        const reducer = reducers[key]
        // 給他傳入一個 action
        const initialState = reducer(undefined, { type: ActionTypes.INIT })
        // 如果得到的 state 為 undefined 就拋錯
        if (typeof initialState === 'undefined') {
            throw new Error(
                `Reducer "${key}" returned undefined during initialization. ` +
                    `If the state passed to the reducer is undefined, you must ` +
                    `explicitly return the initial state. The initial state may ` +
                    `not be undefined. If you don't want to set a value for this reducer, ` +
                    `you can use null instead of undefined.`
            )
        }
        // 再過濾一次汽畴,考慮到萬一你在 reducer 中給 ActionTypes.INIT 返回了值
        // 傳入一個隨機(jī)的 action 判斷值是否為 undefined
        const type =
            '@@redux/PROBE_UNKNOWN_ACTION_' +
            Math.random()
                .toString(36)
                .substring(7)
                .split('')
                .join('.')
        if (typeof reducer(undefined, { type }) === 'undefined') {
            throw new Error(
                `Reducer "${key}" returned undefined when probed with a random type. ` +
                    `Don't try to handle ${
                        ActionTypes.INIT
                    } or other actions in "redux/*" ` +
                    `namespace. They are considered private. Instead, you must return the ` +
                    `current state for any unknown actions, unless it is undefined, ` +
                    `in which case you must return the initial state, regardless of the ` +
                    `action type. The initial state may not be undefined, but can be null.`
            )
        }
    })
}

function getUnexpectedStateShapeWarningMessage(
    inputState,
    reducers,
    action,
    unexpectedKeyCache
) {
    // 這里的 reducers 已經(jīng)是 finalReducers
    const reducerKeys = Object.keys(reducers)
    const argumentName =
        action && action.type === ActionTypes.INIT
            ? 'preloadedState argument passed to createStore'
            : 'previous state received by the reducer'

    // 如果 finalReducers 為空
    if (reducerKeys.length === 0) {
        return (
            'Store does not have a valid reducer. Make sure the argument passed ' +
            'to combineReducers is an object whose values are reducers.'
        )
    }
        // 如果你傳入的 state 不是對象
    if (!isPlainObject(inputState)) {
        return (
            `The ${argumentName} has unexpected type of "` +
            {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
            `". Expected argument to be an object with the following ` +
            `keys: "${reducerKeys.join('", "')}"`
        )
    }
        // 將參入的 state 于 finalReducers 下的 key 做比較,過濾出多余的 key
    const unexpectedKeys = Object.keys(inputState).filter(
        key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
    )

    unexpectedKeys.forEach(key => {
        unexpectedKeyCache[key] = true
    })

    if (action && action.type === ActionTypes.REPLACE) return

// 如果 unexpectedKeys 有值的話
    if (unexpectedKeys.length > 0) {
        return (
            `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
            `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
            `Expected to find one of the known reducer keys instead: ` +
            `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
        )
    }
}

接下來讓我們先來看看 compose 函數(shù)

// 這個函數(shù)設(shè)計(jì)的很巧妙,通過傳入函數(shù)引用的方式讓我們完成多個函數(shù)的嵌套使用隙券,術(shù)語叫做高階函數(shù)
// 通過使用 reduce 函數(shù)做到從右至左調(diào)用函數(shù)
// 對于上面項(xiàng)目中的例子
compose(
        applyMiddleware(thunkMiddleware),
        window.devToolsExtension ? window.devToolsExtension() : f => f
)
// 經(jīng)過 compose 函數(shù)變成了 applyMiddleware(thunkMiddleware)(window.devToolsExtension()())
// 所以在找不到 window.devToolsExtension 時你應(yīng)該返回一個函數(shù)
export default function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

然后我們來解析 createStore 函數(shù)的部分代碼

export default function createStore(reducer, preloadedState, enhancer) {
  // 第4個參數(shù)也是函數(shù)時,報錯牲迫,多個增強(qiáng)函數(shù)可以合并成一個
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }
 // 一般 preloadedState 用的少铐刘,判斷類型檩禾,如果第二個參數(shù)是函數(shù)且沒有第三個參數(shù)画株,就調(diào)換位置
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

   // 判斷 enhancer 是否是函數(shù)
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
     // 類型沒錯的話芹关,先執(zhí)行 enhancer侥衬,然后再執(zhí)行 createStore 函數(shù)
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 判斷 reducer 是否是函數(shù)
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 當(dāng)前 reducer
  let currentReducer = reducer
   // 當(dāng)前狀態(tài)
  let currentState = preloadedState
  // 當(dāng)前監(jiān)聽函數(shù)數(shù)組
  let currentListeners = []
    // 這是一個很重要的設(shè)計(jì)博个,為的就是每次在遍歷監(jiān)聽器的時候保證 currentListeners 數(shù)組不變
    // 可以考慮下只存在 currentListeners 的情況往堡,如果我在某個 subscribe 中再次執(zhí)行 subscribe
    // 或者 unsubscribe,這樣會導(dǎo)致當(dāng)前的 currentListeners 數(shù)組大小發(fā)生改變穆咐,從而可能導(dǎo)致
    // 索引出錯
  let nextListeners = currentListeners

    // reducer 是否正在執(zhí)行
  let isDispatching = false

  /**
   * This makes a shallow copy of currentListeners so we can use
   * nextListeners as a temporary list while dispatching.
   *
   * This prevents any bugs around consumers calling
   * subscribe/unsubscribe in the middle of a dispatch.
   */
  // 如果 currentListeners 和 nextListeners 相同庸娱,就賦值回去
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }


  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

   // 然后在 createStore 末尾會發(fā)起一個 action dispatch({ type: ActionTypes.INIT });
 // 用以初始化 state
  dispatch({ type: ActionTypes.INIT })

  //dispatch一個用于初始化的action,相當(dāng)于調(diào)用一次reducer
    //然后將reducer中的子reducer的初始值也獲取到
    //詳見下面reducer的實(shí)現(xiàn)。
  return {
    dispatch,
    subscribe,
    getState,
     //下面兩個是主要面向庫開發(fā)者的方法一铅,暫時先忽略
    replaceReducer,
    [$$observable]: observable
  }
}

可以看出,createStore方法創(chuàng)建了一個store,但是并沒有直接將這個store的狀態(tài)state返回,而是返回了一系列方法柑土,外部可以通過這些方法(getState)獲取state灸促,或者間接地(通過調(diào)用dispatch)改變state荒叼。
至于state呢萝玷,被存在了閉包中蜓斧。

我們再來詳細(xì)的看看每個模塊是如何實(shí)現(xiàn)的

getState

 // 獲取state
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

dispatch

 // 觸發(fā)了一個action豆拨,因此我們調(diào)用reducer,得到的新的state邮绿,并且執(zhí)行所有添加到store中的監(jiān)聽函數(shù)。
  function dispatch(action) {
     // 原生的 dispatch 會判斷 action 是否為對象
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 注意在 Reducers 中是不能執(zhí)行 dispatch 函數(shù)的
// 因?yàn)槟阋坏┰?reducer 函數(shù)中執(zhí)行 dispatch,會引發(fā)死循環(huán)
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
// 執(zhí)行 combineReducers 組合后的函數(shù)
    try {
      isDispatching = true
       //調(diào)用reducer凛辣,得到新state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
// 然后遍歷 currentListeners,執(zhí)行數(shù)組中保存的函數(shù)
    const listeners = (currentListeners = nextListeners)
     //調(diào)用監(jiān)聽數(shù)組中的所有監(jiān)聽函數(shù)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

subscribe

subscribe返回的是一個取消訂閱的方法。取消訂閱是非常必要的锁右,當(dāng)添加的監(jiān)聽器沒用了之后痪署,應(yīng)該從store中清理掉余寥。不然每次dispatch都會調(diào)用這個沒用的監(jiān)聽器呐馆。

  // 添加一個監(jiān)聽函數(shù)续膳,每當(dāng)dispatch被調(diào)用的時候都會執(zhí)行這個監(jiān)聽函數(shù)
  function subscribe(listener) {
    // 添加到監(jiān)聽函數(shù)數(shù)組摔桦,
    // 注意:我們添加到了下一次dispatch時才會生效的數(shù)組
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    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.'
      )
    }
    //設(shè)置一個標(biāo)志,標(biāo)志該監(jiān)聽器已經(jīng)訂閱了
    let isSubscribed = true
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    //subscribe返回的是一個取消訂閱的方法。
    //取消訂閱是非常必要的鸥拧,當(dāng)添加的監(jiān)聽器沒用了之后钠绍,應(yīng)該從store中清理掉。
    //不然每次dispatch都會調(diào)用這個沒用的監(jiān)聽器赵誓。

    return function unsubscribe() {
      if (!isSubscribed) {
        return // 如果已經(jīng)取消訂閱過了幻枉,直接返回
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      // 從下一輪的監(jiān)聽函數(shù)數(shù)組(用于下一次dispatch)中刪除這個監(jiān)聽器蔓罚。
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

在這之前我需要先來介紹一下函數(shù)柯里化贡这,柯里化是一種將使用多個參數(shù)的一個函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù)的技術(shù)。

function add(a,b) { return a + b }
add(1, 2) => 3
// 對于以上函數(shù)如果使用柯里化可以這樣改造
function add(a) {
        return b => {
                return a + b
        }
}
add(1)(2) => 3
// 你可以這樣理解函數(shù)柯里化正歼,通過閉包保存了外部的一個變量,然后返回一個接收參數(shù)的函數(shù),在該函數(shù)中使用了保存的變量湃密,然后再返回值。

在redux中使用中間件

還記得redux 的createStore()方法的第三個參數(shù)enhancer嗎?

function createStore(reducer, preloadedState, enhancer) {
    if(enhancer是有效的){  
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    //...
}

在這里铺厨,我們可以看到赃磨,enhancer(可以叫做強(qiáng)化器)是一個函數(shù),這個函數(shù)接受一個「普通createStore函數(shù)」作為參數(shù)缕减,返回一個「加強(qiáng)后的createStore函數(shù)」搅裙。

applyMiddleware嫂易,顧名思義颅和,「應(yīng)用中間件」。輸入為若干中間件,輸出為enhancer案训。面來看看它的源碼:

// 這個函數(shù)應(yīng)該是整個源碼中最難理解的一塊了
// 該函數(shù)返回一個柯里化的函數(shù)
// 所以調(diào)用這個函數(shù)應(yīng)該這樣寫 applyMiddleware(...middlewares)(createStore)(...args)
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
      //用參數(shù)傳進(jìn)來的createStore創(chuàng)建一個store
    const store = createStore(...args)
     //注意猜旬,我們在這里需要改造的只是store的dispatch方法
    let dispatch = () => {
      //一個臨時的dispatch
      //作用是在dispatch改造完成前調(diào)用dispatch只會打印錯誤信息
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
     // 每個中間件都應(yīng)該有這兩個函數(shù)
      //接下來我們準(zhǔn)備將每個中間件與我們的state關(guān)聯(lián)起來(通過傳入getState方法)熟嫩,得到改造函數(shù)柠逞。
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
     // 把 middlewares 中的每個中間件都傳入 middlewareAPI
      //middlewares是一個中間件函數(shù)數(shù)組,中間件函數(shù)的返回值是一個改造dispatch的函數(shù)
        //調(diào)用數(shù)組中的每個中間件函數(shù),得到所有的改造函數(shù)
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
     // 和之前一樣,從右至左調(diào)用每個中間件,然后傳入 store.dispatch

      //將這些改造函數(shù)compose(翻譯:構(gòu)成,整理成)成一個函數(shù)
        //用compose后的函數(shù)去改造store的dispatch
    dispatch = compose(...chain)(store.dispatch)

         // compose方法的作用是,例如這樣調(diào)用:
        // compose(func1,func2,func3)
        // 返回一個函數(shù): (...args) => func1( func2( func3(...args) ) )
        // 即傳入的dispatch被func3改造后得到一個新的dispatch糠悯,新的dispatch繼續(xù)被func2改造...



     // 返回store讯泣,用改造后的dispatch方法替換store中的dispatch
    return {
      ...store,
      dispatch
    }
  }
}
            


總結(jié)一下好渠,applyMiddleware的工作方式是:

調(diào)用(若干個)中間件函數(shù)拳锚,獲然舨簟(若干個)改造函數(shù)
把所有改造函數(shù)compose成一個改造函數(shù)
改造dispatch方法

中間件的工作方式是:

中間件是一個函數(shù),不妨叫做中間件函數(shù)
中間件函數(shù)的輸入是store的getState和dispatch简卧,輸出為改造函數(shù)(改造dispatch的函數(shù))
改造函數(shù)輸入是一個dispatch贞滨,輸出「改造后的dispatch」

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勺良,一起剝皮案震驚了整個濱河市尚困,隨后出現(xiàn)的幾起案子谬泌,更是在濱河造成了極大的恐慌掌实,老刑警劉巖贱鼻,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異父丰,居然都是意外死亡掘宪,警方通過查閱死者的電腦和手機(jī)魏滚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門蘑斧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竖瘾,“玉大人捕传,你說我怎么就攤上這事庸论∧羰荆” “怎么了鱼喉?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵扛禽,是天一觀的道長编曼。 經(jīng)常有香客問我搀矫,道長瓤球,這世上最難降的妖魔是什么卦羡? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任绿饵,我火速辦了婚禮拟赊,結(jié)果婚禮上吸祟,老公的妹妹穿的比我還像新娘葛碧。我一直安慰自己过吻,他們只是感情好纤虽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布洋措。 她就那樣靜靜地躺著,像睡著了一般专缠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上墩弯,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天渔工,我揣著相機(jī)與錄音引矩,去河邊找鬼。 笑死区端,一個胖子當(dāng)著我的面吹牛织盼,可吹牛的內(nèi)容都是我干的悔政。 我是一名探鬼主播谋国,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捌蚊,長吁一口氣:“原來是場噩夢啊……” “哼缅糟!你這毒婦竟也來了窗宦?” 一聲冷哼從身側(cè)響起赴涵,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寄纵,沒想到半個月后程拭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哺壶,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了绷杜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鞭盟。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡筝野,死狀恐怖歇竟,靈堂內(nèi)的尸體忽然破棺而出焕议,到底是詐尸還是另有隱情盅安,我是刑警寧澤别瞭,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布畜隶,位于F島的核電站壁肋,受9級特大地震影響号胚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜浸遗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一猫胁、第九天 我趴在偏房一處隱蔽的房頂上張望跛锌。 院中可真熱鬧弃秆,春花似錦、人聲如沸髓帽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郑藏。三九已至衡查,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間必盖,已是汗流浹背拌牲。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工俱饿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塌忽。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓拍埠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親土居。 傳聞我的和親對象是個殘疾皇子枣购,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

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