細(xì)讀 ES6 | Generator 生成器

配圖源自 Freepik

在 ES6 標(biāo)準(zhǔn)中诀豁,提供了 Generator 函數(shù)(即“生成器函數(shù)”)倘核,它是一種異步編程的解決方案窿撬。在前面一篇文章中也提到一二吩跋。

一撇叁、Generator 簡(jiǎn)述

避免有人混淆概念,先說明一下:

生成器對(duì)象常被我們稱為“生成器”(Generator)劲藐,而 Generator 函數(shù)常稱為“生成器函數(shù)”(Generator Function)八堡。

由于生成器對(duì)象是實(shí)現(xiàn)了可迭代協(xié)議迭代器協(xié)議的,因此生成器也是一個(gè)迭代器聘芜,生成器也是一個(gè)可迭代對(duì)象兄渺。所以,本文有時(shí)候直接稱為迭代器汰现,其實(shí)指的就是生成器對(duì)象挂谍。

// 生成器函數(shù)
function* genFn() {}

// 生成器對(duì)象
const gen = genFn()

// 生成器對(duì)象包含 @@iterator 方法,因此滿足可迭代協(xié)議
gen[Symbol.iterator] // ? [Symbol.iterator]() { [native code] }

// 生成器對(duì)象含 next 方法瞎饲,因此也是滿足迭代器協(xié)議的
gen.next // ? next() { [native code] }

// 生成器對(duì)象的 @@iterator 方法返回自身(即迭代器)
gen === gen[Symbol.iterator]() // true

怎樣理解 Generator 函數(shù)口叙?

  • Generator 函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)嗅战。

  • Generator 函數(shù)返回一個(gè)生成器對(duì)象庐扫,該對(duì)象也實(shí)現(xiàn)了 Iterator 接口(也可供 for...of 等消費(fèi)使用),所以具有了 next() 方法。因此形庭,使得生成器對(duì)象擁有了開始、暫停和恢復(fù)代碼執(zhí)行的能力厌漂。

  • 生成器對(duì)象可以用于自定義迭代器和實(shí)現(xiàn)協(xié)程(coroutine)萨醒。

  • Generator 函數(shù)從字面理解,形式與普通函數(shù)很相似苇倡。在函數(shù)名稱前面加一個(gè)星號(hào)(*)富纸,表示它是一個(gè)生成器函數(shù)。盡管語(yǔ)法上與普通函數(shù)相似旨椒,但語(yǔ)法行為卻完全不同晓褪。

  • Generator 函數(shù)強(qiáng)大之處,感覺很多人沒 GET 到综慎。它可以在不同階段從外部直接向內(nèi)部注入不同的值來調(diào)整函數(shù)的行為涣仿。

生成器對(duì)象,是由 Generator 函數(shù)返回的示惊,并且它返回可迭代協(xié)議和迭代器協(xié)議好港,因此生成器對(duì)象是一個(gè)可迭代對(duì)象。

倘若對(duì)迭代器 Iterator 不熟悉的話米罚,建議先看下這篇文章:細(xì)讀 ES6 之 Iterator 迭代器钧汹,以熟悉相關(guān)內(nèi)容。

二录择、Generator 函數(shù)語(yǔ)法

1. Generator 函數(shù)

與普通函數(shù)聲明類似拔莱,但有兩個(gè)特有特征:

  • 一個(gè)是 function 關(guān)鍵字與函數(shù)名稱之間有一個(gè)星號(hào) *
  • 二是函數(shù)體內(nèi)使用 yield 表達(dá)式隘竭,以定義不同的內(nèi)部狀態(tài)塘秦。

星號(hào) * 位置沒有明確限制,只要處于關(guān)鍵字與函數(shù)名之間即可货裹,空格可用可無嗤形,不影響。還有弧圆,這里 yield 是“產(chǎn)出”的意思赋兵。

實(shí)際中,基本上使用字面量形式去聲明一個(gè) Generator 函數(shù)搔预,很少用到構(gòu)造函數(shù) GeneratorFunction 來聲明的霹期。

例如,先來一個(gè)最簡(jiǎn)單的示例拯田。

// generatorFn 是一個(gè)生成器函數(shù)
function* generatorFn() {
  console.log('do something...')
  // other statements
}

// 調(diào)用生成器函數(shù)历造,返回一個(gè)生成器對(duì)象。
const gen = generatorFn()

// 注意,上面像平常一樣調(diào)用函數(shù)吭产,并不會(huì)執(zhí)行函數(shù)體內(nèi)部的邏輯/語(yǔ)句侣监。
// 需要(不斷地)調(diào)用生成器對(duì)象的 next() 方法,才會(huì)開始(繼續(xù))執(zhí)行內(nèi)部的語(yǔ)句臣淤。
// 具體如何執(zhí)行橄霉,請(qǐng)看下一個(gè)示例。
gen.next()
// 執(zhí)行到這里邑蒋,才會(huì)打印出:"do something..."
// 且 gen.next() 的返回值是:{ value: undefined, done: true }

上述示例中姓蜂,調(diào)用生成器函數(shù)被調(diào)用,并不會(huì)立即立即執(zhí)行函數(shù)體內(nèi)部的語(yǔ)句医吊。另外钱慢,函數(shù)體內(nèi)的 yield 表達(dá)式是可選的,可以不寫卿堂,但這就失去了生成器函數(shù)本身的意義了束莫。

再看示例:

function* generatorFn() {
  console.log(1)
  yield '100'
  console.log(2)
  yield '200'
  console.log(3)
  return '300'
}

const gen = generatorFn()

前面提到,Generator 函數(shù)返回一個(gè)生成器御吞,它也是一個(gè)迭代器麦箍。因此生成器內(nèi)部存在一個(gè)指針對(duì)象,指向每次遍歷結(jié)束的位置陶珠。每調(diào)用生成器的 next() 方法挟裂,指針對(duì)象會(huì)從函數(shù)頭部(首次調(diào)用時(shí))或上一次停下來的地方開始執(zhí)行,直到遇到下一個(gè) yield 表達(dá)式(或 return 語(yǔ)句)為止揍诽。

上面一共調(diào)用了四次 next() 方法诀蓉,從結(jié)果分析:

當(dāng)首次調(diào)用 gen.next() 方法,代碼執(zhí)行到 yield '100' 會(huì)停下來(指針對(duì)象指向此處)暑脆,并返回一個(gè) IteratorResult 對(duì)象:{ value: '100', done: false }渠啤,包含 donevalue 屬性。其中 value 屬性值就是 yield 表達(dá)式的返回值 '100'添吗,donefalse 表示遍歷還沒結(jié)束沥曹。

第二次調(diào)用 next() 方法,它會(huì)從上次 yield 表達(dá)式停下的地方開始執(zhí)行碟联,直到下一個(gè) yield 表達(dá)式(指針對(duì)象也會(huì)指向此處)妓美,并返回 IteratorResult 對(duì)象:{ value: '200', done: false }

第三次調(diào)用 next() 方法,執(zhí)行過程同理鲤孵。它遇到 return 語(yǔ)句遍歷就結(jié)束了壶栋。返回 IteratorResult 對(duì)象為:{ value: '300', done: true },其中 value 對(duì)應(yīng) return 表達(dá)式的返回值普监。如果 Generator 函數(shù)內(nèi)沒有 return 語(yǔ)句贵试,那么 value 屬性值為 undefined琉兜,因此返回 { value: undefined, done: true }

第四次調(diào)用 next() 方法毙玻,返回 { value: undefined, done: true }豌蟋,原因是生成器對(duì)象 gen 已遍歷結(jié)束。當(dāng)?shù)饕驯闅v結(jié)束淆珊,無論你再調(diào)用多少次 next() 方法夺饲,都是返回這個(gè)值。

2. yield 表達(dá)式

生成器函數(shù)返回的迭代器對(duì)象施符,只有調(diào)用 next() 方法才會(huì)遍歷下一個(gè)內(nèi)部狀態(tài),所以它提供了一種可以暫停執(zhí)行的函數(shù)擂找。而 yield 表達(dá)式就是暫停標(biāo)志戳吝。

遍歷器對(duì)象的 next() 方法的運(yùn)行邏輯如下:

(1)遇到 yield 表達(dá)式,就暫停執(zhí)行后面的操作贯涎,并將緊跟在 yield 后面的那個(gè)表達(dá)式的值听哭,作為返回的對(duì)象的 value 屬性值。

(2)下一次調(diào)用next()方法時(shí)塘雳,再繼續(xù)往下執(zhí)行陆盘,直到遇到下一個(gè) yield 表達(dá)式。

(3)如果沒有再遇到新的 yield 表達(dá)式败明,就一直運(yùn)行到函數(shù)結(jié)束隘马,直到 return 語(yǔ)句為止,并將 return 語(yǔ)句后面的表達(dá)式的值妻顶,作為返回的對(duì)象的 value 屬性值酸员。

(4)如果該函數(shù)沒有 return 語(yǔ)句,則返回的對(duì)象的 value 屬性值為 undefined讳嘱。

需要注意的是幔嗦,yield 表達(dá)式后面的表達(dá)式,只有在調(diào)用 next() 方法沥潭,且內(nèi)部指針指向該語(yǔ)句時(shí)才會(huì)執(zhí)行邀泉,因此相當(dāng)于為 JavaScript 提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語(yǔ)法功能。

function* generatorFn() {
  // 請(qǐng)注意 yield 關(guān)鍵字后面的表達(dá)式钝鸽,是惰性求值的汇恤!
  // 為了更明顯地說明問題,這里使用 IIFE寞埠。
  yield (function () {
    console.log('here here')
    return 1
  })()
}

const gen = generatorFn()
gen.next() // 調(diào)用 next 方法才會(huì)打印出:"here here"

上面的示例中屁置,yield 后面的立即執(zhí)行函數(shù)表達(dá)式,不會(huì)在調(diào)用 generatorFn() 后立即求值仁连,只會(huì)在調(diào)用 gen.next() 方法才會(huì)進(jìn)行求值蓝角。

3. yield 與 return 的特點(diǎn)及異同點(diǎn)
  • 無論普通函數(shù)還是 Generator 函數(shù)阱穗,最多只能有一個(gè) return 語(yǔ)句,表示該函數(shù)的終止使鹅。若沒有顯式聲明揪阶,相當(dāng)于在函數(shù)體最后 return undefined

  • yield 表達(dá)式患朱,只能在 Generator 函數(shù)內(nèi)使用鲁僚,否則會(huì)報(bào)錯(cuò)。

  • 一個(gè) Generator 函數(shù)中裁厅,可以有多個(gè) yield 語(yǔ)句冰沙。每個(gè) yield 語(yǔ)句對(duì)應(yīng)生成器的一個(gè)狀態(tài)。

  • yield 表達(dá)式具備“記憶”功能执虹,而 return 是不具備的拓挥。每當(dāng)遇到 yield,函數(shù)暫停執(zhí)行袋励,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行侥啤。它是由迭代器內(nèi)部由一個(gè)(指針)對(duì)象去維護(hù)的,我們無需關(guān)心茬故。

  • Generator 函數(shù)內(nèi)部可以不用 yield 表達(dá)式盖灸。但如果這樣使用 Generator 函數(shù)就沒意義了,不如考慮使用普通函數(shù)磺芭。

  • 理論上赁炎,yield 表達(dá)式可以返回任何值。若語(yǔ)句僅有 yield;徘跪,相當(dāng)于 yield undefined;甘邀。

4. yield 注意點(diǎn)

請(qǐng)注意以下幾點(diǎn),否則可能會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤垮庐。

// ? 1. yield 只能用在 Generator 函數(shù)里面
function* foo() {
  [1].map(item => {
    yield item // SyntaxError
    // 這里 Array.prototype.map() 的回調(diào)函數(shù)松邪,并不是一個(gè)生成器函數(shù)
  })
}

// ? 2. 當(dāng) yield 表達(dá)式作用于另外一個(gè)表達(dá)式,必須放入圓括號(hào)里面
function* foo() {
  // Wrong
  // console.log('Hello' + yield) // SyntaxError
  // console.log('Hello' + yield 'World') // SyntaxError

  // Correct
  console.log('Hello ' + (yield))
  console.log('Hello ' + (yield 'World'))
  // 不過要注意的是哨查,(多次)調(diào)用生成器實(shí)例的 next() 方法
  // 以上兩個(gè)都會(huì)打印出 "Hello undefined"逗抑,并不是想象中的 "Hello World"。
  // yield 表達(dá)式本身沒有返回值寒亥,或者說總是返回 undefined邮府,
  // yield 關(guān)鍵字后面的表達(dá)式結(jié)果,只會(huì)作為 IteratorResult 對(duì)象的 value 值溉奕。
}

// ? yield 表達(dá)式可以用作函數(shù)參數(shù)褂傀,或放在表達(dá)式的右邊,可以不加括號(hào)
function* foo() {
  const bar = (a, b) => {
    console.log('paramA:', a)
    console.log('paramB:', b)
  }
  bar(yield 'AAA', yield 'BBB')

  let input = yield
  return input
  // 多次調(diào)用 next 方法加勤,bar 函數(shù)中仙辟,只會(huì)打印出:"paramA: undefined"同波、"paramB: undefined"
  // 原因第 2 點(diǎn)提到過了
}

Generator 函數(shù)還可以這樣用:

// 函數(shù)聲明形式
function* generatorFn() {}

// 函數(shù)表達(dá)式形式
const generatorFn = function* () {}

// 作為對(duì)象屬性
const obj = {
  * generatorFn() {} // or
  // generatorFn: function* () {}
}

// 作為類的實(shí)例方法,或類的靜態(tài)方法
class Foo {
  static * generatorFn() {}
  * generatorFn() {}
}

三叠国、Generator 應(yīng)用詳解

前面提到的只是生成器函數(shù)的語(yǔ)法與簡(jiǎn)單用法未檩,并沒有體現(xiàn)其強(qiáng)大之處冀宴。

1. Generator 與 Iterator

生成器里面是部署了 Iterator 接口鲫剿,因此可以把它當(dāng)做迭代器供 for...of 等使用佑女。前面一篇文章提到斋射,使用生成器函數(shù)來實(shí)現(xiàn)自定義迭代器。

看示例:

class Counter {
  constructor([min = 0, max = 10]) {
    this.min = min
    this.max = max
  }

  *[Symbol.iterator]() {
    let point = this.min
    const end = this.max
    while (point <= end) {
      yield point++
    }
  }
}

const counter = new Counter([0, 3])
const gen = counter[Symbol.iterator]() // gen 既是生成器拉队,又是迭代器
for (const x of gen) {
  console.log(x)
}
// 依次打臃:0缀程、1香追、2怜奖、3
2. next 方法傳參

yield 表達(dá)式本身沒有返回值,或者說總是返回 undefined翅阵。next() 方法可以帶一個(gè)參數(shù),該參數(shù)被作為上一個(gè) yield 表達(dá)式的返回值迁央。

function* generatorFn() {
  let str = 'Hello ' + (yield 'World')
  console.log(str)
  return str
}
const gen1 = generatorFn()
const gen2 = generatorFn()

不傳遞參數(shù)時(shí)掷匠,執(zhí)行結(jié)果如下:

// 第一次調(diào)用 next()
console.log(gen1.next())
// 打印出:{ done: false, value: 'World' }

// 第二次調(diào)用 next()
console.log(gen1.next())
// "Hello undefined"
// { done: true, value: 'Hello undefined' }

相信剛開始學(xué) Generator 的童鞋,會(huì)認(rèn)為在第二次調(diào)用 gen1.next() 方法時(shí)岖圈,str 變量的值會(huì)變成 'Hello World'讹语,當(dāng)初我也是這么認(rèn)為的,但這是錯(cuò)誤的蜂科,str 的值 'Hello undefined'顽决。

yield 關(guān)鍵字后面的表達(dá)式結(jié)果,僅作為 next() 方法的返回對(duì)象 IteratorResultvalue 屬性值导匣,即:{ done: false, value: 'World' }才菠。

但如果我們?cè)?next() 方法進(jìn)行傳參呢?

// 第一次調(diào)用 next()
console.log(gen2.next('Invalid'))
// 打印出:{ done: false, value: 'World' }

// 第二次調(diào)用 next()
console.log(gen2.next('JavaScript'))
// "Hello JavaScript"
// { done: true, value: 'Hello JavaScript' }

需要注意的是贡定,由于 next() 方法表示上一個(gè) yield 表達(dá)式的返回值赋访,因此在第一次使用 next() 方法時(shí),傳遞的參數(shù)是無效的缓待。只有第二次(起)調(diào)用 next() 方法蚓耽,參數(shù)才有效。從語(yǔ)義上講旋炒,第一個(gè) next() 方法用于啟動(dòng)遍歷器對(duì)象步悠,所以不用帶有參數(shù)。

第一次調(diào)用 gen2.next('Invalid') 時(shí)瘫镇,參數(shù) 'Invalid' 是無效的鼎兽,所以結(jié)果還是 { done: false, value: 'World' }答姥。

當(dāng)?shù)诙握{(diào)用 gen2.next('JavaScript') 時(shí),由于該參數(shù)將作為上一次 yield 表達(dá)式的返回值接奈。所以 let str = 'Hello ' + (yield 'World') 就相當(dāng)于 let str = 'Hello ' + 'JavaScript'踢涌,因此 str 就變成了 'Hello JavaScript',自然 gen2.next() 的返回值就是 { done: true, value: 'Hello JavaScript' }序宦。

這個(gè)功能有很重要的語(yǔ)法意義睁壁。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的互捌。通過給 next() 方法傳遞參數(shù)潘明,就有辦法在 Generator 函數(shù)開始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值秕噪。也就是說钳降,可以在 Generator 函數(shù)運(yùn)行的不同階段,從外部向內(nèi)部注入不同的值腌巾,從而調(diào)整函數(shù)行為遂填。

如果還沒弄懂,再看一個(gè)示例:

function* foo(x) {
  const y = 2 * (yield (x + 1))
  const z = yield (y / 3)
  return (x + y + z)
}

const f1 = foo(5)
f1.next()     // { done: false, value: 6 }
f1.next()     // { done: false, value: NaN }
f1.next()     // { done: true, value: NaN }

const f2 = foo(5)
f2.next()     // { done: false, value: 6 }
f2.next(12)   // { done: false, value: 8 }
f2.next(13)   // { done: true, value: 42 }

// 若結(jié)果跟你內(nèi)心預(yù)期的一樣澈蝙,那說明你弄明白了吓坚!

如果想在第一次調(diào)用 next() 方法時(shí)傳入?yún)?shù)并使其有效。換個(gè)思路就行:在 Generator 函數(shù)外面包裹一個(gè)函數(shù)灯荧,在此函數(shù)內(nèi)部調(diào)用第一次礁击,并返回生成器即可。

function genWrapper(genFn) {
  return function (...args) {
    const g = genFn(...args)
    g.next() // 其實(shí)是在內(nèi)部調(diào)用了真正意義上的第一次 next 方法逗载。
    return g
  }
}

function* generatorFn() {
  let str = 'Hello ' + (yield 'World')
  console.log(str)
  return str
}

const gen = genWrapper(generatorFn)(5)

// 這樣在外部調(diào)用 next() 就算是“第一次”
gen.next('JavaScript') // { done: true, value: 'Hello JavaScript' }

3. for...of 語(yǔ)句

for...of 語(yǔ)句是 ES6 標(biāo)準(zhǔn)新增的一種循環(huán)遍歷的的方式哆窿,為了 Iterator 而生的。只有任何部署了 Iterator 接口的對(duì)象厉斟,都可以使用它來遍歷挚躯。

那 for...of 什么時(shí)候會(huì)停止循環(huán)呢?

我們知道 for...of 內(nèi)部其實(shí)是不斷調(diào)用迭代器 next() 的過程捏膨,當(dāng) next() 方法返回的 IteratorResult 對(duì)象的 done 屬性為 true 時(shí)秧均,循環(huán)就會(huì)中止,且不包含返回對(duì)象号涯。

請(qǐng)看示例和注釋:

function* generatorFn() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
  return 6 // 一般不指定 return 語(yǔ)句
}

const gen = generatorFn()
for (const x of gen) {
  console.log(x)
}
// 依次打幽亢:1、2链快、3誉己、4、5

console.log([...gen]) // 打印結(jié)果為:[]

// ?
// 一般情況下域蜗,迭代器是不指定 return 語(yǔ)句的巨双,即返回 return undefined噪猾,
// 因?yàn)橛龅?return 時(shí),調(diào)用 next 會(huì)返回:{ done: true, value: '對(duì)應(yīng)return的結(jié)果' }
// 這時(shí)無論使用 for...of筑累,還是數(shù)組解構(gòu)或其他袱蜡,它們看到狀態(tài) done 為 true(表示遍歷結(jié)束),
// 它們就停止往下遍歷了慢宗,而且不會(huì)遍歷 { done: true } 的這一次哦坪蚁!
// 所以,示例中 for...of 只會(huì)打印出 0 ~ 5镜沽,而不包括 6敏晤。
// 同理,執(zhí)行到 [...gen] 時(shí)缅茉,由于此前迭代器已經(jīng)是 done: true 結(jié)束狀態(tài)嘴脾,
// 因此解構(gòu)結(jié)果就是一個(gè)空數(shù)組:[]

此前的文章提到過,迭代器是一次性對(duì)象蔬墩,而且不應(yīng)該重用生成器译打。例如上面示例中,已經(jīng)使用 for...of 去遍歷完 gen 對(duì)象了拇颅,然后還使用解構(gòu)去遍歷 gen 對(duì)象扶平,由于解構(gòu)之前 gen 對(duì)象已結(jié)束,再去使用就沒意義了蔬蕊。

再看示例,你就明白了:

function* foo() {
  yield 1
  yield 2
  return 3
  yield 4
}

[...foo()] // [1, 2]
Array.from(foo()) // [1, 2]
const [x, y] = foo() // x 為 1, y 為 2
for (const x of foo()) { console.log(x) } // 依次打痈绻取:1岸夯、2

所以,無論是 for...of 或是解構(gòu)操作们妥,遇到狀態(tài) donetrue 就會(huì)中止猜扮,且不包含返回對(duì)象

for...of 本質(zhì)上就是一個(gè) while 循環(huán)监婶。

const arr = [1, 2, 3]
for (const x of arr) {
  console.log(x)
}

// 相當(dāng)于
const iter = arr[Symbol.iterator]() // 迭代器
let iterRes = iter.next() // IteratorResult
while (!iterRes.done) { // 當(dāng) done 為 true 時(shí)退出循環(huán)
  console.log(iterRes.value)
  iterRes = iter.next()
}

建議:同一個(gè)迭代器最好不要重復(fù)使用旅赢。

4. Generator.prototype.return()

此前的文章提到過,迭代器要提前退出惑惶,并“關(guān)閉”迭代器(即狀態(tài) done 變?yōu)?true)煮盼,需要實(shí)現(xiàn)迭代器協(xié)議的 return() 方法。

也提到過带污,生成器對(duì)象本身實(shí)現(xiàn)了 return() 方法僵控。因此,因應(yīng)不同場(chǎng)景鱼冀,使用 break报破、continue悠就、returnthrow 或數(shù)組解構(gòu)未消費(fèi)所有值時(shí)充易,都會(huì)提前關(guān)閉狀態(tài)梗脾。

function* foo() {
  yield 1
  yield 2
  console.log('here')
  yield 3
}

// 情況一:屬于未消費(fèi)所有值,也會(huì)提前關(guān)閉盹靴。其中 x 為 1, y 為 2炸茧。
const [x, y] = foo()

// 情況二:使用 break 提前退出,因此不會(huì)執(zhí)行到 console.log('here') 這條語(yǔ)句鹉究。
for (const x of foo()) {
  console.log(x)
  if (x === 2) break
}
// 依次打佑盍ⅰ:1、2

// 情況三:屬于從開始到結(jié)尾自赔,迭代完全
for (const x of foo()) {
  console.log(x)
}
// 依次打勇栲凇:1、2绍妨、"here"润脸、3

對(duì)于生成器對(duì)象,除了通過以上方式“提前關(guān)閉”之外他去,還提供了一個(gè) Generator.prototype.return() 方法供我們使用毙驯。

function* foo() {
  yield 1
  yield 2
  yield 3
}

const gen = foo()
gen.next() // { done: false, value: 1 }
gen.return('closed') // { done: true, value: 'closed' } // 若 return 不傳參時(shí),value 為 undefined灾测。
gen.next() // { done: true, value: undefined }

注意爆价,return() 方法的參數(shù)是可選的。當(dāng)傳遞某個(gè)參數(shù)時(shí)媳搪,它將作為 { done: true, value: '參數(shù)對(duì)應(yīng)的值' }铭段。若不傳參,那么 value 的值為 undefined秦爆。

但如果 Generator 函數(shù)體內(nèi)序愚,包含 try...finally 代碼塊,且正在執(zhí)行 try 代碼塊等限,那么 return() 方法會(huì)導(dǎo)致立即進(jìn)入 finally 代碼塊爸吮,執(zhí)行完以后,整個(gè)函數(shù)才會(huì)結(jié)束望门。

function* foo() {
  yield 1

  try {
    yield 2
    yield 3
  } finally {
    yield 4
    yield 5
  }

  yield 6
}

// ? 注意執(zhí)行順序及結(jié)果
const gen = foo()
gen.next()             // { done: false, value: 1 }
gen.next()             // { done: false, value: 2 }
gen.return('closed')   // { done: false, value: 4 }
gen.next()             // { done: false, value: 5 }
gen.next()             // { done: true, value: 'closed' }

上面代碼中形娇,調(diào)用 return() 方法后,就開始執(zhí)行 finally 代碼塊筹误,不執(zhí)行 try 里面剩下的代碼了埂软,然后等到 finally 代碼塊執(zhí)行完,再返回 return() 方法指定的返回值。

5. Generator.prototype.throw()

生成器對(duì)象都有一個(gè) throw() 方法(注意勘畔,它跟全局的 throw 關(guān)鍵字是兩回事)所灸,可以在函數(shù)體外拋出錯(cuò)誤,然后在 Generator 函數(shù)體內(nèi)捕獲炫七。

當(dāng)生成器未開始之前或者已結(jié)束(已關(guān)閉)之后爬立,調(diào)用生成器的 throw() 方法。它的錯(cuò)誤信息會(huì)被生成器函數(shù)外部的 try...catch 捕獲到万哪。若外部沒有 try...catch 語(yǔ)句侠驯,則會(huì)報(bào)錯(cuò)且代碼就會(huì)停止執(zhí)行。

  • 未開始奕巍,是指調(diào)用 Generator 函數(shù)返回生成器對(duì)象之后吟策,第一次就調(diào)用了 throw() 方法。此時(shí)由于 Generator 函數(shù)還沒開始執(zhí)行的止,throw() 方法拋出的錯(cuò)誤只能拋出到 Generator 函數(shù)外檩坚。

  • 已結(jié)束,是指生成器對(duì)象的狀態(tài)是 { done: true }诅福。此后再調(diào)用生成器對(duì)象 throw() 方法匾委,錯(cuò)誤只能在 Generator 函數(shù)外被捕獲。

以上兩種情況均不會(huì)被 Generator 函數(shù)內(nèi)部的 try...catch 捕獲到氓润。

看示例:

function* generatorFn() {
  try {
    yield
  } catch (e) {
    console.log('Generator Inner:', e)
  }
}

const gen = generatorFn()
gen.next()

try {
  console.log(gen.throw('a'))
  console.log(gen.throw('b'))
} catch (e) {
  console.log('Generator Outer:', e)
}

// 依次打印出:
// "Generator Inner: a"
// { value: undefined, done: true }
// "Generator Outer: b"

上面示例中赂乐,當(dāng)代碼執(zhí)行到 gen.throw('a') 時(shí)(此前已調(diào)用過一次 gen.next() 了),由于 Generator 函數(shù)體內(nèi)部署了 try...catch 語(yǔ)句塊咖气,因此在外部的 gen.throw('a') 會(huì)被內(nèi)部的 catch 捕獲到挨措,而且參數(shù) 'a' 將作為 catch 語(yǔ)句塊的參數(shù),所以打印出 'Generator Inner: a'崩溪。

請(qǐng)注意运嗜,當(dāng) throw() 方法被捕獲到之后,會(huì)“自動(dòng)”執(zhí)行下一條 yield 表達(dá)式悯舟,相當(dāng)于調(diào)用一次 next() 方法。由于 Generator 函數(shù)體內(nèi)在執(zhí)行 catch 之后砸民,已經(jīng)沒有其他語(yǔ)句抵怎,相當(dāng)于有一個(gè)隱式的 return undefined,即 gen 對(duì)象會(huì)變成 donetrue 而關(guān)閉岭参。所以 console.log(gen.throw('a')) 就會(huì)打印出 { value: undefined, done: true }反惕。

完了繼續(xù)執(zhí)行 gen.throw('b') 方法,由于 gen 已經(jīng)是“結(jié)束狀態(tài)”演侯,所以 throw() 方法拋出的錯(cuò)誤將會(huì)在 Generator 函數(shù)外部被捕獲到姿染。所以就是打印出:'Generator Outer: b'

怕有人還沒完全理解,再給出一個(gè)示例:

function* generatorFn() {
  try {
    yield 1
  } catch (e) {
    console.log('Generator Inner:', e)
  }
  yield 2
}

const gen = generatorFn()
console.log(gen.next())
console.log(gen.throw(new Error('Oops')))
console.log(gen.next())
// 依次打印出:
// { value: 1, done: false }
// "Generator Inner: Error: Oops"
// { value: 2, done: false }
// { value: undefined, done: true }

以上示例中悬赏,gen.throw() 之后狡汉,內(nèi)部會(huì)自動(dòng)執(zhí)行一次 next() 方法,即執(zhí)行到 yield 2闽颇,因此返回的 IteratorResult 對(duì)象為:{ value: 2, done: false }盾戴。接著再執(zhí)行一次 gen.next() 方法生成器就會(huì)變成關(guān)閉狀態(tài)。

這種函數(shù)體內(nèi)捕獲錯(cuò)誤的機(jī)制兵多,大大方便了對(duì)錯(cuò)誤的處理尖啡。多個(gè) yield 表達(dá)式,可以只用一個(gè) try...catch 代碼塊來捕獲錯(cuò)誤剩膘。如果使用回調(diào)函數(shù)的寫法衅斩,想要捕獲多個(gè)錯(cuò)誤,就不得不為每個(gè)函數(shù)內(nèi)部寫一個(gè)錯(cuò)誤處理語(yǔ)句怠褐,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次 try...catch 語(yǔ)句就可以了畏梆。

還有,當(dāng) Generator 函數(shù)內(nèi)報(bào)錯(cuò)惫搏,且未被捕獲具温,生成器就會(huì)變成“關(guān)閉”狀態(tài)。若后續(xù)再次調(diào)用此生成器的 next() 方法筐赔,只會(huì)返回 { done: true, value: undefined } 結(jié)果铣猩。

6. next、return茴丰、throw 的共同點(diǎn)

其實(shí) next()达皿、return()throw() 三個(gè)方法本質(zhì)上都是同一事件贿肩,可以放在一起理解峦椰。它們的作用都是讓 Generator 函數(shù)恢復(fù)執(zhí)行,兵器使用不同的語(yǔ)句替換 yield 表達(dá)式汰规。

const gen = function* (x, y) {
  const result = yield x + y
  return result
}(1, 2)

gen.next() // { done: false, value: 3 }

next() 方法是將 yield 表達(dá)式替換成一個(gè)值汤功。注意,首次調(diào)用 next() 方法進(jìn)行傳參是無效的溜哮,從第二次起才有效滔金。

gen.next(10) // { done: true, value: 10 }
// 如果第二次調(diào)用 next 方法,且不傳參時(shí)茂嗓,yield 表達(dá)式返回值為 undefined餐茵。因此,
// gen.next() // { done: true, value: undefined }

return() 方法是將 yield 表達(dá)式替換成一個(gè) return 語(yǔ)句

gen.return('closed') // { done: true, value: 'closed' }
// 這樣的話 `let result = yield x + y` 相當(dāng)于變成 `let result = return 'closed'`

throw() 方法是將 yield 表達(dá)式替換成一個(gè) throw 語(yǔ)句述吸,以主動(dòng)拋出錯(cuò)誤忿族。

gen.throw(new Error('exception')) // 報(bào)錯(cuò):Uncaught Error: exception
// 這樣的話 `let result = yield x + y` 相當(dāng)于變成 `let result = throw new Error('exception')`
7. yield* 表達(dá)式

如果在 Generator 函數(shù)內(nèi)部調(diào)用另外一個(gè) Generator 函數(shù),需要前者的函數(shù)體內(nèi)部“手動(dòng)”完成遍歷。

function* foo() {
  yield 'foo1'
  yield 'foo2'
  // return 'something'
  // 假設(shè)指定一個(gè) return 語(yǔ)句道批,
  // 使用 yield* foo() 迭代時(shí)將不會(huì)被迭代到错英,
  // 因此可以理解成 yield* 內(nèi)部執(zhí)行了一遍 for...of 循環(huán)。
  // 返回值 something屹徘,僅當(dāng) let result = yield* foo() 使用時(shí)走趋,作為 result 的結(jié)果。
}

function* bar() {
  yield 'bar1'
  for (let x of foo()) {
    console.log(x)
  }
  yield 'bar2'
}

for (let x of bar()) {
  console.log(x)
}
// 依次打印出:
// "foo1"
// "bar1"
// "bar2"
// "foo2"

上面示例中噪伊,foobar 都是Generator 函數(shù)簿煌,在 bar 內(nèi)部調(diào)用 foo,需要“手動(dòng)”迭代 foo 的生成器實(shí)例鉴吹。如果存在多個(gè) Generator 函數(shù)嵌套時(shí)姨伟,寫起來就會(huì)非常麻煩。

針對(duì)這種情況豆励,ES6 提供了 yield* 表達(dá)式夺荒,用于在一個(gè) Generator 函數(shù)里面執(zhí)行另外一個(gè) Generator 函數(shù)。

因此良蒸,上面的示例可以利用 yield* 改寫成:

function* foo() {
  yield 'foo1'
  yield 'foo2'
}

function* bar() {
  yield 'bar1'
  yield* foo()
  yield 'bar2'
}

for (let x of bar()) {
  console.log(x)
}

關(guān)于 yieldyield* 的區(qū)別:

  • yield 關(guān)鍵字后面技扼,可以跟著一個(gè)值或表達(dá)式,其結(jié)果將作為 next() 方法返回值的 value 屬性值嫩痰。

  • yield* 后面剿吻,只能跟著一個(gè)可迭代對(duì)象(即具有 Iterator 接口的任意對(duì)象),否則會(huì)報(bào)錯(cuò)串纺。生成器本身就是迭代器丽旅,也是可迭代對(duì)象耕拷。

因此篮昧,yield* 后面除了生成器對(duì)象,還可以是以下這些可迭代對(duì)象等等砚著。

function* foo() {
  yield 'foo1'
  yield* [1, 2] // 數(shù)組祷蝌、字符串均屬于可迭代對(duì)象
  yield [3, 4] // 未使用星號(hào)時(shí)茅撞,將會(huì)返回?cái)?shù)組
  yield 'foo2'
  yield* 'Hi'
  yield 'JSer' // 同理,未使用星號(hào)將會(huì)返回整個(gè)字符串
  // yield 100 // 若 yield* 后面跟一個(gè)不可迭代對(duì)象巨朦,將會(huì)報(bào)錯(cuò):TypeError: undefined is not a function
}

for (const x of foo()) {
  console.log(x)
}
// 依次打印出:"foo1"米丘、1、2罪郊、[3, 4]、"foo2"尚洽、"H"悔橄、"i"、"JSer"
8. Generator 函數(shù)中的 this

在普通函數(shù)中 this 指向當(dāng)前的執(zhí)行上下文環(huán)境,而箭頭函數(shù)則不存在 this癣疟,那么 Generator 函數(shù)中 this 是怎樣的呢挣柬?

function* foo() {}
const gen = foo()
foo.prototype.sayHi = function () { console.log('Hi~') }

console.log(gen instanceof foo) // true
gen.sayHi() // "Hi~"

上面的示例中,實(shí)例 gen 繼承了 foo.prototype睛挚。Generator 函數(shù)算是構(gòu)造函數(shù)邪蛔,但它是“特殊”的構(gòu)造函數(shù),它不返回 this 實(shí)例扎狱,而是生成器實(shí)例侧到。

function* foo() {
  this.a = 1
}
const gen = foo()
gen.next()
console.log(gen.a) // undefined

// 其實(shí)我們通過打印 this 可知,this 仍指向當(dāng)前執(zhí)行上下文環(huán)境淤击。
// 此處執(zhí)行上下文環(huán)境是全局匠抗,因此 this 是 window 對(duì)象。
// 如果執(zhí)行 gen.next() 時(shí)所處的上下文是某個(gè)對(duì)象(假設(shè)為 obj)污抬,
// 那么 this 就會(huì)指向 obj汞贸,而不是 gen 對(duì)象。

// 看著是不是有點(diǎn)像以下這個(gè):
// function Bar() {
//   this.a = 1
//   return {} // 不返回 this印机,返回另一個(gè)對(duì)象
// }
// const bar = new Bar()
// console.log(bar.a) // undefined

上面的示例中矢腻,當(dāng)我們調(diào)用 gen.next() 方法,會(huì)給 this.a 賦值為 1射赛,接著打印 gen.a 的結(jié)果卻是 undefined多柑,說明 this 并不是指向 gen 生成器實(shí)例。所以咒劲,Generator 函數(shù)跟平常的構(gòu)造函數(shù)是不一樣的顷蟆。

而且,不能使用 new 關(guān)鍵字進(jìn)行實(shí)例化腐魂,會(huì)報(bào)錯(cuò)帐偎。

const gen2 = new foo() // TypeError: foo is not a constructor
9. Generator 與上下文

JavaScript 代碼運(yùn)行時(shí),會(huì)產(chǎn)生一個(gè)全局的上下文環(huán)境(context蛔屹,又稱運(yùn)行環(huán)境)削樊,包含了當(dāng)前所有的變量和對(duì)象。然后兔毒,執(zhí)行函數(shù)(或塊級(jí)代碼)的時(shí)候漫贞,又會(huì)在當(dāng)前上下文環(huán)境的上層,產(chǎn)生一個(gè)函數(shù)運(yùn)行的上下文育叁,變成當(dāng)前(active)的上下文迅脐,由此形成一個(gè)上下文環(huán)境的堆棧(context stack)。

這個(gè)堆棧是“后進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu)豪嗽,最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成谴蔑,退出堆棧豌骏,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成隐锭,堆棧清空窃躲。

Generator 函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境钦睡,一旦遇到 yield 命令蒂窒,就會(huì)暫時(shí)退出堆棧,但是并不消失荞怒,里面的所有變量和對(duì)象會(huì)凍結(jié)在當(dāng)前狀態(tài)洒琢。等到對(duì)它執(zhí)行 next 命令時(shí),這個(gè)上下文環(huán)境又會(huì)重新加入調(diào)用棧挣输,凍結(jié)的變量和對(duì)象恢復(fù)執(zhí)行纬凤。

function* foo() {
  yield 1
  return 2
}

let gen = foo()

console.log(
  gen.next().value,
  gen.next().value
)

上面代碼中,第一次執(zhí)行 gen.next()時(shí)撩嚼,Generator 函數(shù) foo 的上下文會(huì)加入堆棧停士,即開始運(yùn)行 foo 內(nèi)部的代碼。等遇到 yield 1時(shí)完丽,foo 上下文退出堆棧恋技,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行 gen.next() 時(shí)逻族,foo 上下文重新加入堆棧蜻底,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行聘鳞。

四薄辅、Generator 的應(yīng)用

Generator 與 Promise 都是 ES6 通過的異步編程的解決方案。盡管 Promise 有效解決了 ES6 之前的“回調(diào)地獄”(Callback Hell)抠璃,但它仍然需要寫一堆的 then()catch() 的處理站楚。

如示例:

// 這里 delay 表示各種異步操作,比如網(wǎng)絡(luò)請(qǐng)求等等
// 一下子想不到要列舉哪些異步操作搏嗡,就用 setTimeout 表示吧
// 問題不大窿春,舉例而已
function delay(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

function requestByPromise(url) {
  let result = null
  window.fetch(url)
    .then(respone => respone.json())
    .then(res => {
      result = res
    })
    .then(() => {
      return delay(1000)
    })
    .then(() => {
      return delay(2000)
    })
    .then(() => {
      return delay(3000)
    })
    .then(() => {
      console.log('Done', result)
      // do something...
    })
    .catch(err => {
      console.warn('Exception', err)
    })
}

requestByPromise('/config/user')

上述示例中,當(dāng)我們存在多個(gè)異步操作采盒,想利用 Promise 封裝的話旧乞,避免不了要寫一系列的 then()catch() 方法,假設(shè) requestByPromise() 方法磅氨,進(jìn)行網(wǎng)絡(luò)請(qǐng)求之后尺栖,還有很多個(gè)異步操作要執(zhí)行,等它們完成之后烦租,這里封裝的 requestPromise(請(qǐng)求操作)才會(huì)完結(jié)延赌。整個(gè)代碼的實(shí)現(xiàn)起來代碼里還是很長(zhǎng)货徙。

雖然利用 async...await 可以寫出很簡(jiǎn)潔的結(jié)構(gòu),但是本文的主角不是它皮胡。

當(dāng)然,利用 Promise.all() 等方法也可以簡(jiǎn)化以上流程赏迟。如果利用 Generator 要怎么做呢屡贺?

如果我們想寫出如下這樣更直觀的“同步”方式:

function* requestByGenerator(url) {
  let response = yield window.fetch(url)
  let result = yield response.json()
  yield delay(1000)
  yield delay(2000)
  yield delay(3000)
  return result
}

如果像下面那樣,直接去(多次)調(diào)用 next() 方法锌杀,顯然不會(huì)得到我們預(yù)期結(jié)果甩栈,且會(huì)報(bào)錯(cuò)。

const gen = requestByGenerator('/config/user')
gen.next()
gen.next() // 這一步就會(huì)報(bào)錯(cuò)糕再,TypeError: Cannot read property 'json' of undefined
// ...

原因很簡(jiǎn)單量没,yield 表達(dá)式的返回值總是 undefined。如果 response 要得到預(yù)期值突想,在調(diào)用 gen.next() 方法時(shí)殴蹄,應(yīng)傳入 window.fetch(url) 的結(jié)果,在下一個(gè) yield 表達(dá)式才會(huì)正確解析猾担。而且還有一個(gè)最大的問題袭灯,由于實(shí)例化 gen 對(duì)象,以及調(diào)用 gen.next() 都是同步的绑嘹,當(dāng)我們?nèi)缟鲜鍪纠{(diào)用第二次 next() 方法時(shí)稽荧,F(xiàn)etch 請(qǐng)求還沒有得到結(jié)果。即使已經(jīng)請(qǐng)求到數(shù)據(jù)工腋,但由于 Event Loop 機(jī)制姨丈,它的處理也后于 next() 方法。

請(qǐng)注意擅腰,盡管 Generator 函數(shù)是異步編程的解決方案蟋恬,但它并不是異步的,而是同步的惕鼓。只是 Generator 函數(shù)在調(diào)用之后筋现,不會(huì)立即執(zhí)行函數(shù)體內(nèi)的代碼,而是提供了 next() 等方法箱歧,方便我們?nèi)タ刂飘惒搅鞒塘T了矾飞。

因此,像前一個(gè)示例的 requestByGenerator 函數(shù)呀邢,它并不會(huì)按編寫順序“同步”地處理這些異步操作洒沦,還需要我們進(jìn)一步去封裝,才能按照預(yù)期的“同步”執(zhí)行多個(gè)異步操作价淌。

Generator 還有一個(gè)很蛋疼的問題申眼,需要主動(dòng)調(diào)用 next() 才會(huì)去執(zhí)行 Generator 函數(shù)體內(nèi)的代碼瞒津。如果利用 for...of 等語(yǔ)句去遍歷,遇到 donetrue 的又不執(zhí)行括尸。

所以巷蚪,我們要做的就是實(shí)現(xiàn)一個(gè) Generator 執(zhí)行器。

/**
 * 思路:
 * 1. 封裝方法并返回一個(gè) Promise 對(duì)象濒翻;
 * 2. Promise 對(duì)象的返回值就是 Generator 函數(shù)的 return 結(jié)果屁柏;
 * 3. 封裝的方法內(nèi)部,要自動(dòng)調(diào)用生成器的 next() 方法有送,在生成器結(jié)束時(shí)淌喻,將結(jié)果返回 Promise 對(duì)象(fulfilled);
 * 4. 這里將 Generator 內(nèi)部的異常情況雀摘,在 Generator 外部使用 try...catch 補(bǔ)換裸删,并返回 Promise 對(duì)象(rejected);
 * 5. 針對(duì) Generator 函數(shù)內(nèi) yield 關(guān)鍵字后的異步操作阵赠,若非 Promise 的話涯塔,請(qǐng)使用 Promise 包裝一層;
 * 6. 由于封裝方法會(huì)自動(dòng)調(diào)用 next() 方法清蚀,在 Generator 函數(shù)內(nèi)若不是異步操作伤塌,沒必要使用 yield 關(guān)鍵字去創(chuàng)建一個(gè)狀態(tài),直接同步寫法即可轧铁。
 *
 * @param {GeneratorFunction} genFn 生成器函數(shù)
 * @param  {...any} args 傳遞給生成器函數(shù)的參數(shù)
 * @returns {Promise}
 */
function generatorExecutor(genFn, ...args) {
  const getType = obj => {
    const type = Object.prototype.toString.call(obj)
    return /^\[object (.*)\]$/.exec(type)[1]
  }

  if (getType(genFn) !== 'GeneratorFunction') {
    throw new TypeError('The first parameter of generatorExecutor must be a generator function!')
  }

  // 下面就是不斷調(diào)用 next() 方法的過程每聪,直至結(jié)束或報(bào)錯(cuò)
  return new Promise((resolve, reject) => {
    const gen = genFn(...args)
    let iterRes = gen.next()

    const goNext = iteratorResult => {
      const { done, value } = iteratorResult

      // Generator 結(jié)束時(shí)退出
      if (done) return resolve(value)

      if (getType(value) !== 'Promise') {
        const nextRes = gen.next(value)
        goNext(nextRes)
        return
      }

      // 處理 yield 為 Promise 的情況
      value.then(res => {
        const nextRes = gen.next(res)
        goNext(nextRes)
      }).catch(err => {
        try {
          // 利用 Generator.prototype.throw() 拋出異常,同時(shí)使得 gen 結(jié)束
          gen.throw(err)
        } catch (e) {
          reject(e)
        }
      })
    }

    goNext(iterRes)
  })
}

然后齿风,像下面那樣去調(diào)用即可药薯。

function* requestByGenerator(url) {
  let response = yield window.fetch(url)
  let result = yield response.json()
  yield delay(1000)
  yield delay(2000)
  yield delay(3000)
  return result
}

generatorExecutor(requestByGenerator, '/config/user')
  .then(res => {
    // do something...
    // res 將會(huì)預(yù)期地得到 fetch 的響應(yīng)結(jié)果
  })
  .catch(err => {
    // do something...
    // 處理異常情況
  })

盡管 Generator 函數(shù)提出了一種全新的異步編程的解決方案,可以在函數(shù)外部注入值取干預(yù)函數(shù)內(nèi)部的行為救斑,這種思想提供了極大的創(chuàng)造性童本,強(qiáng)大之處不是 Promise 能比的。但是在結(jié)合實(shí)際場(chǎng)景時(shí)脸候,很大可能需要自實(shí)現(xiàn)一個(gè) Generator 執(zhí)行器穷娱,使其自動(dòng)執(zhí)行生成器。

例如运沦,著名的 co 函數(shù)庫(kù)就是去做了這件事情泵额。如果想了解,可以看一下這篇文章携添,或直接看官方文檔嫁盲。但看了下 GitHub 上最新一次提交已經(jīng)是 5 年前,大概都去用 async/await 了吧烈掠。

接下來就介紹 async/await 了羞秤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缸托,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瘾蛋,更是在濱河造成了極大的恐慌俐镐,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哺哼,死亡現(xiàn)場(chǎng)離奇詭異京革,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)幸斥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咬扇,“玉大人甲葬,你說我怎么就攤上這事⌒负兀” “怎么了经窖?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)梭灿。 經(jīng)常有香客問我画侣,道長(zhǎng),這世上最難降的妖魔是什么堡妒? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任配乱,我火速辦了婚禮,結(jié)果婚禮上皮迟,老公的妹妹穿的比我還像新娘搬泥。我一直安慰自己,他們只是感情好伏尼,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布忿檩。 她就那樣靜靜地躺著,像睡著了一般爆阶。 火紅的嫁衣襯著肌膚如雪燥透。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天辨图,我揣著相機(jī)與錄音班套,去河邊找鬼。 笑死故河,一個(gè)胖子當(dāng)著我的面吹牛孽尽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忧勿,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼杉女,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瞻讽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起熏挎,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤速勇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后坎拐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烦磁,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年哼勇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了都伪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡积担,死狀恐怖陨晶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帝璧,我是刑警寧澤先誉,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站的烁,受9級(jí)特大地震影響褐耳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渴庆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一铃芦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧襟雷,春花似錦杨帽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至叙赚,卻和暖如春老客,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背震叮。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工胧砰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苇瓣。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓尉间,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哲嘲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容