入理解异步编程的核心 Promise和Generator、Async/await 等异步编程的语法糖

其实在 ES6 标准出现之前,社区就最早提出了 Promise 的方案,后随着 ES6 将其加入进去,才统一了其用法,并提供了原生的 Promise 对象,Promise 也是日常前端开发使用比较多的编程方式。

Promise 的基本情况

如果一定要解释 Promise 到底是什么,简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。我们来简单看一下 Promise 实现的链式调用代码,如下所示。

function read(url) {

    return new Promise((resolve, reject) => {

        fs.readFile(url, 'utf8', (err, data) => {

            if(err) reject(err);

            resolve(data);

        });

    });

}

read(A).then(data => {

    return read(B);

}).then(data => {

    return read(C);

}).then(data => {

    return read(D);

}).catch(reason => {

    console.log(reason);

});

结合上面的代码,我们一起来分析一下 Promise 内部的状态流转情况,Promise 对象在被创建出来时是待定的状态,它让你能够把异步操作返回最终的成功值或者失败原因,和相应的处理程序关联起来。

一般 Promise 在执行过程中,必然会处于以下几种状态之一。

1、待定(pending):初始状态,既没有被完成,也没有被拒绝。

2、已完成(fulfilled):操作成功完成。

3、已拒绝(rejected):操作失败。

待定状态的 Promise 对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promise 的 then 方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是一个 Promise, 所以它们可以继续被链式调用。

关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆,你需要在编程过程中加以注意。文字描述比较晦涩,我们直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况,如下所示(图片来源于网络)

从上图可以看出,我们最开始创建一个新的 Promise 返回给 p1 ,然后开始执行,状态是 pending,当执行 resolve 之后状态就切换为 fulfilled,执行 reject 之后就变为 rejected 的状态。

关于 Promise 的状态切换如果你想深入研究,可以学习一下“有限状态机”这个知识点。日常中比较常见的状态机有很多,比如马路上的红绿灯。

那么,Promise 的基本情况先介绍到这里,我们再一起来分析下,Promise 如何解决回调地狱的问题。

Promise 如何解决回调地狱

首先,请你再回想一下什么是回调地狱,回调地狱有两个主要的问题:

1、多层嵌套的问题;

2、每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

这两种问题在“回调函数时代”尤为突出,Promise 的诞生就是为了解决这两个问题。Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。

下面我们通过一段代码来说明,如下所示。

let readFilePromise = filename => {

  return new Promise((resolve, reject) => {

    fs.readFile(filename, (err, data) => {

      if (err) {

        reject(err)

      } else {

        resolve(data)

      }

    })

  })

}

readFilePromise('1.json').then(data => {

  return readFilePromise('2.json')

});

从上面的代码中可以看到,回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定。接下来我们针对上面的代码做一下微调,如下所示。

let x = readFilePromise('1.json').then(data => {

  return readFilePromise('2.json')  //这是返回的Promise

});

x.then(/* 内部逻辑省略 */)

我们根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。这便是返回值穿透的效果,这两种技术一起作用便可以将深层的嵌套回调写成下面的形式。

readFilePromise('1.json').then(data => {

    return readFilePromise('2.json');

}).then(data => {

    return readFilePromise('3.json');

}).then(data => {

    return readFilePromise('4.json');

});

这样就显得清爽了许多,更重要的是,它更符合人的线性思维模式,开发体验也更好,两种技术结合产生了链式调用的效果。

这样解决了多层嵌套的问题,那另外一个问题,即每次任务执行结束后分别处理成功和失败的情况怎么解决的呢?Promise 采用了错误冒泡的方式。其实很容易理解,我们来看看效果。

readFilePromise('1.json').then(data => {

    return readFilePromise('2.json');

}).then(data => {

    return readFilePromise('3.json');

}).then(data => {

    return readFilePromise('4.json');

}).catch(err => {

  // xxx

})

这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。从上面的这些代码中可以看到,Promise 解决效果也比较明显:实现链式调用,解决多层嵌套问题;实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题。

接下来我们再看看 Promise 提供了哪些静态的方法。

Promise 的静态方法

我会从语法、参数以及方法的代码几个方面来分别介绍 all、allSettled、any、race 这四种方法。

all 方法

语法: Promise.all(iterable)

参数: 一个可迭代对象,如 Array。

描述: 此方法对于汇总多个 promise 的结果很有用,在 ES6 中可以将多个 Promise.all 异步请求并行操作,返回结果一般有下面两种情况。

1、当所有结果成功返回时按照请求顺序返回成功。

2、当其中有一个失败方法时,则进入失败方法。

我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好,请看代码片段。

//1.获取轮播数据列表

function getBannerList(){

  return new Promise((resolve,reject)=>{

      setTimeout(function(){

        resolve('轮播数据')

      },300)

  })

}

//2.获取店铺列表

function getStoreList(){

  return new Promise((resolve,reject)=>{

    setTimeout(function(){

      resolve('店铺数据')

    },500)

  })

}

//3.获取分类列表

function getCategoryList(){

  return new Promise((resolve,reject)=>{

    setTimeout(function(){

      resolve('分类数据')

    },700)

  })

}

function initLoad(){

  Promise.all([getBannerList(),getStoreList(),getCategoryList()])

  .then(res=>{

    console.log(res)

  }).catch(err=>{

    console.log(err)

  })

}

initLoad()

从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all 来实现,看起来更清晰、一目了然。

下面我们再来看另一种方法。

allSettled 方法

Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。

我们来看一下用 allSettled 实现的一段代码。

const resolved = Promise.resolve(2);

const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {

  console.log(results);

});

// 返回结果:

// [

//    { status: 'fulfilled', value: 2 },

//    { status: 'rejected', reason: -1 }

// ]

从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。你也可以根据 all 方法提供的业务场景的代码进行改造,其实也能知道多个请求发出去之后,Promise 最后返回的是每个参数的最终状态。

接下来看一下 any 这个方法。

any 方法

语法: Promise.any(iterable)

参数: iterable 可迭代的对象,例如 Array。

描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

还是对上面 allSettled 这段代码进行改造,我们来看下改造完的代码和执行结果。

const resolved = Promise.resolve(2);

const rejected = Promise.reject(-1);

const anyPromise = Promise.any([resolved, rejected]);

anyPromise.then(function (results) {

  console.log(results);

});

// 返回结果:

// 2

从改造后的代码中可以看出,只要其中一个 Promise 变成 fulfilled 状态,那么 any 最后就返回这个 Promise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2。

我们最后来看一下 race 方法。

race 方法

语法: Promise.race(iterable)

参数: iterable 可迭代的对象,例如 Array。

描述: race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。

我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。

//请求某个图片资源

function requestImg(){

  var p = new Promise(function(resolve, reject){

    var img = new Image();

    img.onload = function(){ resolve(img); }

    img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';

  });

  return p;

}

//延时函数,用于给请求计时

function timeout(){

  var p = new Promise(function(resolve, reject){

    setTimeout(function(){ reject('图片请求超时'); }, 5000);

  });

  return p;

}

Promise.race([requestImg(), timeout()])

.then(function(results){

  console.log(results);

})

.catch(function(reason){

  console.log(reason);

});

从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。

综上,这四种方法的参数传递形式基本是一致的,但是最后每个方法实现的功能还是略微有些差异的,这一点你需要留意。

上面探讨了 JS 异步编程以及其中 Promise 的编程方式,那么下面来了解另外两种异步编程的方式。Generator 是 ES6 标准中的异步编程方式,而 async/await 是 ES7 标准中的。

Generator 基本介绍

Generator(生成器)是 ES6 的新关键词,学习起来比较晦涩难懂,那么什么是 Generator 的函数呢?通俗来讲 Generator 是一个带星号的“函数”(它并不是真正的函数,下面的代码会为你验证),可以配合 yield 关键字来暂停或者执行函数。我们来看一段使用 Generator 的代码,如下所示。

function* gen() {

  console.log("enter");

  let a = yield 1;

  let b = yield (function () {return 2})();

  return 3;

}

var g = gen()          // 阻塞住,不会执行任何语句

console.log(g.next())

console.log(g.next())

console.log(g.next())

console.log(g.next())

// output:

// { value: 1, done: false }

// { value: 2, done: false }

// { value: 3, done: true }

// { value: undefined, done: true }

结合上面的代码,我们分析一下 Generator 函数的执行情况。Generator 中配合使用 yield 关键词可以控制函数执行的顺序,每当执行一次 next 方法,Generator 函数会执行到下一个存在 yield 关键词的位置。

总结下来,Generator 的执行有这几个关键点。

1、调用 gen() 后,程序会阻塞住,不会执行任何语句。

2、调用 g.next() 后,程序继续执行,直到遇到 yield 关键词时执行暂停。

3、一直执行 next 方法,最后返回一个对象,其存在两个属性:value 和 done。

这就是 Generator 的基本内容,其中提到了 yield 这个关键词,下面我们就来看看它的基本情况。

yield基本介绍

yield 同样也是 ES6 的新关键词,配合 Generator 执行以及暂停。yield 关键词最后返回一个迭代器对象,该对象有 value 和 done 两个属性,其中 done 属性代表返回值以及是否完成。yield 配合着 Generator,再同时使用 next 方法,可以主动控制 Generator 执行进度。

前面说 Generator 的时候,我举的是一个生成器函数的示例,下面我们看看多个 Generator 配合 yield 使用的情况,请看下面一段代码。

function* gen1() {

    yield 1;

    yield* gen2();

    yield 4;

}

function* gen2() {

    yield 2;

    yield 3;

}

var g = gen1();

console.log(g.next())

console.log(g.next())

console.log(g.next())

console.log(g.next())

// output:

// { value: 1, done: false }

// { value: 2, done: false }

// { value: 3, done: false }

// { value: 4, done: false }

// {value: undefined, done: true}

从上面的代码中可以看出,使用 yield 关键词的话还可以配合着 Generator 函数嵌套使用,从而控制函数执行进度。这样对于 Generator 的使用,以及最终函数的执行进度都可以很好地控制,从而形成符合你设想的执行顺序。即便 Generator 函数相互嵌套,也能通过调用 next 方法来按照进度一步步执行。

那么讲到这里你可能会有几个疑惑,Generator 和异步编程有什么联系?怎么才可以把 Generator 函数按照顺序一次性执行完呢?接着往下看,你就会明白了。

thunk 函数介绍

下面我带你看一下 thunk 函数,直接说概念可能会有些晦涩,我们通过一段代码来了解一下什么是 thunk 函数,就拿判断数据类型来举例,代码如下。

let isString = (obj) => {

  return Object.prototype.toString.call(obj) === '[object String]';

};

let isFunction = (obj) => {

  return Object.prototype.toString.call(obj) === '[object Function]';

};

let isArray = (obj) => {

  return Object.prototype.toString.call(obj) === '[object Array]';

};

....

可以看到,其中出现了非常多重复的数据类型判断逻辑,平常业务开发中类似的重复逻辑的场景也同样会有很多。我们将它们做一下封装,如下所示。

let isType = (type) => {

  return (obj) => {

    return Object.prototype.toString.call(obj) === `[object ${type}]`;

  }

}

那么封装了之后我们可以这么来使用,从而来减少重复的逻辑代码,如下所示。

let isString = isType('String');

let isArray = isType('Array');

isString("123");    // true

isArray([1,2,3]);  // true

相应的 isString 和 isArray 是由 isType 方法生产出来的函数,通过上面的方式来改造代码,明显简洁了不少。像 isType 这样的函数我们称为 thunk 函数,它的基本思路都是接收一定的参数,会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能。

这样的函数在 JS 的编程过程中会遇到很多,尤其是你在阅读一些开源项目时,抽象度比较高的 JS 代码往往都会采用这样的方式。

那么请你想一下,Generator 和 thunk 函数的结合是否能为我们带来一定的便捷性呢?

Generator 和 thunk 结合

下面我以文件操作的代码为例,看一下 Generator 和 thunk 的结合能够对异步操作产生什么样的效果。

const readFileThunk = (filename) => {

  return (callback) => {

    fs.readFile(filename, callback);

  }

}

const gen = function* () {

  const data1 = yield readFileThunk('1.txt')

  console.log(data1.toString())

  const data2 = yield readFileThunk('2.txt')

  console.log(data2.toString)

}

let g = gen();

g.next().value((err, data1) => {

  g.next(data1).value((err, data2) => {

    g.next(data2);

  })

})

readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了。上面第三段代码执行起来嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可读性不强,因此我们有必要把执行的代码封装优化一下,如下所示。

function run(gen){

  const next = (err, data) => {

    let res = gen.next(data);

    if(res.done) return;

    res.value(next);

  }

  next();

}

run(g);

改造完之后,我们可以看到 run 函数和上面的执行效果其实是一样的。代码虽然只有几行,但其包含了递归的过程,解决了多层嵌套的问题,并且完成了异步操作的一次性的执行效果。这就是通过 thunk 函数完成异步操作的情况,你可以好好体会一下。

以上介绍了 Generator 和 thunk 结合的情况,其实 Promise 也可以和 Generator 配合,以实现上面的效果,下面我们来看一下这种情况。

Generator 和 Promise 结合

还是利用上面的输出文件的例子,对代码进行改造,如下所示。

// 最后包装成 Promise 对象进行返回

const readFilePromise = (filename) => {

  return new Promise((resolve, reject) => {

    fs.readFile(filename, (err, data) => {

      if(err) {

        reject(err);

      }else {

        resolve(data);

      }

    })

  }).then(res => res);

}

let g = gen();

// 这块和上面 thunk 的方式一样

const gen = function* () {

  const data1 = yield readFilePromise('1.txt')

  console.log(data1.toString())

  const data2 = yield readFilePromise('2.txt')

  console.log(data2.toString)

}

// 这块和上面 thunk 的方式一样

function run(gen){

  const next = (err, data) => {

    let res = gen.next(data);

    if(res.done) return;

    res.value.then(next);

  }

  next();

}

run(g);

从上面的代码可以看出,thunk 函数的方式和通过 Promise 方式执行效果本质上是一样的,只不过通过 Promise 的方式也可以配合 Generator 函数实现同样的异步操作。希望你能参照上面 thunk 的例子,仔细体会一下递归调用的过程。

co 函数库

co 函数库是著名程序员 TJ 发布的一个小工具,用于处理 Generator 函数的自动执行。核心原理其实就是上面讲的通过和 thunk 函数以及 Promise 对象进行配合,包装成一个库。它使用起来非常简单,比如还是用上面那段代码,第三段代码就可以省略了,直接引用 co 函数,包装起来就可以使用了,代码如下。

const co = require('co');

let g = gen();

co(g).then(res =>{

  console.log(res);

})

这段代码比较简单,几行就完成了之前写的递归的那些操作。那么为什么 co 函数库可以自动执行 Generator 函数,它的处理原理是什么呢?

1、因为 Generator 函数就是一个异步操作的容器,它需要一种自动执行机制,co 函数接受 Generator 函数作为参数,并最后返回一个 Promise 对象。

2、在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数;如果不是就返回,并将 Promise 对象的状态改为 resolved。

3、co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulfilled 函数。这主要是为了能够捕捉抛出的错误。

4、关键的是 next 函数,它会反复调用自身。

关于 co 的内部原理,你可以去 co 的源码库学习。代码不是很多,也比较清晰,按照上面我所讲的思路,你可以试着去理解,这对于提升你的 JavaScript 编码能力是很有帮助的。

那么,说完了 co 函数库,我们最后就来探究异步编程的终极解决方案:async/await。


async/await 介绍

JS 的异步编程从最开始的回调函数的方式,演化到使用 Promise 对象,再到 Generator+co 函数的方式,每次都有一些改变,但又让人觉得不彻底,都需要理解底层运行机制。

而 async/await 被称为 JS 中异步终极解决方案,它既能够像 co+Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无须借助任何第三方库。

接下来,我们就从原理的角度来看看 async/await 这个语法糖背后到底做了哪些优化和改进,使得我们用起来会更加方便。还是按照上面 Generator 和 Promise 结合的例子,使用 async/await 语法糖来进行改造,请看改造后的代码。

// readFilePromise 依旧返回 Promise 对象

const readFilePromise = (filename) => {

  return new Promise((resolve, reject) => {

    fs.readFile(filename, (err, data) => {

      if(err) {

        reject(err);

      }else {

        resolve(data);

      }

    })

  }).then(res => res);

}

// 这里把 Generator的 * 换成 async,把 yield 换成 await

const gen = async function() {

  const data1 = await readFilePromise('1.txt')

  console.log(data1.toString())

  const data2 = await readFilePromise('2.txt')

  console.log(data2.toString)

}

从上面的代码中可以看到,虽然我们简单地将 Generator 的 * 号换成了 async,把 yield 换成了 await,但其实 async 的内部做了不少工作。我们根据 async 的原理详细拆解一下,看看它到底做了哪些工作。

总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点。

1、内置执行器:Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行。

2、适用性更好:co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束。

3、可读性更好:async 和 await,比起使用 * 号和 yield,语义更清晰明了。

说了这么多优点,我们还是通过一段简单的代码来看下 async 返回的结果,是不是使用起来更方便,请看下面的代码。

async function func() {

  return 100;

}

console.log(func());

// Promise {<fulfilled>: 100}

从执行的结果可以看出,async 函数 func 最后返回的结果直接是 Promise 对象,比较方便让开发者继续往后处理。而之前 Generator 并不会自动执行,需要通过 next 方法控制,最后返回的也并不是 Promise 对象,而是需要通过 co 函数库来实现最后返回 Promise 对象。

这样看来,ES7 加入的 async/await 的确解决了之前的问题,使开发者在编程过程中更容易理解,语法更清晰,并且也不用再单独引用 co 函数库了。因此用 async/await 写出的代码也更加优雅,相比于之前的 Promise 和 co+Generator 的方式更容易理解,上手成本也更低,不愧是 JS 异步的终极解决方案

总结

最后,整理了一下 Promise 的几个方法到下面的表格,还整理了这几个异步编程的特点,你可以对比着来回顾,以加深记忆,请看下面的表格。

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

推荐阅读更多精彩内容