【Vue3.0】- 侦听器

监听器

侦听器可以帮助我们去观察某个数据的变化然后去执行一段逻辑

  • Vue.js 2.x 中,通过 watch 选项,或者$watch API去初始化一个侦听器,称作 watcher
  • Vue.js 3.0 使用Composition API中的watch API可以实现侦听器的效果

watch API 的用法

  • 1、watch API 可以侦听一个 getter 函数
    • 这个getter必须返回一个响应式对象
    • 当该响应式对象更新后,会执行对应的回调函数
import { reactive, watch } from 'vue' 
const state = reactive({ count: 0 }) 
watch(() => state.count, (count, prevCount) => { 
  // 当 state.count 更新,会触发此回调函数 
}) 
  • 2、watch API 可以直接侦听一个响应式对象,当响应式对象更新后,会执行对应的回调函数
import { ref, watch } from 'vue' 
const count = ref(0) 
watch(count, (count, prevCount) => { 
  // 当 count.value 更新,会触发此回调函数 
})
  • 3、watch API 可以直接侦听多个响应式对象,任意一个响应式对象更新后,就会执行对应的回调函数。
import { ref, watch } from 'vue' 
const count = ref(0) 
const count2 = ref(1) 
watch([count, count2], ([count, count2], [prevCount, prevCount2]) => { 
  // 当 count.value 或者 count2.value 更新,会触发此回调函数 
})

watch API 实现原理

  • effect副作用函数有什么区别?

watch API 的具体实现

function watch(source, cb, options) { 
  if ((process.env.NODE_ENV !== 'production') && !isFunction(cb)) { 
    warn(`\`watch(fn, options?)\` signature has been moved to a separate API. ` + 
      `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + 
      `supports \`watch(source, cb, options?) signature.`) 
  } 
  return doWatch(source, cb, options) 
} 
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { 
  // 标准化 source 
  // 构造 applyCb 回调函数 
  // 创建 scheduler 时序执行函数 
  // 创建 effect 副作用函数 
  // 返回侦听器销毁函数 
} 
  • watch 函数内部调用了 doWatch 函数
  • 调用前会在非生产环境下判断第二个参数 cb 是不是一个函数
  • 如果不是则会报警告以告诉用户应该使用 watchEffect(fn, options) API

1、标准化 source

  • watch 函数的第一个参数 source,可以有三种类型,所以需要先进行标准化source
// source 不合法的时候会报警告 
const warnInvalidSource = (s) => { 
  warn(`Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + 
    `a reactive object, or an array of these types.`) 
} 
// 当前组件实例 
const instance = currentInstance 
let getter 
if (isArray(source)) { 
  getter = () => source.map(s => { 
    if (isRef(s)) { 
      return s.value 
    } 
    else if (isReactive(s)) { 
      return traverse(s) 
    } 
    else if (isFunction(s)) { 
      return callWithErrorHandling(s, instance, 2 /* WATCH_GETTER */) 
    } 
    else { 
      (process.env.NODE_ENV !== 'production') && warnInvalidSource(s) 
    } 
  }) 
} 
else if (isRef(source)) { 
  getter = () => source.value 
} 
else if (isReactive(source)) { 
  getter = () => source 
  deep = true 
} 
else if (isFunction(source)) { 
  if (cb) { 
    // getter with cb 
    getter = () => callWithErrorHandling(source, instance, 2 /* WATCH_GETTER */) 
  } 
  else { 
    // watchEffect 的逻辑 
  } 
} 
else { 
  getter = NOOP 
  (process.env.NODE_ENV !== 'production') && warnInvalidSource(source) 
} 
if (cb && deep) { 
  const baseGetter = getter 
  getter = () => traverse(baseGetter()) 
}
  • source 标准化主要是根据 source 的类型,将其变成 标准成 getter 函数
  • 1)如果 sourceref 对象,则创建一个访问 source.valuegetter 函数
  • 2)如果 sourcereactive 对象,则创建一个访问 sourcegetter 函数,并设置 deeptrue
  • 3)如果 source 是一个函数,则会进一步判断第二个参数 cb是否存在
    • 对于 watch API 来说,cb是一定存在且是一个回调函数
    • 这种情况下,getter就是一个简单的对 source函数封装的函数
  • 最终标准化生成的 getter函数,返回一个响应式对象
    • 在后续创建 effect runner 副作用函数需要用到
    • 每次执行 runner 就会把 getter 函数返回的响应式对象作为 watcher 求值的结果
  • deeptrue的情况下
    • 生成的 getter函数会被 traverse 函数包装一层
    • traverse 函数,通过递归的方式访问 value 的每一个子属性
    • reactive API可以创建复杂对象,为了保证修改任意属性都能触发watcher,需要使用traverse包装
  • 如果嵌套层级很深,递归 traverse 就会有一定的性能耗时,如何优化使得 traverse 不执行?答案:监听一个 getter 函数
watch(() => state.count.a.b, (newVal, oldVal) => { 
  console.log(newVal) 
}) 
state.count.a.b = 2

2、构造 applyCb 回调函数

  • 这里是处理第二个参数 cb
  • cb 是一个回调函数,它有三个参数
    • 1)newValue:代表新值
    • 2)oldValue:代表旧值。
    • 3)onInvalidate:
  • 构造回调函数的处理逻辑如下:
let cleanup 
// 注册无效回调函数 
const onInvalidate = (fn) => { 
  cleanup = runner.options.onStop = () => { 
    callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */) 
  } 
} 
// 旧值初始值 
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE /*{}*/ 
// 回调函数 
const applyCb = cb 
  ? () => { 
    // 组件销毁,则直接返回 
    if (instance && instance.isUnmounted) { 
      return 
    } 
    // 求得新值 
    const newValue = runner() 
    if (deep || hasChanged(newValue, oldValue)) { 
      // 执行清理函数 
      if (cleanup) { 
        cleanup() 
      } 
      callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [ 
        newValue, 
        // 第一次更改时传递旧值为 undefined 
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, 
        onInvalidate 
      ]) 
      // 更新旧值 
      oldValue = newValue 
    } 
  } 
  : void 0 
  • applyCb是对cb的封装,当侦听的值发生变化时就会执行 applyCb 方法
  • watch API 和组件实例相关,当组件销毁后,回调函数 cb 不应该被执行而是直接返回
  • 执行 runner 求得新值,实际上是执行前面创建的 getter 函数求新值
  • 判断如果是 deep 的情况或者新旧值发生了变化,则执行回调函数 cb,传入参数 newValueoldValue
  • 执行完回调函数 cb 后,把旧值 oldValue 再更新为 newValue

创建 scheduler

  • scheduler的作用是根据某种调度的方式去执行某种函数
  • watch API 中,主要影响到的是回调函数的执行方式,实现逻辑如下:
const invoke = (fn) => fn() 
let scheduler 
if (flush === 'sync') { 
  // 同步 
  scheduler = invoke 
} 
else if (flush === 'pre') { 
  scheduler = job => { 
    if (!instance || instance.isMounted) { 
      // 进入异步队列,组件更新前执行 
      queueJob(job) 
    } 
    else { 
      // 如果组件还没挂载,则同步执行确保在组件挂载前 
      job() 
    } 
  } 
} 
else { 
  // 进入异步队列,组件更新后执行 
  scheduler = job => queuePostRenderEffect(job, instance && instance.suspense) 
} 
  • Watch API第三个参数options,可以主动设置deep: true
  • scheduler 的创建逻辑受到了第三个参数 options中的 flush 属性值的影响,不同的 flush 决定了 watcher 的执行时机
  • 1)当 flushsync 的时候,表示它是一个同步 watcher,即当数据变化时同步执行回调函数
  • 2)当 flushpre 的时候,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行
  • 3)如果没设置 flush,那么回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行

4、创建 effect

  • 计算新值时执行的runner函数,是watcher 内部创建的 effect 函数,逻辑如下:
const runner = effect(getter, { 
  // 延时执行 
  lazy: true, 
  // computed effect 可以优先于普通的 effect 先运行,比如组件渲染的 effect 
  computed: true, 
  onTrack, 
  onTrigger, 
  scheduler: applyCb ? () => scheduler(applyCb) : scheduler 
}) 
// 在组件实例中记录这个 effect 
recordInstanceBoundEffect(runner) 
// 初次执行 
if (applyCb) { 
  if (immediate) { 
    applyCb() 
  } 
  else { 
    // 求旧值 
    oldValue = runner() 
  } 
} 
else { 
  // 没有 cb 的情况 
  runner() 
} 
  • 核心是:通过effect API创建一个副作用函数 runner
  • 1)runner 是一个computed effect
    • computed effect 可以优先于普通的 effect(比如组件渲染的 effect)先运行
    • 可以实现当配置 flushpre 的时候,watcher 的执行可以优先于组件更新
  • 2)runner执行的方式
    • runnerlazy 的,不会在创建后立刻执行
    • 第一次手动执行 runner 会执行前面的 getter 函数,访问响应式数据并做依赖收集,此时activeEffect 就是 runner
    • 在后面更新响应式数据时,就可以触发 runner 执行 scheduler 函数
  • 3)runner 的返回结果,手动执行runner相当于执行了前面标准化的getter函数
    • getter 函数的返回值就是 watcher计算出的值
    • 第一次执行 runner 求得的值可以作为 oldValue
  • 4)配置了 immediate 的情况
    • 当我们配置了 immediate ,创建完watcher 会立刻执行 applyCb 函数
    • 此时 oldValue 还是初始值
    • applyCb 执行时也会执行 runner 进而执行前面的 getter 函数做依赖收集,求得新值

5、返回销毁函数

  • 返回侦听器销毁函数,也就是 watch API 执行后返回的函数。
  • 通过调用销毁函数来停止 watcher 对数据的侦听
return () => { 
  stop(runner) 
  if (instance) { 
    // 移除组件 effects 对这个 runner 的引用 
    remove(instance.effects, runner) 
  } 
} 
function stop(effect) { 
  if (effect.active) { 
    cleanup(effect) 
    if (effect.options.onStop) { 
      effect.options.onStop() 
    } 
    effect.active = false 
  } 
} 
  • 销毁函数内部会执行 stop 方法让runner 失活
  • 清理 runner 的相关依赖,可以停止对数据的侦听
  • 如果是在组件中注册的 watcher,也会移除组件 effects 对这个 runner 的引用

侦听器的实现原理

异步任务队列的设计

  • 当侦听器的回调函数的调度方式flush不是sync时,会把回调函数执行的任务推到一个异步队列中执行,为什么会需要异步队列?

异步任务队列的设计

思考:当连续修改响应式数据,会回调多次吗
答案:不会。
因为,在一个 Tick(宏任务执行的生命周期)内,即使多次修改侦听的值,它的回调函数也只执行一次

知识延伸
组件的更新过程是异步的,我们知道修改模板中引用的响应式对象的值时,会触发组件的重新渲染,但是在一个 Tick 内,即使你多次修改多个响应式对象的值,组件的重新渲染也只执行一次。这是因为如果每次更新数据都触发组件重新渲染,那么重新渲染的次数和代价都太高

异步任务队列的创建

  • 在创建一个 watcher 时,如果配置 flushpre 或不配置 flush * 那么 watcher 的回调函数就会异步执行
  • 此时分别是通过 queueJobqueuePostRenderEffect 把回调函数推入异步队列中的
// 异步任务队列 
const queue = [] 
// 队列任务执行完后执行的回调函数队列 
const postFlushCbs = [] 
function queueJob(job) { 
  if (!queue.includes(job)) { 
    queue.push(job) 
    queueFlush() 
  } 
} 
function queuePostFlushCb(cb) { 
  if (!isArray(cb)) { 
    postFlushCbs.push(cb) 
  } 
  else { 
    // 如果是数组,把它拍平成一维 
    postFlushCbs.push(...cb) 
  } 
  queueFlush() 
}
  • Vue.js 内部维护了一个 queue 数组和一个 postFlushCbs 数组
  • queue 数组用作异步任务队列
  • postFlushCbs 数组用作异步任务队列执行完毕后的回调函数队列
  • 在添加完毕后都执行了 queueFlush 函数
const p = Promise.resolve() 
// 异步任务队列是否正在执行 
let isFlushing = false 
// 异步任务队列是否等待执行 
let isFlushPending = false 
function nextTick(fn) { 
  return fn ? p.then(fn) : p 
} 
function queueFlush() { 
  if (!isFlushing && !isFlushPending) { 
    isFlushPending = true 
    nextTick(flushJobs) 
  } 
}
  • Vue.js 内部还维护了 isFlushingisFlushPending 变量,用来控制异步任务的刷新逻辑
  • 首次执行queueFlush,设置isFlushPendingfalse,并执行nextTick(flushJobs)去执行队列里的任务
  • nextTickVue.js 3.0 中的实现也是非常简单,通过 Promise.resolve().then 去异步执行 flushJobs

·JavaScript· 是单线程执行的,这样的异步设计使你在一个 Tick 内,可以多次执行 queueJob 或者 queuePostFlushCb 去添加任务,也可以保证在宏任务执行完毕后的微任务阶段执行一次 flushJobs

异步任务队列的执行

  • flushJobs 的实现
const getId = (job) => (job.id == null ? Infinity : job.id) 
function flushJobs(seen) { 
  isFlushPending = false 
  isFlushing = true 
  let job 
  if ((process.env.NODE_ENV !== 'production')) { 
    seen = seen || new Map() 
  } 
  // 组件的更新是先父后子 
  // 如果一个组件在父组件更新过程中卸载,它自身的更新应该被跳过 
  queue.sort((a, b) => getId(a) - getId(b)) 
  while ((job = queue.shift()) !== undefined) { 
    if (job === null) { 
      continue 
    } 
    if ((process.env.NODE_ENV !== 'production')) { 
      checkRecursiveUpdates(seen, job) 
    } 
    callWithErrorHandling(job, null, 14 /* SCHEDULER */) 
  } 
  flushPostFlushCbs(seen) 
  isFlushing = false 
  // 一些 postFlushCb 执行过程中会再次添加异步任务,递归 flushJobs 会把它们都执行完毕 
  if (queue.length || postFlushCbs.length) { 
    flushJobs(seen) 
  } 
} 
  • flushJobs 函数开始执行的时候,把 isFlushPending 重置为 false
  • isFlushing 设置为 true 来表示正在执行异步任务队列
  • 对于异步任务队列 queue,在遍历执行它们前会先对它们做一次从小到大的排序,这是因为两个主要原因:
  • 1)创建组件的过程是由父到子,所以创建组件副作用渲染函数也是先父后子
    • 父组件的副作用渲染函数的 effect id 是小于子组件的
    • 每次更新组件也是通过 queueJobeffect 推入异步任务队列 queue 中的
    • 所以为了保证先更新父组再更新子组件,要对 queue 做从小到大的排序
  • 2)如果一个组件在父组件更新过程中被卸载,它自身的更新应该被跳过
    • 所以也应该要保证先更新父组件再更新子组件,要对 queue 做从小到大的排序
  • 然后,遍历这个 queue,依次执行队列中的任务
    • 在遍历过程中,checkRecursiveUpdates 是用来在非生产环境下检测是否有循环更新的
  • 遍历完 queue 后,又会进一步执行 flushPostFlushCbs 方法,去遍历执行所有推入到 postFlushCbs回调函数
function flushPostFlushCbs(seen) { 
  if (postFlushCbs.length) { 
    // 拷贝副本 
    const cbs = [...new Set(postFlushCbs)] 
    postFlushCbs.length = 0 
    if ((process.env.NODE_ENV !== 'production')) { 
      seen = seen || new Map() 
    } 
    for (let i = 0; i < cbs.length; i++) { 
      if ((process.env.NODE_ENV !== 'production')) {                                                       
        checkRecursiveUpdates(seen, cbs[i]) 
      } 
      cbs[i]() 
    } 
  } 
} 
  • const cbs = [...new Set(postFlushCbs)] 拷贝一个 postFlushCbs 的副本,避免执行回调函数时修改postFlushCbs
  • 遍历完 postFlushCbs 后,会重置 isFlushingfalse
    • 因为一些 postFlushCb 执行过程中可能会再次添加异步任务,所以需要继续判断如果 queue 或者 postFlushCbs 队列中还存在任务,则递归执行 flushJobs 把它们都执行完毕

检测循环更新

  • 使用checkRecursiveUpdates检测是否有循环更新,用以解决在对某个响应式数据的watcher回调函数中修改这个响应式数据的循环更新问题
const RECURSION_LIMIT = 100 
function checkRecursiveUpdates(seen, fn) { 
  if (!seen.has(fn)) { 
    seen.set(fn, 1) 
  } 
  else { 
    const count = seen.get(fn) 
    if (count > RECURSION_LIMIT) { 
      throw new Error('Maximum recursive updates exceeded. ' + 
        "You may have code that is mutating state in your component's " + 
        'render function or updated hook or watcher source function.') 
    } 
    else { 
      seen.set(fn, count + 1) 
    } 
  } 
} 
  • flushJobs开始,创建了seen
  • seen用于在checkRecursiveUpdates中,记录某个job进行重复任务添加次数
  • 如果次数超过100,则抛出错误

优化:只用一个变量

  • isFlushPending 用于判断是否在等待 nextTick 执行 flushJobs
    isFlushing 判断是否正在执行任务队列
  • 从功能上来看,它们的作用是为了确保以下两点:
  • 1)在一个 Tick 内可以多次添加任务到队列中,但是任务队列会在 nextTick 后执行
  • 2)在执行任务队列的过程中,也可以添加新的任务到队列中,并且在当前 Tick 去执行剩余的任务队列
只使用isFlushing来控制
  • 在执行 queueFlush 的时候,判断 isFlushingfalse,则把它设置为 true
  • 然后 nextTick 会执行 flushJobs
  • flushJobs 函数执行完成的最后,也就是所有的任务(包括后添加的)都执行完毕,再设置 isFlushingfalse

watchEffect API

watchEffect API 的作用是注册一个副作用函数,副作用函数内部可以访问到响应式对象,当内部响应式对象变化后再立即执行这个函数

import { ref, watchEffect } from 'vue' 
const count = ref(0) 
watchEffect(() => console.log(count.value)) 
count.value++ 

watchEffect 和 watch API 区别

侦听的源不同
  • watch API 可以侦听一个或多个响应式对象,也可以侦听一个 getter 函数
  • watchEffect API 侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象
没有回调函数
  • watchEffect API 没有回调函数,副作用函数的内部响应式对象发生变化后,会再次执行这个副作用函数
立即执行
  • watchEffect API 在创建好 watcher 后,会立刻执行它的副作用函数
  • watch API 需要配置 immediatetrue,才会立即执行回调函数

watchEffect简易实现

function watchEffect(effect, options) { 
  return doWatch(effect, null, options); 
} 
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { 
  instance = currentInstance; 
  let getter; 
  if (isFunction(source)) { 
    getter = () => { 
      if (instance && instance.isUnmounted) { 
        return; 
      } 
       // 执行清理函数 
      if (cleanup) { 
        cleanup(); 
      } 
      // 执行 source 函数,传入 onInvalidate 作为参数 
      return callWithErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onInvalidate]); 
    }; 
  } 
  let cleanup; 
  const onInvalidate = (fn) => { 
    cleanup = runner.options.onStop = () => { 
      callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); 
    }; 
  }; 
  let scheduler; 
  // 创建 scheduler 
  if (flush === 'sync') { 
    scheduler = invoke; 
  } 
  else if (flush === 'pre') { 
    scheduler = job => { 
      if (!instance || instance.isMounted) { 
        queueJob(job); 
      } 
      else { 
        job(); 
      } 
    }; 
  } 
  else { 
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense); 
  } 
  // 创建 runner 
  const runner = effect(getter, { 
    lazy: true, 
    computed: true, 
    onTrack, 
    onTrigger, 
    scheduler 
  }); 
  recordInstanceBoundEffect(runner); 
  // 立即执行 runner 
  runner(); 
  // 返回销毁函数 
  return () => { 
    stop(runner); 
    if (instance) { 
      remove(instance.effects, runner); 
    } 
  }; 
} 
  • getter 函数就是对 source 函数的简单封装
    • 它会先判断组件实例是否已经销毁
    • 然后每次执行 source 函数前执行 cleanup 清理函数
  • watchEffect 内部创建的 runner 对应的 scheduler 对象就是 scheduler 函数本身
    • 再次执行时,就会执行这个 scheduler 函数
    • 并且传入 runner 函数作为参数
    • 按照一定的调度方式去执行基于 source 封装的 getter 函数
  • 创建完 runner 后就立刻执行了 runner
    • 其实就是内部同步执行了基于 source 封装的 getter 函数
  • 在执行 source 函数的时候,会传入一个 onInvalidate 函数作为参数

注册无效回调函数

  • 有些时候,watchEffect 会注册一个副作用函数,在函数内部可以做一些异步操作
  • 但是当这个 watcher 停止后,如果我们想去对这个异步操作做一些额外事情(比如取消这个异步操作),我们可以通过 onInvalidate 参数注册一个无效函数
import {ref, watchEffect } from 'vue' 
const id = ref(0) 
watchEffect(onInvalidate => { 
  // 执行异步操作 
  const token = performAsyncOperation(id.value) 
  onInvalidate(() => { 
    // 如果 id 发生变化或者 watcher 停止了,则执行逻辑取消前面的异步操作 
    token.cancel() 
  }) 
})
onInvalidate 在 doWatch 中的实现
const onInvalidate = (fn) => { 
  cleanup = runner.options.onStop = () => { 
    callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); 
  }; 
};
  • 当响应式数据发生变化,会执行 cleanup 方法,注册runneronStop方法
  • watcher 被停止,会执行 onStop 方法
  • 这两者都会执行注册的无效回调函数 fn
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容