JS中的异步编程

前言

编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续提出的新概念是否解决了问题。

将回调包装成promise

观察者模式与事件监听

内容来源于《ES6入门-阮一峰》《你不知道的JS》《MDN web文档》《N
ode.js》、‘网上的诸多博客’、‘自己以前的代码’。
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。

异步与多线程

先说结论:JS是单线程的但是宿主环境:浏览器或node 不是。

简单的看JS的代码是从上至下执行的,如果中间遇到了特别耗时的任务。那此任务下面的代码就会一直等待等待。也就是阻塞。想解决这个问题,也就是解决耗时任务的问题,一种可以开启多个线程,将任务移到子线程去不占用主线程,比如安卓app就是在主线程更新UI,在子线程种执行耗时操作譬如网络请求。(html5新增了web worker也可以开个子线程执行其他操作了)。一种就是现在js主流采用的异步编程:先执行所有的同步代码块,然后再执行异步代码块。

一个比较有趣的点是,刚开始听说异步的时候,总是会想起来JS是单线程的。就好像是只有一个工人一样,分给他的任务即使先做简单的,那复杂的不一样的得慢慢做吗?很显然我是错的,JS是单线程的,但是浏览器是多线程的。
具体请看这篇博客,讲的很好,这篇

浏览器线程

  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,执行回调。


    事件

    Snipaste_2018-10-15_17-59-32.jpg

在node种也有类似的Event loop
1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
2、主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
4、主线程不断重复上面的第三步。
至于具体实现以及与浏览器之间的差异就不写了,我也没搞懂。

回调

wiki定义:In computer programming, a callback, also known as a "call-after" function, is any executable code that is passed as an argument to other code that is expected to call back (execute) the argument at a given time.This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback

简单的说就是做好了叫我。根据上文的浏览器或node异步事件处理机制叙述,可以很轻松的看出,怎么用回调写异步代码。将耗时操作与需要得到结果后执行的操作用回调函数写就。例如(还有事件监听或订阅模式与本篇内容关系不大,不写。)

function foo() {
  console.log('执行完了')
}
function bar1() {
  console.log('同步函数1')
}
function bar2() {
  console.log('同步函数2')
}
bar1()
setTimeout(foo, 1000)
bar2()
/**
同步函数1
同步函数2
执行完了
*/

回调是异步的解决方案,但是回调的问题又是什么?

回调地狱

listen( "click", function handler(evt){
    setTimeout( function request(){
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} );

谁都写过类似的代码,一个套一个。所以回调的问题一是不清晰。

顺序

如果想组织两个或多个异步函数的顺序用回调函数就会变得复杂,比如上面回调地狱那种,按照顺序一一执行,又或要等两个异步函数都出结果才会回调,或一个出了结果立刻回调另一个作废。这些操作都需要在每一个回调函数里书写判断逻辑。

控制倒转

假设

bar1()
setTimeout(foo, 1000)
bar2()

我们的目的是先执行bar1、bar2稍后执行一次foo。这个代码用了最熟悉的setTimeout,所以可以保证按照设计执行,但是如果这个异步函数是第三方提供的,那么就失去了foo的执行控制权。称之为控制倒转。
这种情况可能会导致回调不调用、调用过早、调用多次、没有得到必要参数、吞掉了异常等等。
可以利用错误优先模式解决部分问题

function response(err,data) {
    // 有错?
    if (err) {
        console.error( err );
    }
    // 否则,认为成功
    else {
        console.log( data );
    }
}
ajax( "http://some.url.1", response );

总结

又不是不能用,应该可以很准确的形容异步回调。组织代码不清晰缺乏顺序性,对很多场景要加判断逻辑缺乏可靠性。现在轮到promise解决这些问题了。

Promise

回调缺乏可靠性的原因就是我们不知道它何时会完成也不知道是否成功。Promise就解决了这个问题。
Promise中保存着未来才会结束的事件,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)一旦状态改变,就不会再变。这时就称为 resolved(已定型)。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
==============
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

虽然依然是基于回调的,但是却是通过固定的形式将回调的形式固定下来,并且会传递异步得到的数据或错误给回调函数。解决了信任问题。

回调包装成promise

假设是包装某个特定的函数,可以直接构造并设定好resolve与reject。

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

一种是调用个方法可以随时将回调promise化,这个可以下载第三方库,比如bluebird

解决回调问题

  1. 回调过早调用
    如果对一个 Promise 调用 then(..) 的时候,即使这个 Promise是立即resolve的函数(即Promise内部没有ajax等异步操作,只有同步操作), 提供给then(..) 的回调也是会被异步调用的
    Promise.resolve()可以将现有对象转化为Promise对象。根据参数不同而结果不同
  • 参数Promise实例
    不做任何修改返回这个实例。
  • 参数是个thenable对象
    将这个对象转为Promise对象,并立刻执行thenable对象的then方法。thenable对象指的是具有then方法的对象
  • 参数不是具有then方法的对象,或根本就不是对象,比如基本类型值
    返回一个新的 Promise 对象,状态为resolved。
  • 不带有任何参数
    直接返回一个resolved状态的 Promise 对象。
  1. 回调调用次数过多
    Promise 的内部机制决定了调用单个Promise的then方法, 回调只会被执行一次,因为Promise的状态变化是单向不可逆的,当这个Promise第一次调用resolve方法, 使得它的状态从pending(正在进行)变成fullfilled(已成功)或者rejected(被拒绝)后, 它的状态就再也不能变化了
  2. 回调中的报错被吞掉
    Promise中的then方法中的error回调被调用的时机有两种情况:
    • Promise中主动调用了reject (有意识地使得Promise的状态被拒绝), 这时error
      回调能够接收到reject方法传来的参数(reject(error))
    • 在定义的Promise中, 运行时候报错(未预料到的错误), 也会使得Promise的
      状态被拒绝,从而使得error回调能够接收到捕捉到的错误

4.回调没有调用
设置一个超时用Promise.race组合。Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例,只要其中一个实例改变状态新的大实例就会改变状态。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

组织问题

  1. 链式
    将回调地狱改成链式,每一个then里面的异步操作都可以返回一个值,传递给下一个异步操作当作参数。
    • 每次你在一个Promise上调用then(..)的时候,它都创建并返回一个新的Promise,我们可以在它上面进行 链接。
    • 无论你从then(..)调用的完成回调中(第一个参数)返回什么值,它都做为被链接的Promise的完成。
Promise.then(
  // 第一个异步操作
).then(
  // 第二个异步操作
).then(
  // 第三个异步操作
)

  1. Promise.all方法
    all方法接收一个Promise数组,并且返回一个新的“大Promise”, 只有数组里的全部Promise的状态都转为Fulfilled(成功),这个“大Promise”的状态才会转为Fulfilled(成功), 这时候, then方法里的成功的回调接收的参数也是数组,分别和数组里的子Promise一一对应
  2. 竞态
    Promise.race方法
    Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例,只要其中一个实例改变状态新的大实例就会改变状态。

错误处理

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

promise
  .then(function(data) { 
    // success
  })
  .catch(function(err) {
    // error
  });

总结

Promise是一个用可靠语义来增强回调的模式,所以它的行为更合理更可靠。通过将回调的 控制倒转 反置过来,我们将控制交给一个可靠的系统(Promise),它是为了将你的异步处理进行清晰的表达而特意设计的。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Genarator

遍历器(Intertor)

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

有些数据结构原生部署了Iterator接口,可以直接调用原生Array、Map、Set、NodeList 对象等等。对象是没有此接口的,如果需要可以用Map代替。或者用Generator 函数。
这些数据结构有此接口的原因是它们有Symbol.iterator属性, 因为是Symbol值要用[]调用,此属性为一个函数,叫遍历器生成函数,调用后返回遍历器。

原生数组可以直接这样遍历。
let arr = [1,2,3]
let arrIter = arr[Symbol.iterator]()
arrIter.next() //? ​​​​​{ value: 1, done: false }​​​​​
arrIter.next() //? { value: 2, done: false }​​​​​
arrIter.next() //? ​​​​​{ value: 3, done: false }​​​​​
arrIter.next() //? ​​​​​{ value: undefined, done: true }​​​​​
================================
Generator 函数顺便测试一下
let obj = {
    bar: {
        b : 2
    },
    [Symbol.iterator]: function *() {
        yield 1
        yield 2+3
        yield 'hello'
        yield (function () {
            return '立即执行函数的返回值'
        })()
        yield  foo = {
            a : 1
        }
        yield this.bar
    }
}
let ObjIter = obj[Symbol.iterator]()
ObjIter.next() //? ​​​​​{ value: 1, done: false }​​​​​
ObjIter.next() //? ​​​​​{ value: 5, done: false }​​​​​
ObjIter.next() //? ​​​​​{ value: 'hello', done: false }​​​​​
ObjIter.next() //? ​​​​​{ value: '立即执行函数的返回值', done: false }​​​​​
ObjIter.next() //? ​​​​​{ value: { a: 1 }, done: false }​​​​​
ObjIter.next() //? ​​​​​​​​​​​​​​​​​​​​{ value: { b: 2 }, done: false }​​​​​

let arr = [...obj] //? ​​​​​[ 1, 5, 'hello', '立即执行函数的返回值', { a: 1 },{ b: 2 } ]​​​​​

for (const x of obj) {
    console.log(x) //? 1, 5, 'hello', '立即执行函数的返回值', { a: 1 } ,{ b: 2 }
}
for (const key in obj) {
    const element = obj[key]
    console.log(element) //?​​​​​{ b: 2 }​​​​​
}
Reflect.ownKeys(obj) //? ​​​​​[ 'bar', Symbol(Symbol.iterator) ]​​​​​
for (const x of Reflect.ownKeys(obj)) {
    console.log(obj[x]) //? ​​​​​{ b: 2 },​​​​​[λ: [Symbol.iterator]]​​​​​
}

想让对象有遍历器接口只要给它一个Symbol.iterator属性就行,这个属性也得是个函数。上面的做法,将一个generator函数赋予给对象的遍历器属性,当遍历这个对象时就会调用此函数,但是遍历结果是此函数内部的表达式结果,与对象属性无关。
下面两种方法可以遍历对象属性的值,一种就是不给它赋予遍历器属性而是借其他数据结构之手,另一种在Symbol.iterator属性函数里写逻辑将对象属性遍历后返回属性值。

for (let key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}
==========================
function* entries() {
    for (let key of Reflect.ownKeys(this)) {
      yield [key, this[key]];
    }
}
  let obj1 = {
      a : 1,
      b: 2
  }
  obj1[Symbol.iterator]  = entries
  for (const [key, value] of obj1) {
      console.log(`${key}: ${value}`)
  }

Generator

generator函数是遍历器对象生成函数,执行后返回遍历器对象。*号注明函数为生成器函数,在函数内部使用yield表达式产出不同的内部状态

function* foo() {
    yield 1
    yield 2
    return 3
}
let bar = foo()
bar.next() //? ​​​​​{ value: 1, done: false }​​​​​
bar.next()  //? ​​​​​{ value: 2, done: false }​​​​​
bar.next()  //? ​​​​​{ value: 3, done: true }​​​​​

当调用next方法的时候,函数内部语句会运行到第一个yield停止并将yield后的表达式计算出结果后返回,与return类似,不同的是,yiled后的语句会在调用下一个next()方法后继续执行知道遇到第二个yield

状态机与协程

这个和此篇无关,所以只写个标题吧。

Generator内外部通信

生成器函数不仅可以一步一步的运行下去输出结果,还可以传入不同数据来操控下面的步骤。

next方法的参数

yield意为产出,其后跟的表达式的结果在计算后会直接产出,函数内部并不能得到

function* foo(x) {
    let y = 2*(yield x+1)
    yield y
}
let bar = foo(3)
bar.next() //? ​​​​​{ value: 4, done: false }​​​​​
bar.next()  //? ​​​​​{ value: NaN, done: false }​​​​​
bar.next()  //? ​​​​​{ value: undefined, done: true }​​​​​

第一步运行的是x+1得出的结果为4产出了,外部可以看到。当内部需要它再参与计算的时候就发现根本不存在变成2*undefined结果就是NaN

如果yield算产出,那next方法的参数算投入。参数会被当作上个yield表达式的结果值,如果没有上个自然无效。所以第一个next()算是启动遍历器的不能加参数。
上例改一下

function* foo(x) {
    let y = x*(yield x+1)
    console.log(x) //?3
    yield y
}
let bar = foo(3)
bar.next() //? ​​​​​{ value: 4, done: false }​​​​​
bar.next(5) //? ​​​​​{ value: 15, done: false }​​​​​

return和throw

return方法,可以返回给定的值,并且终结遍历 Generator 函数。

function *foo() {
    yield 1
    yield 2
    yield 3
}
let bar = foo()
bar.next() //? ​​​​​{ value: 1, done: false }​​​​​
bar.return('结束了') //? ​​​​​{ value: '结束了', done: true }​​​​​
bar.next() //? ​​​​​{ value: undefined, done: true }​​​​​

如果函数内部有finally就会在调用return后立刻执行,finally内代码,最后执行return

function *foo() {
    try {
        yield 1
    } finally {
        yield 2
    }
    yield 3
}
let bar = foo()
bar.next() //?  ​​​​​{ value: 1, done: false }​​​​​
bar.return('结束了') //? ​​​​​{ value: 2, done: false }​​​​​
bar.next() //? ​​​​​{ value: '结束了', done: true }​​​​​

throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

function *foo() {
   try {
       yield 1
       yield 2
   } catch (e) {
       yield 3
       console.log(e) //?出错了
   }
    yield 4
}
let bar = foo()
bar.next() //? ​​​​​{ value: 1, done: false }​​​​​
bar.throw('出错了') //? ​​​​​{ value: 3, done: false }​​​​​
bar.next() //? ​​​​​{ value: 4, done: false }​​​​​

throw方法被调用后,会结束调用下面的代码,并从catch后继续,注意throw自带next(),传入的错误值会被捕获成为catch的参数,并传出下一个yield的值。

总结

nextreturnthrow可以粗略的理解为替换yield表达式,
next是换为值
return是换为return语句
throw是换为throw语句
不同点在于,生成器函数内如果存在try catch或finally即使return或throw了还是可以继续运行。

Generator与Promise

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。具体的就是上文写的,
下面的代码用Fetch实现,它是基于promise的,在node环境中使用要装个插件

npm install node-fetch --save

简单的用下

const fetch = require('node-fetch');
let url = 'http://api.apiopen.top/singlePoetry' //下文省略上面两句
fetch(url)
    .then(res => res.json())
    .then(json => console.log(json));
会随机出个诗句,网上随便找的接口
{ code: 200,
  message: '成功!',
  result:
   { author: '陆游',
     origin: '夜泊水村',
     category: '古诗文-人生-青春',
     content: '一身报国有万死,双鬓向人无再青。' } }

异步

回到起点,为什么需要回调?因为如果把耗时的代码写成同步的形式,那代码就会卡在那里也就是发生了阻塞,所以需要先执行完同步代码,最后执行耗时代码得出结果后再执行处理结果的代码,这个过程叫做用回调完成异步。
generator函数中的yield表达式有两个特性,一是向函数外传出消息、二是暂停函数。所以generator函数天生就具有异步的特征,如果手动执行generator函数,那此函数是不会执行的,自然会在同步代码后执行,当我们执行函数时,在yeild后放异步操作,那函数就会暂停于此直到执行完毕,当传出结果就可以继续next()执行下一步,这个下一步就和回调函数一个含义。

function *foo() {
    try {
        let result = yield fetch(url)
        console.log(result)
    } catch (error) {
        console.log(error)
    }
}
===================================
let it = foo()
let result = it.next().value
result
    .then(data => data.json())
    .then(data => it.next(data))
    .catch(e => it.throw(e))

可以看到let result = yield fetch(url) console.log(result)将异步回调的形式,写成了同步的方式,没有回调地狱也没有无限的then。
这个函数问题是执行起来复杂,虽然函数内部包装异步操作将异步改同步,但是真正运行的时候要多几步,首先要先启动第一步,其次yield是产出值,所以代码中的result其实是等于undefined,需要在bar.next(data)下一次执行时传入上次的结果值 并赋值。
如果生成器函数里包含着一个异步操作序列,很多步的promise,那generator内的复杂度其实不高,写成同步形式反而更好分清执行顺序。但是运行起来的时候就要在外面写一长串的promise链了,因为要将函数分步运行并且将yield传出的异步结果值再传进去。

基于Promise的自动执行

(还有基于回调的自动执行 Thunk函数,不写了,所有下面yield后跟的异步操作必须是基于promise的,如果不是要先用new Promise包装。)
上文的问题就是写generator函数简单,执行起来麻烦,如果自动执行的话就完美了。

function run(fn) {
    let it = fn()
    function next(data) {
      let result = it.next(data)
      if (result.done) return result.value
      result.value
                .then(res => res.json())
                .then((data) => next(data))
                .catch(e => it.throw(e))
    }
  
    next()
}                  

一个利用递归简单的实现。运行一下

function *foo() {
    try {
        let result = yield fetch(url)
        console.log(result)
        let result2 = yield fetch(url)
        console.log(result2)
    } catch (error) {
        console.log(error)
    }
}
run(foo) //结果是吟了两句诗

只是简单的实现,下面复制下《你不知道的js》与es6入门中的实现

function run(gen) {
    var args = [].slice.call( arguments, 1), it;

    // 在当前的上下文环境中初始化generator
    it = gen.apply( this, args );

    // 为generator的完成返回一个promise
    return Promise.resolve()
        .then( function handleNext(value){
            // 运行至下一个让出的值
            var next = it.next( value );

            return (function handleResult(next){
                // generator已经完成运行了?
                if (next.done) {
                    return next.value;
                }
                // 否则继续执行
                else {
                    return Promise.resolve( next.value )
                        .then(
                            // 在成功的情况下继续异步循环,将解析的值送回generator
                            handleNext,

                            // 如果`value`是一个拒绝的promise,就将错误传播回generator自己的错误处理g
                            function handleErr(err) {
                                return Promise.resolve(
                                    it.throw( err )
                                )
                                .then( handleResult );
                            }
                        );
                }
            })(next);
        } );
}
--------------------------------------------------------------------------------

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF();
    function step(nextF) {
      try {
        var next = nextF();
      } catch(e) {
        return reject(e); 
      }
      if(next.done) {
        return resolve(next.value);
      } 
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });      
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

async

语言内置的,不需要执行器的generator异步实现。

async  function  foo() {
    try {
        let result = await fetch(url)
        let data = await result.json()
        console.log(data)
    } catch (error) {
        console.log(error)
    }
}
foo()

await

yeild后面只能跟promise对象,且结果值会传出函数外还要再传进来一次,await后面不仅可以跟promise对象还可以跟原生类型,更重要的是await的结果可以直接赋值,不需要传来传去。当yield两个特性:产出值与自我阻塞,去掉了产出那就剩下await

async

async返回的是Promise,所以可以在后面用then添加回调函数

总结

写了generator之后async确实没什么好写的了。因为这个用起来确实太方便了,将异步改为同步写法,流程明了,错误处理也很清晰。

总结

回调,promise,async,其实相当于一步一步的封装。promise通过固定的形式封装了回调,在then里添加回调函数在catch里捕捉错误,并且通过自身特性固化了异步操作的完成与否。相当于好用版的回调,async通过进行了进一步的封装,将promise表达式跟在await后面等待解析,并将结果赋值下个语句处理,达成了用同步代码的写法组织异步代码。所以如果有原生异步回调的方法或第三方库,想要方便的使用async就需要一步步封装了。
下一篇写分别封装好三者,并在复杂的异步环境下比较。

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

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,296评论 5 22
  • 在此处先列下本篇文章的主要内容 简介 next方法的参数 for...of循环 Generator.prototy...
    醉生夢死阅读 1,436评论 3 8
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,693评论 0 5
  • 测试
    hello12327阅读 86评论 0 0
  • 这无奈而又炽热的执念,是生活的全部。
    Jack张了了阅读 163评论 0 1