Redux 家族(一):实战和源码实现

每一种新语言,新框架或者工具的出现都是为了解决项目中存在的问题。

动机

随着 Javascript 单页应用的开发越来越复杂,管理不断变化的 state 变得非常困难。这些状态有很多:UI 状态,服务器数据,缓存数据等。而组件,页面之间状态共享也是一个问题。

在 React 中,数据在组件中是单向流动的,数据从父组件传递给子组件是通过 props,由于这个特征,两个非父子关系的组件(或叫兄弟组件)之间的通信就比较麻烦。

Redux 的出现正式是为了解决 state 数据的管理问题。

设计思想

Redux 将整个应用的状态都存储在一个地方,就是 Store(容器),里面保存着应用的 state(状态树),组件可以通过 dispach(派发)action(行为)给 store(状态容器),而不是直接通知其他组件。其他组件可以通过订阅 store 中的 state(状态)来更新视图。


三大原则

写 Redux 需要遵循三个原则,有制约,有规则,状态才会可预测。因此 Redux 需要遵循以下三个原则:

单一数据源

整个应用的 state 被储存在一棵 Object tree 中,并且这个 Object tree 只存在于唯一一个 store 中;

State 是只读

State 是只读的,惟一改变 state 的方法就是dispatch(触发) action(动作);

使用纯函数来执行修改

通过 disptch action 来改变 state,action 是一个用于描述已发生事件的普通对象,使用纯函数(没有副作用的函数:函数的输出完全取决函数的参数;副作用:获取网络请求数据,修改 dom 标题等都属于副作用)来执行修改,为了描述 action 如何改变 state tree ,需要编写 reducer (处理器)。

单一数据源的设计让组件之间的通信更加方便,同时也便于状态的统一管理。


基本使用

通过一个 React 计数器组件来使用一下,新建一个项目,删除多余的文件,启动:

npx create-react-app redux-family
cd redux-family
npm install redux -S
npm start

编写一个 Counter 组件,应用 redux :
通过使用createStore来创建一个srore仓库,参数就是reducer初始 state,通过 store.getState()方法获取状态,store.dispatch来派发动作,store. subscribe订阅更新,store.unsubscribe取消订阅,这些基本就是 redux 的全部了:

// src/components/Counter1.js
import React, { Component } from 'react';
import { createStore} from 'redux';

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

let initState = { number: 0 };

const store = createStore(reducer, initState);

export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
      super(props);
      this.state = { number: 0 };
    }
    componentDidMount() {
      this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
      this.unsubscribe();
    }
    render() {
      return (
        <div>
          <p>{this.state.number}</p>
          <button onClick={() => store.dispatch({ type: 'ADD' })}>+</button>
          <button onClick={() => store.dispatch({ type: 'MINUS' })}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
                    store.dispatch({ type: 'ADD' });
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';

ReactDOM.render(
  <Counter1 />,
  document.getElementById('root')
);

结合 React 框架 react-redux

react-redux 用来整合 react 状态和数据订阅,使其使用起来更简便。提供 Provider使所有组件都可以获取状态,connect联接状态和组件,获取state状态,dispatch派发动作:

npm install react-redux -S
npm start
// src/components/Counter2.js
import React, { Component } from 'react';
import { connect } from 'react-redux'

class Counter extends Component {
    render() {
      return (
        <div>
          <p>{this.props.number}</p>
          <button onClick={() => this.props.add()}>+</button>
          <button onClick={() => this.props.minus()}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
                  this.props.add({ type: 'ADD' });
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}
const mapStateToProps = (state) => {
  return {
    number: state.number
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    add: () => dispatch({type: 'ADD'}),
    minus: () => dispatch({type: 'MINUS'})
  };
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore} from 'redux';
import { Provider } from 'react-redux'
import Counter2 from './components/Counter2'

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

let initState = { number: 0 };

const store = createStore(reducer, initState);

ReactDOM.render(
  <Provider store={store}>
    <Counter2 />
  </Provider>,
  document.getElementById('root')
);

不仅仅使用在 React 上

Redux 是一个状态数据流管理方案,不是为了 react 单独实现的,可以使用在任何框架以及原生 Javascript 中,这更体现了 Redux 的广阔的视角和先进的思想。

下面在原生 Javascript 应用中使用如下:
需要修改下 index.html,增加显示数据的 dom,以及操作按钮的 dom:

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="counter">
      <p id="number">0</p>
      <button id="add">+</button>
      <button id="minus">-</button>
    </div>
  </body>
</html>

index.js文件中通过订阅渲染函数来做数据的更新:

// src/index.js
import { createStore} from 'redux';

let counterValue = document.getElementById('number');
let incrementBtn = document.getElementById('add');
let decrementBtn = document.getElementById('minus');

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
let initState = { number: 0 };

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
  }
}

let store = createStore(reducer);

function render() {
  counterValue.innerHTML = store.getState().number;
}

store.subscribe(render);

render();

incrementBtn.addEventListener('click', function () {
  store.dispatch({ type: INCREMENT });
});
decrementBtn.addEventListener('click', function () {
  store.dispatch({ type: DECREMENT });
});

以上就是Redux的基本应用,学会使用一个框架很快,但是不能仅限于使用,还要知道其内部的实现原理,下面就一起来写一个具有简单功能的模仿的 redux

脱离框架的简单实现(源码)

createStore 创建仓库

从使用中可以看出,redux需要返回一个createStore方法,这个方法返回一个对象,对象上有getState(),dispatch,subscribe方法,getState()返回最新的statedispatch需要派发actionsubscribe需要订阅更新函数并返回一个取消订阅的函数redux内部需要先派发一次INIT动作填充初始状态。如此分析下来,这样一个redux壳就有了,先来写壳,再往里面填充功能:
redux/index.js

import createStore from './createStore.js'

export {
  createStore
}

redux/utils/ActionType.js

/**
 * These are private action types reserved(保留的) by Redux.
 * For any unknown actions, you must return the current state.
 * (所有未知的action都必须返回当前的state)
 * If the current state is undefined, you must return the initial state.
 * (如果当前state是undefined,必须返回和一个初始的state)
 * Do not reference these action types directly in your code.
 * (不要在代码中直接引入这些action type)
 */

const randomString = () => {
  return Math.random().toString(36).substring(7).split('').join('.')
}

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`
}

export default ActionTypes

redux/createStore.js

import ActionType from './utils/ActionType'

function createStore(reducer, preloadedState) {
  let currentState = preloadedState || {}
  let currentReducer = reducer
  let currentListeners = []

  const getState = () => {
    return currentState
  }
  const dispatch = (action) => {
    
  }

  const subscribe = (listener) => {
    return function unsubscribe() {
    }
  }

  // 内部需要先派发一次动作
  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates(填充)
  // the initial state tree.

  dispatch({type: ActionType.INIT})

  return {
    getState,
    dispatch,
    subscribe,
  }
}

export default createStore

这样的壳子就写好了,现在填充功能,订阅函数需要收集所有的订阅事件存储到数组中,并返回一个取消订阅的函数;
dispatch派发actionreducer获取最新的状态,最后通知所有的订阅组件(这里是更新函数);

const subscribe = (listener) => {
    currentListeners.push(listener)
    // 返回取消订阅的函数
    return function unsubscribe() {
      const index = currentListeners.indexOf(listener)
      // 取消订阅就是将这个订阅事件从数组中删除
      currentListeners.splice(index, 1)
    }
  }
const dispatch = (action) => {
    // reducer:传入旧的状态和action,返回新的state
    currentState = currentReducer(currentState, action)

    const listeners = currentListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 执行每一个订阅函数
      listener()
    }
    return action
  }

这样基本的redux功能就实现完了,将原生 Javascript 应用改成自己实现的redux,功能依然正常。

bindActionCreator

使用action creator直接派发动作,不直接调用 dispatch,这是一种更便捷的方法。bindActionCreatoraction creator 转换为带有相同key,但是每个函数都包含dispatch调用。

将之前直接在 react 中使用 redux 的例子重新改写一下,使用 bindActionCreator 重写一下实现:

import React, { Component } from 'react';
import { createStore, bindActionCreators} from 'redux';

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

+ function Add() {
+  return { type: INCREMENT }
+}
+function Minus() {
+  return { type: DECREMENT }
+}

let initState = { number: 0 };
const store = createStore(reducer, initState);
+ const actions = { Add, Minus }
+ const boundActions = bindActionCreators(actions, store.dispatch)


export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
      super(props);
      this.state = { number: 0 };
    }
    componentDidMount() {
      this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
      this.unsubscribe();
    }
    render() {
      return (
        <div>
          <p>{this.state.number}</p>
+           <button onClick={boundActions.Add}>+</button>
+           <button onClick={boundActions.Minus}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
+                   boundActions.Add();
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}

可以看到所有 action 都由 action creator 派发,不再显式的调用store.dispatch,bindActionCreator 内部就是返回了 dispatch 函数的调用,下面我们来写一下这个 bindActionCreator 函数实现:

思路:就是给每一个action creator 都返回 dispatch 方法调用,如果传入的是函数,也就是单个 action creator,直接调用返回 type,将参数传给调用 dispatch 的函数,如果是一个对象,就给这个对象上的每一个 action creator 都加上 dispatch 方法调用,所以代码的核心就是bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  // 这里派发 action,执行 actionCreator 函数,返回一个 type 作为 dispatch 的参数调用
  return function(...args) {
    dispatch(actionCreator.apply(this, args))
  }
}

完整的实现代码如下:

/**
 * function Add() {
      return { type: INCREMENT }
   }
   function Minus() {
      return { type: DECREMENT }
   }
 * const actions = { Add, Minus }
 * const boundActions = bindActionCreators(actions, store.dispatch)
 */

function bindActionCreator(actionCreator, dispatch) {
  // 这里派发 action,执行 actionCreator 函数,返回一个 type 作为 dispatch 的参数调用的函数
  return function(...args) {
    dispatch(actionCreator.apply(this, args))
  }
}

function bindActionCreators(actionCreators, dispatch) {
  // 如果是一个函数,表示就是一个 actionCreator,直接返回包装了调用dispatch的函数
  if (actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 如果是一个对象,给每个 actionCreator 都包装返回调用dispatch的函数
  if (typeof actionCreators === 'object' && actionCreators !== null) {
    const bindActionCreators = {}
    for(const key in actionCreators) {
      const actionCreator = actionCreators[key]
      bindActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
    return bindActionCreators
  }
}

export default bindActionCreators

combineReducers

通常组件不会只有一个 reducer,每个组件都可以有自己的 reducer 和 state,便于模块化管理。但是 redux 规定,一个应用只能有一个 store,一个 reducer,一个 state,根据程序模块化,分而治之的管理模式,这个时候就需要合并多个 reducer,多个状态为一个 reducer,一个state,combineReducers 就是做合并 reducer操作的,下面先来看看用法,然后再实现一个。
这里只贴重要的代码,其他细致的可以到 github 仓库看实战。
src/components/Counter5.js

import React, { Component } from 'react';
import { bindActionCreators} from '../redux';
import * as actionType from '../actionTypes'
// import { bindActionCreators } from '../redux'
import store from '../store'


const actions = {
  Add5: actionType.Add5,
  Minus5: actionType.Minus5
}
const boundActions = bindActionCreators(actions, store.dispatch)

export default class Counter5 extends Component {
  constructor(props) {
    super(props)
    this.state = { number : 5}
  }
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().Counter5.number }));
  }
  componentWillUnmount() {
    this.unsubscribe();
  }
  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={boundActions.Add5}>+</button>
        <button onClick={boundActions.Minus5}>-</button>
        <button onClick={
          () => {
              setTimeout(() => {
                boundActions.Add5();
              }, 1000);
          }
        }>1秒后加5</button>
      </div>
    )
  }
}

新建 reducers 和 action types
src/reducers/Counter5.jssrc/actionTypes

// src/reducers/Counter5.js
import * as actionType from '../actionTypes'

export let Counter5State = { number: 5 };

const reducer = (state = Counter5State, action) => {
  switch (action.type) {
    case actionType.INCREMENT5:
      return { number: state.number + 5 };
    case actionType.DECREMENT5:
      return { number: state.number - 5 };
    default:
      return state;
    }
}
export default reducer
// src/actionTypes.js
export const INCREMENT5 = 'ADD5';
export const DECREMENT5 = 'MINUS5';

export const INCREMENT6 = 'ADD6';
export const DECREMENT6 = 'MINUS6';

export function Add5() {
  return { type: INCREMENT5 }
}
export function Minus5() {
  return { type: DECREMENT5 }
}

export function Add6() {
  return { type: INCREMENT6 }
}
export function Minus6() {
  return { type: DECREMENT6 }
}

src/store.js

import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers);
export default store

src/reducers/index.js

import { combineReducers } from 'redux'
import Counter5 from '../reducers/Counter5'
import Counter6 from '../reducers/Counter6'

const reducers = {
  Counter5,
  Counter6
}
const combinedReducers = combineReducers(reducers)
export default combinedReducers

其他的不贴了,效果就是组件的状态互不干扰:


combineReducers就是将多个组件的 reducer 合并为一个 reducer,内部实现是过滤 reducers 对象上的函数属性,然后返回一个combination函数,这个combination函数内部会遍历过滤之后的对象,解析出来每一个 reducer,拿到对应的状态,最后再根据 reducer 的 key,返回一个新的状态对象。根据 reducer 解析出来的 state 是最重要的一段,const reducer = finalReducers[key]const nextStateForKey = reducer(previousStateForKey, action)。这里面有一个优化操作:定义一个 hasChanged的标志,新状态和老状态不一致就返回新状态,否则返回老的状态。
具体代码实现如下,跟源码的实现是一致的,代码有备注解释:
src/redux/combineReducers

export default function combineReducers (reducers) {
  // 1. 过滤 reducers 对象上的函数属性
  const reducerKeys = Object.keys(reducers) // ["counter1", "counter2"]
  const finalReducers = {}
  for(let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i] // "counter1"
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key] // finalReducers: {counter1: counter1, counter2: counter2}
    }
  }

  // 2. 返回一个函数 遍历finalReducers,生成state
  const finalReducerKeys = Object.keys(finalReducers) // ['counter1', 'counter2']
  return function combination (state = {}, action) {
    let hasChanged = false // hasChanged是作性能优化,没有改变就返回原来的state
    const nextState = {}
    for(let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i] // "counter1"
      const reducer = finalReducers[key] // reducer counter1 函数
      const previousStateForKey = state[key] // 取出reducer对应的state

      // 这步最重要:给reducer传入老的state和action,返回新的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 将新的state的key 拼到总的state上,组件通过 state.counter1.number获取
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 前后状态不一样就是改变了
    }
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length // reducers的key不一样也是变了
    return hasChanged ? nextState : state
  }

以上就是 combineReducers 的实现。

react-redux 实现

从上面可以 Counter5,Counter6 组件可以看出来,这两个组件其实有很多的相似代码,比如订阅更新,获取状态等,这些都可以抽离出来,减少冗余代码,将 store 绑定到 context 上,至上而下的传递,这就是 react-redux 做的事情。

先来看一下 react-redux 的应用,实现一个 Counter7 组件,对应的 actions 和 reducer 和上面一样,就不赘述了。Counter7 使用 react-redux 的 connect 返回一个高阶组件

src/components/Counter7.js

import React, { Component } from 'react'
import * as actionType from '../actionTypes'
import { connect } from 'react-redux'


const actions = {
  add: actionType.Add7,
  minus: actionType.Minus7
}

const mapStateToProps = (state) => {
  console.log(state)
  return {
    number: state.Counter7.number
  }
}

const mapDispatchToProps = actions

class Counter7 extends Component {
  render() {
    const { number, add, minus } = this.props
    return (
      <div>
        <p>{number}</p>
        <button onClick={add}>+</button>
        <button onClick={minus}>-</button>
        <button onClick={
          () => {
              setTimeout(() => {
                add()
              }, 1000)
          }
        }>1秒后加5</button>
      </div>
    )
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps 
)(Counter7)

src/index.js 使用 Provider 共享 store:

import React from 'react';
import ReactDOM from 'react-dom';
import Counter7 from './components/Counter7'
import store from './store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <Counter7 />
  </Provider>
,
  document.getElementById('root')
)

可以看到 Counter7 使用 connect 连接,mapStateToProps 将 store 上的state 拼到 props 上mapDispatchToProps 将派发函数拼接到 props
下面来看看 Provider 和 connect 实现,根据 react-redux 官方的实现,我们也采用函数和 hooks 的方式实现。

Provider 是利用 react 的 context 创建上下文容器,value 可以设置成共享的数据,provider 提供数据,consumer 消费这些数据。
const { Provider, Consumer } = React.createContext(defaultValue);
新建一个 react-redux 文件目录,里面来写代码实现:
src/react-redux/index.js

import Provider from './Provider'
import connect from './connect'

export {
  Provider,
  connect
}

src/react-redux/ReactReduxContext.js

import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext

src/react-redux/Provider.js

import React from 'react'
import ReactReduxContext from './ReactReduxContext'

export default function Provider (props) {
  return (
    <ReactReduxContext.Provider value={{ store: props.store }}>
      {props.children}
    </ReactReduxContext.Provider>
  )
}

最复杂的是 connnect 的实现:
connnect 是一个函数,返回一个被包装过后的组件,里面做的事情就是将 state,dispatch 派发函数作为 props 拼到被包装的组件上,然后在渲染的时候订阅更新函数,更新状态。
mapStateToProps 映射 state 到 props 上const stateProps = useMemo(() => mapStateToProps(preState), [preState])

const mapStateToProps = (state) => {
  return {
    number: state.Counter7.number
  }
}

mapDispatchToProps 就是映射 dispatch 函数到 props,这里分三种情况,如果参数是对象就要做 bindActionCreators,分别包装返回具有 dispatch能力的函数对象,像这样:{add: ƒ, minus: ƒ};如果参数是函数,就直接拼mapDispatchToProps(props, dispatch),否则就直接返回一个包含 dispatch 的对象;

const dispatchProps = useMemo(() => {
  let dispatchProps
  if (typeof mapDispatchToProps === 'object') {
    dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
   } else if (typeof mapDispatchToProps === 'function') {
    dispatchProps = mapDispatchToProps(props, dispatch)
    } else {
    dispatchProps = { dispatch }
    }
    return dispatchProps
}, [dispatch, props])

然后订阅更新函数,更新状态:

const [, forceUpdate] = useReducer(x => x + 1, 0)
useLayoutEffect(() => subscribe(forceUpdate), [subscribe])

useLayoutEffect 会在 dom 更新完之后,浏览器绘制之前执行。
最后返回包装之后的组件:
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
完整的 connect 的实现代码如下:

import React, { useContext, useMemo, useReducer, useLayoutEffect } from 'react'
import ReactReduxContext from './ReactReduxContext'
import { bindActionCreators } from '../redux'

export default function connect (mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    return function (props) {
      const { store } = useContext(ReactReduxContext)
      const { getState, dispatch, subscribe} = store
      const preState = getState()
      // 映射状态到 props
      const stateProps = useMemo(() => mapStateToProps(preState), [preState])

      // 映射 dispatch 到 props
      const dispatchProps = useMemo(() => {
        let dispatchProps
        if (typeof mapDispatchToProps === 'object') {
          dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
        } else if (typeof mapDispatchToProps === 'function') {
          dispatchProps = mapDispatchToProps(props, dispatch)
        } else {
          dispatchProps = { dispatch }
        }
        return dispatchProps
      }, [dispatch, props])

      // 订阅更新函数,更新状态
      const [, forceUpdate] = useReducer(x => x + 1, 0)
      useLayoutEffect(() => subscribe(forceUpdate), [subscribe])

      // 返回被包装的组件
      return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
    }
  }
}

到目前为止,redux 和 react-redux 的基本核心都实现了,参照的是 github 上的源码。还剩下一个 中间件的功能,这个准备在下一章单独去写,分析一下时常用的几个 redux 中间件用法和实现,以及连接中间件的实现原理,到 react 生态的整合,比如 redux-saga,dva,umi等。目前项目用到的生态就是这些,所以想单独开一篇专门写这个(希望不要开天窗),加深理解,温故知新。

参考:
https://github.com/reduxjs/redux
https://github.com/reduxjs/react-redux

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

推荐阅读更多精彩内容