Vuex的状态管理——store

1.前言

状态管理在开发中已经算是老生常谈了,本篇文章我们转向前端方向的Vue框架,看看Vuex是怎么通过store处理数据状态的管理的。由于Vue框架本身就是响应式处理数据的,所以store更多的为了跨路由去管理数据的状态。在使用store之前,我们先来说明store的两个特性:

  • Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 你不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交(commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

store是由五大成员组成的,开发者可根据业务复杂程度,合理的使用它们。我们先来了解一下store的五大成员:

2.五大成员

  • StateVuexstore中的state是响应式的,当state中的数据发生变化时,所有依赖于该数据的组件都会自动更新。
  • GettersVuexstore中的getters可以理解为store的计算属性,它们会基于store中的state派生出一些新的状态,供组件使用。
  • MutationsVuexstore中的mutations是同步的事件操作,它们用于更改store中的state。在Vuex中,mutations是唯一可以修改store中的state的方式。
  • ActionsVuexstore中的actions是异步的事件操作,它们用于处理异步逻辑,如API请求等。Actions可以通过提交mutations来修改store中的state
  • ModulesVuexstore可以通过模块化的方式组织代码,将一个大的store拆分成多个小的模块,每个模块都有自己的stategettersmutationsactions

其中StateMutations是构成store必不可少的成员。那么store是怎么使用的呢?我们来看下一章节。

3.Store的使用

3.1.安装:

npm install vuex

3.2.最简单的用法:

我们创建一个新的js文件,引入vuex并通过Vue.use()使用它,然创建一个Store对象并export

import Vuex from 'vuex'
import Vue from "vue";

Vue.use(Vuex)

const store =  new Vuex.Store({
    state: {    //在state对象建立需要数据
        count: 0
    },
    mutations: {
        add: function (state) {
            state.count++;
        }
    },
});

export default store

其中count是我们在state里声明的需要被监听的变量。add是个方法,其实现是count++。接下来我们在main.js里注入store实例:

import store from "./store"

new Vue({
  router,
  store, 
  render: h => h(App),
}).$mount('#app')

好了我们的store已经可以用了。我们假设有两个路由:home pagenew page,我们看看store是怎么监听数据变化并跨路由共享的。
home page代码如下:

<template>
  <div>
    <div>I am home page</div>
    <div>{{ getCount() }}</div>
    <div @click="addCount" class="button">add count</div>
    <div @click="gotoNewPage" class="button">Go to newpage</div>
  </div>
</template>
  
  <script>
export default {
  methods: {
    gotoNewPage() {
      this.$router.push({ path: "/newpage" });
    },
    getCount() {
        return this.$store.state.count;
    },
    addCount() {
        this.$store.commit('add');
    },
  },
};
</script>
  
  <style lang="scss" scoped>
.button {
  width: 100px;
  height: 50px;
  background: #527dab;
  margin-top: 15px;
}
</style>
  

其中getCount()方法从storestate里读取count的值。addCount()方法用来调用mutationsadd()方法,实现count++。们点击“add count”按钮看下效果:

IMG_2940.GIF

我们可以看到count的状态变化会被监听到。那么我们跳转至new page,代码如下:

<template>
  <div>
    <div>new page</div>
    <div>{{ getCount() }}</div>
    <div @click="back" class="button">back</div>
  </div>
</template>
  
  <script>
export default {
  methods: {
    back() {
      this.$router.push({
        path: "/home",
      });
    },
    getCount() {
        return this.$store.state.count;
    },
  },
};
</script>
  
  <style lang="scss" scoped>
.button {
  width: 100px;
  height: 50px;
  background: #527dab;
  margin-top: 15px;
}
</style>

跨路由读取到了count的值。

IMG_2942.GIF

以上就是Store最简单的用法,接下来我们进阶一下。

3.3.getters和actions

store增加gettersactions

const store = new Vuex.Store({
    state: {    //在state对象建立需要数据
        count: 0
    },
    getters: {
        getCount: function (state) {
            return "count是: " + state.count
        }
    },
    mutations: {
        add: function (state) {
            state.count++;
        }
    },
    actions: {
        addAction: function (context) {
            setTimeout(() => {
                context.commit('add')
            }, 500)

        }
    }
});

如果上一节所讲,getters类似于计算属性,有时候我们需要从store中的 state中派生出一些状态,那么就需要getters了。getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。actions的核心在于可以包含任意异步操作。它提交的是mutation,不能直接修改state的值。addAction模拟了一个异步操作,500ms之后执行add方法。我们接下来看一下使用,改造一下home pagegetCount()addCount()方法:

    getCount() {
      return this.$store.getters.getCount;
    },
    addCount() {
      this.$store.dispatch("addAction");
    },

actions需要使用store.dispatch()进行分发,我们看一下执行结果:

IMG_2944.GIF

点击add count按钮后500ms后,count加1,通过getters得到新的显示文本。

3.4.Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的statemutationactiongetter、甚至是嵌套子模块。接着举个例子:


const userStore = {
    state: {    //在state对象建立需要数据
        name: "tom"
    },
    getters: {
        getName: function (state) {
            return "name是: " + state.name
        }
    },
    mutations: {
        setName: function (state, name) {
            state.name = name;
        }
    },
    actions: {
        setNameAction: function (context,name) {
            console.log("setNameAction")
            setTimeout(() => {
                console.log("setName")
                context.commit('setName', name)
            }, 500)

        }
    }
};

export default userStore

刚才的实例代码中,我们增加一个userStore,为其配置了stategettersmutationsactions。然后我们为之前的store增加modules,代码如下:

import Vuex from 'vuex'
import Vue from "vue";
import userModule from "@/store/modules/user"

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {    //在state对象建立需要数据
        count: 0
    },
    getters: {
        getCount: function (state) {
            return "count是: " + state.count
        }
    },
    mutations: {
        add: function (state) {
            state.count++;
        }
    },
    actions: {
        addAction: function (context) {
            setTimeout(() => {
                context.commit('add')
            }, 500)

        }
    },
    modules:{
        user: userModule
    }
});

export default store

导入了userModule,我们看看这个usermodule是怎么用的。

<template>
  <div>
    <div>I am home page</div>

    <div>{{ getName() }}</div>
    <div @click="setName" class="button">set Name</div>
  </div>
</template>
  
  <script>
export default {
  methods: {
    getName(){
        return this.$store.getters.getName; 
    },
    setName() {
        this.$store.dispatch("setNameAction","bigcatduan");
    },
  },
};
</script>
  
  <style lang="scss" scoped>
.button {
  width: 100px;
  height: 50px;
  background: #527dab;
  margin-top: 15px;
}
</style>

如果是从getters里取数据用法同之前的实例一样。如果是从state里取数据则需要指定module

    getName(){
        return this.$store.state.user.name; 
    },

需要注意的是,如果module里的getter的方法名与父节点store的冲突了,则运行的时候会取父节点store的值,但会报duplicate getter key的错误。如果module里的getter的方法名与同节点的其它modules冲突了,则运行的时候会根据在节点注册的顺序来取值,同时报duplicate getter key的错误。比如:

    modules:{
        user: userModule,
        product: productModule
    }

由于userModule先被注册到modules里,所以取值时会取userModule的值。
如果父子之间或同节点的actionmutation的方法名相同的话,则各个节点同方法名的方法都会被执行。
好了到此为止我们发现一个问题,如果项目庞大,免不了会在不同的module里定义相同的变量或方法名称,让开发者自己去维护庞大的方法/变量列表显然不现实,于是vuex引入了命名空间namespace

3.5.命名空间

默认情况下,模块内部的actionmutation仍然是注册在全局命名空间的——这样使得多个模块能够对同一个actionmutation作出响应。Getter同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的getter从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true的方式使其成为带命名空间的模块。当模块被注册后,它的所有getteractionmutation都会自动根据模块注册的路径调整命名。
比如我们为刚才的userModule设置namespaced: true


const userStore = {
    namespaced: true,
    state: {    //在state对象建立需要数据
        name: "tom"
    },
    getters: {
        getName: function (state) {
            return "name是: " + state.name
        }
    },
    mutations: {
        setName: function (state, name) {
            state.name = name;
        }
    },
    actions: {
        setNameAction: function (context,name) {
            console.log("setNameAction")
            setTimeout(() => {
                console.log("setName")
                context.commit('setName', name)
            }, 500)

        }
    }
};

export default userStore

setget的相应方式变化如下:

    getName() {
      return this.$store.getters["user/getName"]; 
    },
    setName() {
      this.$store.dispatch("user/setNameAction", "bigcatduan"); 
    },

getterdispatchcommit时需要显示的指明局部的命名空间。
接下来我们思考一下,是否可以在不同的module下读取到其它module的内容呢?答案是可以的。比如我们的store结构如下:
根节点:

import Vuex from 'vuex'
import Vue from "vue";
import userModule from "@/store/modules/user"
import productModule from "@/store/modules/product"

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {    //在state对象建立需要数据
        count: 20
    },
    getters: {
        getCount: function (state) {
            return "count是: " + state.count
        }
    },
    mutations: {
        add: function (state) {
            state.count++;
        }
    },
    actions: {
        addAction: function (context) {
            setTimeout(() => {
                context.commit('add')
            }, 500)

        }
    },
    modules:{
        user: userModule,
        product: productModule
    }
});

export default store

里面包含了userproduct这两个module。
代码如下:

//user

const userStore = {
    namespaced: true,
    state: {    //在state对象建立需要数据
        name: "tom"
    },
    getters: {
        getName: function (state, getters, rootState, rootGetters) {
            return "name是: " + state.name
        }
    },
    mutations: {
        setName: function (state, name) {
            state.name = name;
        }
    },
    actions: {
        setNameAction: function (context,name) {
            setTimeout(() => {
                context.commit('setName', name)
            }, 500)

        }
    }
};

export default userStore
//product

const productStore = {
    namespaced: true,
    state: {    //在state对象建立需要数据
        price: 15
    },
    getters: {
        getPrice: function (state) {
            return "price是: " + state.price
        }
    },
    mutations: {
        setPrice: function (state, price) {
            state.price = price;
        }
    },
    actions: {
        setPriceAction: function (context,price) {
            setTimeout(() => {
                context.commit('setPrice', price)
            }, 500)

        }
    }
};

export default productStore

我们改造一下usergetters

//user
    getters: {
        getName: function (state, getters, rootState, rootGetters) {
            console.log("rootState count: ",rootState.count)
            console.log("product state price: ",rootState.product.price)
            console.log("root getter getCount: ",rootGetters.getCount)
            console.log("product getter getPrice: ",rootGetters["product/getPrice"])
            return "name是: " + state.name
        }
    },

除了statevuex还为我们提供了gettersrootStaterootGetters这几个参数。于是我们可以读取根节点state相应的变量,其他节点state相应的变量,根getters里的方法,还可以找到其他getters里的方法。上面代码的打印结果如下:

截屏2023-04-03 11.11.34.png

我们再来改造一下actions

    actions: {
        setNameActionRoot: {
            root:true,
            handler (namespacedContext, name){
                console.log("setNameAction")
                setTimeout(() => {
                    console.log("setName")
                    namespacedContext.commit('setName', name)
                }, 500)
            }

        },
    }

root:true意味着我们为这个带命名空间的模块注册全局action,所以我们依然可以不指定命名空间执行dispatch方法:

this.$store.dispatch("setNameActionRoot", "bigcatduan");

继续改造一下actions

    actions: {
        setNameAction: function (context,name) {
            setTimeout(() => {
                context.commit('setName', name)
            }, 500)

        },
        setSomeActions ( { dispatch, commit, getters, rootGetters },someAction){
            console.log("root getter getCount: ",rootGetters.getCount)
            console.log("product getter getPrice: ",rootGetters["product/getPrice"])
            dispatch('setNameAction',someAction.name)
            commit('setName',someAction.name)
            commit('product/setPrice', someAction.price, {root:true})
        }
    }

可以拿到全局rootGetters。若需要在全局命名空间内分发action 或提交 mutation,将{ root: true } 作为第三参数传给dispatchcommit即可。
好了以上就是store的用法,更多用法大家可以继续探索。接下来我们来讲一下store的实现原理。

4.实现原理

我们先来看看构造方法:

//Store
  constructor (options = {}) {
    if (__DEV__) {
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }

    const {
      plugins = [],
      strict = false,
      devtools
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._makeLocalGettersCache = Object.create(null)

    // EffectScope instance. when registering new getters, we wrap them inside
    // EffectScope so that getters (computed) would not be destroyed on
    // component unmount.
    this._scope = null

    this._devtools = devtools

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // initialize the store state, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreState(this, state)

    // apply plugins
    plugins.forEach(plugin => plugin(this))
  }

创建了各个成员变量,其中最重要的是_modules的创建。之后依次执行installModule()resetStoreState()plugins.forEach(plugin => plugin(this))。我们先来看看_modules的创建。

4.1. ModuleCollection

_modules对所有的module进行了初始化并构造了其依赖关系。其构造方法实现如下:

//ModuleCollection
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

调用了register方法:

  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

在初始化的时候,由于传入的是根modulepath.length === 0,所以会创建一个Module对象,并为root赋值,把它设置为根moduleModule的构造方法如下:

//Module
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

回到ModuleCollection,如果存在子modules,就会遍历modules,递归执行register()方法。而再次执行register()方法时,path.length === 0false,会找到它的parent,并执行addChild()方法:

//Module
  addChild (key, module) {
    this._children[key] = module
  }

由此实现了modules依赖关系的创建,生成了一个modules树。接下来我们看installModule()方法的实现:

4.2.installModule()

它的作用是对根module进行初始化,根据上一章节生成的modules树,递归注册每一个module。代码如下:

export function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      parentState[moduleName] = module.state
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

如果设置了nameSpace,则向_modulesNamespaceMap注册。之后如果不是根module,将模块的state添加到state链中,就可以按照state.moduleName进行访问。
接下来创建makeLocalContext上下文,为该module设置局部的dispatchcommit方法以及gettersstate,为的是在局部的模块内调用模块定义的actionmutation,这个过程具体就不展开了。
接下来分别遍历_mutations_actions_wrappedGetters进行注册。注册过程大同小异,我们取_mutations的注册来看看:

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

把对应的值函数封装后存储在数组里面,然后作为store._mutations的属性。store._mutations收集了我们传入的所有mutation函数。
installModule()的分析就完成了,我们继续看resetStoreState()的实现。

4.4.resetStoreState()

这个方法的作用是初始化state,并且注册getters作为一个computed属性。

export function resetStoreState (store, state, hot) {
  const oldState = store._state
  const oldScope = store._scope

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computedObj = {}
  const computedCache = {}

  // create a new effect scope and create computed object inside it to avoid
  // getters (computed) getting destroyed on component unmount.
  const scope = effectScope(true)

  scope.run(() => {
    forEachValue(wrappedGetters, (fn, key) => {
      // use computed to leverage its lazy-caching mechanism
      // direct inline function use will lead to closure preserving oldState.
      // using partial to return function with only arguments preserved in closure environment.
      computedObj[key] = partial(fn, store)
      computedCache[key] = computed(() => computedObj[key]())
      Object.defineProperty(store.getters, key, {
        get: () => computedCache[key].value,
        enumerable: true // for local getters
      })
    })
  })

  store._state = reactive({
    data: state
  })
//...
}

遍历store._wrappedGetters,并新建computed对象进行存储,通过Object.defineProperty方法为getters对象建立属性并实现响应式,使得我们通过this.$store.getters.xxxgetter能够访问到该getters。最后利用reactivestate实现响应式。

4.5.install

vue里,通过Vue.use()挂载全局变量,所以业务组件中通过this就能访问到store

  install (app, injectKey) {
    app.provide(injectKey || storeKey, this)
    app.config.globalProperties.$store = this

    const useDevtools = this._devtools !== undefined
      ? this._devtools
      : __DEV__ || __VUE_PROD_DEVTOOLS__

    if (useDevtools) {
      addDevtools(app, this)
    }
  }

4.6.commit和dispatch

最后我们再来看看commit和dispatch都做了什么

//commit
  commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
//...
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))
//...
  }

主要做了两件事:1.遍历_mutations,执行handler。2.遍历_subscribers,通知订阅者。我们在看dispatch

  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
//...

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }

dispatch使用Promise封装了异步操作,遍历_actions执行handler操作,并遍历_actionSubscribers通知订阅者。

5.总结

Vuex的状态管理方式store的使用和原理就介绍到这里。最后截取一张官网的图为大家进行一下总结。

截屏2023-04-03 17.54.19.png

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

推荐阅读更多精彩内容