JavaScript設(shè)計(jì)模式之狀態(tài)模式

狀態(tài)模式是一種非同尋常的優(yōu)秀模式,它也許是解決某些需求場(chǎng)景的最好方法柳刮。雖然狀態(tài)模式并不是一種簡(jiǎn)單到一目了然的模式(它往往還會(huì)帶來代碼量的增加)挖垛,但你一旦明白了狀態(tài)模式的精髓,以后一定會(huì)感謝它帶給你的無與倫比的好處秉颗。

狀態(tài)模式的關(guān)鍵是區(qū)分事物內(nèi)部的狀態(tài)痢毒,事物內(nèi)部狀態(tài)的改變往往會(huì)帶來事物的行為改變。

初識(shí)狀態(tài)模式

我們來想象這樣一個(gè)場(chǎng)景:有一個(gè)電燈蚕甥,電燈上面只有一個(gè)開關(guān)哪替。當(dāng)電燈開著的時(shí)候,此時(shí)按下開關(guān)菇怀,電燈會(huì)切換到關(guān)閉狀態(tài)夷家;再按一次開關(guān),電燈又將被打開敏释。同一個(gè)開關(guān)按鈕库快,在不同的狀態(tài)下,表現(xiàn)出來的行為是不一樣的钥顽。

現(xiàn)在用代碼來描述這個(gè)場(chǎng)景义屏,首先定義一個(gè)Light類,可以預(yù)見蜂大,電燈對(duì)象light將從Light類創(chuàng)建而出闽铐, light對(duì)象將擁有兩個(gè)屬性,我們用state來記錄電燈當(dāng)前的狀態(tài)奶浦,用button表示具體的開關(guān)按鈕兄墅。下面來編寫這個(gè)電燈程序的例子。

1.第一個(gè)例子:電燈程序

首先給出不用狀態(tài)模式的電燈程序?qū)崿F(xiàn):

var Light = function(){
    this.state = 'off';    // 給電燈設(shè)置初始狀態(tài)off
    this.button = null;    // 電燈開關(guān)按鈕
};

接下來定義Light.prototype.init方法澳叉,該方法負(fù)責(zé)在頁(yè)面中創(chuàng)建一個(gè)真實(shí)的button節(jié)點(diǎn)隙咸,假設(shè)這個(gè)button就是電燈的開關(guān)按鈕沐悦,當(dāng)button的onclick事件被觸發(fā)時(shí),就是電燈開關(guān)被按下的時(shí)候五督,代碼如下:

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
        self = this;

    button.innerHTML = '開關(guān)';
    this.button = document.body.appendChild( button );
    this.button.onclick = function(){
        self.buttonWasPressed();
    }
};

當(dāng)開關(guān)被按下時(shí)藏否,程序會(huì)調(diào)用self.buttonWasPressed方法, 開關(guān)按下之后的所有行為充包,都將被封裝在這個(gè)方法里副签,代碼如下:

Light.prototype.buttonWasPressed = function(){
    if ( this.state === 'off' ){
        console.log( '開燈' );
        this.state = 'on';
    }else if ( this.state === 'on' ){
        console.log( '關(guān)燈' );
        this.state = 'off';
    }
};

var light = new Light();
light.init();

OK,現(xiàn)在可以看到基矮,我們已經(jīng)編寫了一個(gè)強(qiáng)壯的狀態(tài)機(jī)淆储,這個(gè)狀態(tài)機(jī)的邏輯既簡(jiǎn)單又縝密,看起來這段代碼設(shè)計(jì)得無懈可擊家浇,這個(gè)程序沒有任何bug遏考。實(shí)際上這種代碼我們已經(jīng)編寫過無數(shù)遍,比如要交替切換一個(gè)button的class蓝谨,跟此例一樣灌具,往往先用一個(gè)變量state來記錄按鈕的當(dāng)前狀態(tài),在事件發(fā)生時(shí)譬巫,再根據(jù)這個(gè)狀態(tài)來決定下一步的行為咖楣。

令人遺憾的是,這個(gè)世界上的電燈并非只有一種芦昔。許多酒店里有另外一種電燈诱贿,這種電燈也只有一個(gè)開關(guān),但它的表現(xiàn)是:第一次按下打開弱光咕缎,第二次按下打開強(qiáng)光珠十,第三次才是關(guān)閉電燈。現(xiàn)在必須改造上面的代碼來完成這種新型電燈的制造:

Light.prototype.buttonWasPressed = function(){
    if ( this.state === 'off' ){
        console.log( '弱光' );
        this.state = 'weakLight';
    }else if ( this.state === 'weakLight' ){
        console.log( '強(qiáng)光' );
        this.state = 'strongLight';
    }else if ( this.state === 'strongLight' ){
        console.log( '關(guān)燈' );
        this.state = 'off';
    }
};

現(xiàn)在這個(gè)反例先告一段落凭豪,我們來考慮一下上述程序的缺點(diǎn)焙蹭。

  • 很明顯buttonWasPressed方法是違反開放-封閉原則的,每次新增或者修改light的狀態(tài)嫂伞,都需要改動(dòng)buttonWasPressed方法中的代碼孔厉,這使得buttonWasPressed成為了一個(gè)非常不穩(wěn)定的方法。

  • 所有跟狀態(tài)有關(guān)的行為帖努,都被封裝在buttonWasPressed方法里撰豺,如果以后這個(gè)電燈又增加了強(qiáng)強(qiáng)光、超強(qiáng)光和終極強(qiáng)光拼余,那我們將無法預(yù)計(jì)這個(gè)方法將膨脹到什么地步污桦。當(dāng)然為了簡(jiǎn)化示例,此處在狀態(tài)發(fā)生改變的時(shí)候匙监,只是簡(jiǎn)單地打印一條log和改變button的innerHTML凡橱。在實(shí)際開發(fā)中小作,要處理的事情可能比這多得多,也就是說梭纹,buttonWasPressed方法要比現(xiàn)在龐大得多躲惰。

  • 狀態(tài)的切換非常不明顯致份,僅僅表現(xiàn)為對(duì)state變量賦值变抽,比如this.state = 'weakLight'。在實(shí)際開發(fā)中氮块,這樣的操作很容易被程序員不小心漏掉绍载。我們也沒有辦法一目了然地明白電燈一共有多少種狀態(tài),除非耐心地讀完buttonWasPressed方法里的所有代碼滔蝉。當(dāng)狀態(tài)的種類多起來的時(shí)候击儡,某一次切換的過程就好像被埋藏在一個(gè)巨大方法的某個(gè)陰暗角落里。

  • 狀態(tài)之間的切換關(guān)系蝠引,不過是往buttonWasPressed方法里堆砌if阳谍、else語(yǔ)句,增加或者修改一個(gè)狀態(tài)可能需要改變?nèi)舾蓚€(gè)操作螃概,這使buttonWasPressed更加難以閱讀和維護(hù)矫夯。

2.狀態(tài)模式改進(jìn)電燈程序

現(xiàn)在我們學(xué)習(xí)使用狀態(tài)模式改進(jìn)電燈的程序。有意思的是吊洼,通常我們談到封裝训貌,一般都會(huì)優(yōu)先封裝對(duì)象的行為,而不是對(duì)象的狀態(tài)冒窍。但在狀態(tài)模式中剛好相反递沪,狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨(dú)的類,跟此種狀態(tài)有關(guān)的行為都被封裝在這個(gè)類的內(nèi)部综液,所以button被按下的的時(shí)候款慨,只需要在上下文中,把這個(gè)請(qǐng)求委托給當(dāng)前的狀態(tài)對(duì)象即可谬莹,該狀態(tài)對(duì)象會(huì)負(fù)責(zé)渲染它自身的行為樱调,如圖16-1所示。

image

同時(shí)我們還可以把狀態(tài)的切換規(guī)則事先分布在狀態(tài)類中届良, 這樣就有效地消除了原本存在的大量條件分支語(yǔ)句笆凌,如圖16-2所示。

image

下面進(jìn)入狀態(tài)模式的代碼編寫階段士葫,首先將定義3個(gè)狀態(tài)類乞而,分別是OffLightState、WeakLightState慢显、StrongLightState爪模。這3個(gè)類都有一個(gè)原型方法buttonWasPressed欠啤,代表在各自狀態(tài)下,按鈕被按下時(shí)將發(fā)生的行為屋灌,代碼如下:

// OffLightState:

var OffLightState = function( light ){
    this.light = light;
};

OffLightState.prototype.buttonWasPressed = function(){
    console.log( '弱光' );    // offLightState對(duì)應(yīng)的行為
    this.light.setState( this.light.weakLightState );    // 切換狀態(tài)到weakLightState
};

// WeakLightState:

var WeakLightState = function( light ){
    this.light = light;
};

WeakLightState.prototype.buttonWasPressed = function(){
    console.log( '強(qiáng)光' );    // weakLightState對(duì)應(yīng)的行為
    this.light.setState( this.light.strongLightState );    // 切換狀態(tài)到strongLightState
};

// StrongLightState:

var StrongLightState = function( light ){
    this.light = light;
};

StrongLightState.prototype.buttonWasPressed = function(){
    console.log( '關(guān)燈' );    // strongLightState對(duì)應(yīng)的行為
    this.light.setState( this.light.offLightState );    // 切換狀態(tài)到offLightState
};

接下來改寫Light類洁段,現(xiàn)在不再使用一個(gè)字符串來記錄當(dāng)前的狀態(tài),而是使用更加立體化的狀態(tài)對(duì)象共郭。我們?cè)贚ight類的構(gòu)造函數(shù)里為每個(gè)狀態(tài)類都創(chuàng)建一個(gè)狀態(tài)對(duì)象祠丝,這樣一來我們可以很明顯地看到電燈一共有多少種狀態(tài),代碼如下:

var Light = function(){
    this.offLightState = new OffLightState( this );
    this.weakLightState = new WeakLightState( this );
    this.strongLightState = new StrongLightState( this );
    this.button = null;
};

在button按鈕被按下的事件里除嘹,Context也不再直接進(jìn)行任何實(shí)質(zhì)性的操作写半,而是通過self.currState.buttonWasPressed()將請(qǐng)求委托給當(dāng)前持有的狀態(tài)對(duì)象去執(zhí)行,代碼如下:

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
        self = this;


    this.button = document.body.appendChild( button );
    this.button.innerHTML = '開關(guān)';

    this.currState = this.offLightState;    // 設(shè)置當(dāng)前狀態(tài)

    this.button.onclick = function(){
        self.currState.buttonWasPressed();
    }
};

最后還要提供一個(gè)Light.prototype.setState方法尉咕,狀態(tài)對(duì)象可以通過這個(gè)方法來切換light對(duì)象的狀態(tài)叠蝇。前面已經(jīng)說過,狀態(tài)的切換規(guī)律事先被完好定義在各個(gè)狀態(tài)類中年缎。在Context中再也找不到任何一個(gè)跟狀態(tài)切換相關(guān)的條件分支語(yǔ)句:

Light.prototype.setState = function( newState ){
    this.currState = newState;
};

現(xiàn)在可以進(jìn)行一些測(cè)試:

var light = new Light();
light.init();

不出意外的話悔捶,執(zhí)行結(jié)果跟之前的代碼一致,但是使用狀態(tài)模式的好處很明顯单芜,它可以使每一種狀態(tài)和它對(duì)應(yīng)的行為之間的關(guān)系局部化蜕该,這些行為被分散和封裝在各自對(duì)應(yīng)的狀態(tài)類之中,便于閱讀和管理代碼缓溅。

另外蛇损,狀態(tài)之間的切換都被分布在狀態(tài)類內(nèi)部,這使得我們無需編寫過多的if坛怪、else條件分支語(yǔ)言來控制狀態(tài)之間的轉(zhuǎn)換淤齐。

當(dāng)我們需要為light對(duì)象增加一種新的狀態(tài)時(shí),只需要增加一個(gè)新的狀態(tài)類袜匿,再稍稍改變一些現(xiàn)有的代碼即可更啄。假設(shè)現(xiàn)在light對(duì)象多了一種超強(qiáng)光的狀態(tài),那就先增加SuperStrongLightState類:

var SuperStrongLightState = function( light ){
    this.light = light;
};

SuperStrongLightState.prototype.buttonWasPressed = function(){
    console.log( '關(guān)燈' );
    this.light.setState( this.light.offLightState );
};

然后在Light構(gòu)造函數(shù)里新增一個(gè)superStrongLightState對(duì)象:

var Light = function(){
    this.offLightState = new OffLightState( this );
    this.weakLightState = new WeakLightState( this );
    this.strongLightState = new StrongLightState( this );
    this.superStrongLightState = new SuperStrongLightState( this );  // 新增superStrongLightState對(duì)象

    this.button = null;
};

最后改變狀態(tài)類之間的切換規(guī)則居灯,從StrongLightState---->OffLightState變?yōu)镾trongLightState---->SuperStrongLightState---->OffLightState:

StrongLightState.prototype.buttonWasPressed = function(){
    console.log( '超強(qiáng)光' );    // strongLightState 對(duì)應(yīng)的行為
    this.light.setState( this.light.superStrongLightState );    // 切換狀態(tài)到 superStrongLightState
};

狀態(tài)模式的定義

通過電燈的例子祭务,相信我們對(duì)于狀態(tài)模式已經(jīng)有了一定程度的了解。現(xiàn)在回頭來看GoF中對(duì)狀態(tài)模式的定義:

允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)改變時(shí)改變它的行為怪嫌,對(duì)象看起來似乎修改了它的類义锥。

我們以逗號(hào)分割,把這句話分為兩部分來看岩灭。第一部分的意思是將狀態(tài)封裝成獨(dú)立的類拌倍,并將請(qǐng)求委托給當(dāng)前的狀態(tài)對(duì)象,當(dāng)對(duì)象的內(nèi)部狀態(tài)改變時(shí),會(huì)帶來不同的行為變化柱恤。電燈的例子足以說明這一點(diǎn)数初,在off和on這兩種不同的狀態(tài)下,我們點(diǎn)擊同一個(gè)按鈕梗顺,得到的行為反饋是截然不同的泡孩。

第二部分是從客戶的角度來看,我們使用的對(duì)象寺谤,在不同的狀態(tài)下具有截然不同的行為仑鸥,這個(gè)對(duì)象看起來是從不同的類中實(shí)例化而來的,實(shí)際上這是使用了委托的效果矗漾。

狀態(tài)模式的通用結(jié)構(gòu)

在前面的電燈例子中锈候,我們完成了一個(gè)狀態(tài)模式程序的編寫薄料。首先定義了Light類敞贡,Light類在這里也被稱為上下文(Context)。隨后在Light的構(gòu)造函數(shù)中摄职,我們要?jiǎng)?chuàng)建每一個(gè)狀態(tài)類的實(shí)例對(duì)象誊役,Context將持有這些狀態(tài)對(duì)象的引用,以便把請(qǐng)求委托給狀態(tài)對(duì)象谷市。用戶的請(qǐng)求蛔垢,即點(diǎn)擊button的動(dòng)作也是實(shí)現(xiàn)在Context中的,代碼如下:

var Light = function(){
    this.offLightState = new OffLightState( this );    // 持有狀態(tài)對(duì)象的引用
    this.weakLightState = new WeakLightState( this );
    this.strongLightState = new StrongLightState( this );
    this.superStrongLightState = new SuperStrongLightState( this );
    this.button = null;
};

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
        self = this;


    this.button = document.body.appendChild( button );
    this.button.innerHTML = '開關(guān)';
    this.currState = this.offLightState;    // 設(shè)置默認(rèn)初始狀態(tài)

    this.button.onclick = function(){     // 定義用戶的請(qǐng)求動(dòng)作
        self.currState.buttonWasPressed();
    }
};

接下來可能是個(gè)苦力活迫悠,我們要編寫各種狀態(tài)類鹏漆,light對(duì)象被傳入狀態(tài)類的構(gòu)造函數(shù),狀態(tài)對(duì)象也需要持有l(wèi)ight對(duì)象的引用创泄,以便調(diào)用light中的方法或者直接操作light對(duì)象:

var OffLightState = function( light ){
    this.light = light;
};

OffLightState.prototype.buttonWasPressed = function(){
    console.log( '弱光' );
    this.light.setState( this.light.weakLightState );
};

缺少抽象類的變通方式

我們看到艺玲,在狀態(tài)類中將定義一些共同的行為方法,Context最終會(huì)將請(qǐng)求委托給狀態(tài)對(duì)象的這些方法鞠抑,在這個(gè)例子里饭聚,這個(gè)方法就是buttonWasPressed。無論增加了多少種狀態(tài)類搁拙,它們都必須實(shí)現(xiàn)buttonWasPressed方法秒梳。

在Java中,所有的狀態(tài)類必須繼承自一個(gè)State抽象父類箕速,當(dāng)然如果沒有共同的功能值得放入抽象父類中酪碘,也可以選擇實(shí)現(xiàn)State接口。這樣做的原因一方面是我們?cè)啻翁徇^的向上轉(zhuǎn)型盐茎,另一方面是保證所有的狀態(tài)子類都實(shí)現(xiàn)了buttonWasPressed方法兴垦。遺憾的是,JavaScript既不支持抽象類庭呜,也沒有接口的概念滑进。所以在使用狀態(tài)模式的時(shí)候要格外小心犀忱,如果我們編寫一個(gè)狀態(tài)子類時(shí),忘記了給這個(gè)狀態(tài)子類實(shí)現(xiàn)buttonWasPressed方法扶关,則會(huì)在狀態(tài)切換的時(shí)候拋出異常阴汇。因?yàn)镃ontext總是把請(qǐng)求委托給狀態(tài)對(duì)象的buttonWasPressed方法。

不論怎樣嚴(yán)格要求程序員节槐,也許都避免不了犯錯(cuò)的那一天搀庶,畢竟如果沒有編譯器的幫助,只依靠程序員的自覺以及一點(diǎn)好運(yùn)氣铜异,是不靠譜的哥倔。這里建議的解決方案跟《模板方法模式》中一致,讓抽象父類的抽象方法直接拋出一個(gè)異常揍庄,這個(gè)異常至少會(huì)在程序運(yùn)行期間就被發(fā)現(xiàn):

var State = function(){};

State.prototype.buttonWasPressed = function(){
    throw new Error( '父類的buttonWasPressed方法必須被重寫' );
};

var SuperStrongLightState = function( light ){
    this.light = light;
};

SuperStrongLightState.prototype = new State();   // 繼承抽象父類

SuperStrongLightState.prototype.buttonWasPressed = function(){    // 重寫buttonWasPressed方法
    console.log( '關(guān)燈' );
    this.light.setState( this.light.offLightState );
};

另一個(gè)狀態(tài)模式示例——文件上傳

接下來我們要討論一個(gè)復(fù)雜一點(diǎn)的例子咆蒿,這原本是一個(gè)真實(shí)的項(xiàng)目, 是關(guān)于微云上傳模塊蚂子。實(shí)際上沃测,不論是文件上傳,還是音樂食茎、視頻播放器蒂破,都可以找到一些明顯的狀態(tài)區(qū)分。比如文件上傳程序中有掃描别渔、正在上傳附迷、暫停、上傳成功哎媚、上傳失敗這幾種狀態(tài)喇伯,音樂播放器可以分為加載中、正在播放抄伍、暫停艘刚、播放完畢這幾種狀態(tài)。點(diǎn)擊同一個(gè)按鈕截珍,在上傳中和暫停狀態(tài)下的行為表現(xiàn)是不一樣的攀甚,同時(shí)它們的樣式class也不同。下面我們以文件上傳為例進(jìn)行說明岗喉。上傳中秋度,點(diǎn)擊按鈕暫停,如圖16-3所示钱床。

image

暫停中荚斯,點(diǎn)擊按鈕繼續(xù)播放,如圖16-4所示。

[圖片上傳失敗...(image-f9f3f9-1549107814846)]

看到這里事期,再聯(lián)系一下電燈的例子和之前對(duì)狀態(tài)模式的了解滥壕,我們已經(jīng)找了使用狀態(tài)模式的理由。

1.更復(fù)雜的切換條件

相對(duì)于電燈的例子兽泣,文件上傳不同的地方在于绎橘,現(xiàn)在我們將面臨更加復(fù)雜的條件切換關(guān)系。在電燈的例子中唠倦,電燈的狀態(tài)總是從關(guān)到開再到關(guān)称鳞,或者從關(guān)到弱光、弱光到強(qiáng)光稠鼻、強(qiáng)光再到關(guān)冈止。看起來總是循規(guī)蹈矩的A→B→C→A候齿,所以即使不使用狀態(tài)模式來編寫電燈的程序熙暴,而是使用原始的if、else來控制狀態(tài)切換毛肋,我們也不至于在邏輯編寫中迷失自己怨咪,因?yàn)闋顟B(tài)的切換總是遵循一些簡(jiǎn)單的規(guī)律屋剑,代碼如下:

if ( this.state === 'off' ){
    console.log( '開弱光' );
    this.button.innerHTML = '下一次按我是強(qiáng)光';
    this.state = 'weakLight';
}else if ( this.state === 'weakLight' ){
    console.log( '開強(qiáng)光' );
    this.button.innerHTML = '下一次按我是關(guān)燈';
    this.state = 'strongLight';
}else if ( this.state === 'strongLight' ){
    console.log( '關(guān)燈' );
    this.button.innerHTML = '下一次按我是弱光';
    this.state = 'off';
}

而文件上傳的狀態(tài)切換相比要復(fù)雜得多润匙,控制文件上傳的流程需要兩個(gè)節(jié)點(diǎn)按鈕,第一個(gè)用于暫停和繼續(xù)上傳唉匾,第二個(gè)用于刪除文件孕讳,如圖16-5所示。

image

現(xiàn)在看看文件在不同的狀態(tài)下巍膘,點(diǎn)擊這兩個(gè)按鈕將分別發(fā)生什么行為厂财。

  • 文件在掃描狀態(tài)中,是不能進(jìn)行任何操作的峡懈,既不能暫停也不能刪除文件璃饱,只能等待掃描完成。掃描完成之后肪康,根據(jù)文件的md5值判斷荚恶,若確認(rèn)該文件已經(jīng)存在于服務(wù)器,則直接跳到上傳完成狀態(tài)磷支。如果該文件的大小超過允許上傳的最大值谒撼,或者該文件已經(jīng)損壞,則跳往上傳失敗狀態(tài)雾狈。剩下的情況下才進(jìn)入上傳中狀態(tài)廓潜。

  • 上傳過程中可以點(diǎn)擊暫停按鈕來暫停上傳,暫停后點(diǎn)擊同一個(gè)按鈕會(huì)繼續(xù)上傳。

  • 掃描和上傳過程中辩蛋,點(diǎn)擊刪除按鈕無效呻畸,只有在暫停、上傳完成悼院、上傳失敗之后擂错,才能刪除文件。

2.一些準(zhǔn)備工作

微云提供了一些瀏覽器插件來幫助完成文件上傳樱蛤。插件類型根據(jù)瀏覽器的不同钮呀,有可能是ActiveObject,也有可能是WebkitPlugin昨凡。

上傳是一個(gè)異步的過程爽醋,所以控件會(huì)不停地調(diào)用JavaScript提供的一個(gè)全局函數(shù)window.external.upload,來通知JavaScript目前的上傳進(jìn)度便脊,控件會(huì)把當(dāng)前的文件狀態(tài)作為參數(shù)state塞進(jìn)window.external.upload蚂四。在這里無法提供一個(gè)完整的上傳插件,我們將簡(jiǎn)單地用setTimeout來模擬文件的上傳進(jìn)度哪痰,window.external.upload函數(shù)在此例中也只負(fù)責(zé)打印一些log:

window.external.upload = function( state ){
    console.log( state );     // 可能為sign遂赠、uploading、done晌杰、error
};

另外我們需要在頁(yè)面中放置一個(gè)用于上傳的插件對(duì)象:

var plugin = (function(){
    var plugin = document.createElement( 'embed' );
    plugin.style.display = 'none';

    plugin.type = 'application/txftn-webkit';

    plugin.sign = function(){
        console.log( '開始文件掃描' );
    }
    plugin.pause = function(){
        console.log( '暫停文件上傳' );
    };

    plugin.uploading = function(){
        console.log( '開始文件上傳' );
    };

    plugin.del = function(){
        console.log( '刪除文件上傳' );
    }

    plugin.done = function(){
        console.log( '文件上傳完成' );
    }

    document.body.appendChild( plugin );

    return plugin;
})();

3.開始編寫代碼

接下來開始完成其他代碼的編寫跷睦,先定義Upload類,控制上傳過程的對(duì)象將從Upload類中創(chuàng)建而來:

var Upload = function( fileName ){
    this.plugin = plugin;
    this.fileName = fileName;
    this.button1 = null;
    this.button2 = null;
    this.state = 'sign';    // 設(shè)置初始狀態(tài)為waiting
};

Upload.prototype.init方法會(huì)進(jìn)行一些初始化工作肋演,包括創(chuàng)建頁(yè)面中的一些節(jié)點(diǎn)抑诸。在這些節(jié)點(diǎn)里,起主要作用的是兩個(gè)用于控制上傳流程的按鈕爹殊,第一個(gè)按鈕用于暫停和繼續(xù)上傳蜕乡,第二個(gè)用于刪除文件:

Upload.prototype.init = function(){
    var that = this;
    this.dom = document.createElement( 'div' );
    this.dom.innerHTML =
        '<span>文件名稱:'+ this.fileName +'</span>\
        <button data-action="button1">掃描中</button>\
        <button data-action="button2">刪除</button>';

    document.body.appendChild( this.dom );
    this.button1 = this.dom.querySelector( '[data-action="button1"]' );    // 第一個(gè)按鈕
    this.button2 = this.dom.querySelector( '[data-action="button2"]' );    // 第二個(gè)按鈕
    this.bindEvent();
};

接下來需要給兩個(gè)按鈕分別綁定點(diǎn)擊事件:

Upload.prototype.bindEvent = function(){
    var self = this;
    this.button1.onclick = function(){
        if ( self.state === 'sign' ){    // 掃描狀態(tài)下,任何操作無效
            console.log( '掃描中梗夸,點(diǎn)擊無效...' );
        }else if ( self.state === 'uploading' ){     // 上傳中层玲,點(diǎn)擊切換到暫停
            self.changeState( 'pause' );
        }else if ( self.state === 'pause' ){    // 暫停中,點(diǎn)擊切換到上傳中
            self.changeState( 'uploading' );
        }else if ( self.state === 'done' ){
            console.log( '文件已完成上傳, 點(diǎn)擊無效' );
        }else if ( self.state === 'error' ){
            console.log( '文件上傳失敗, 點(diǎn)擊無效' );
        }
    };

    this.button2.onclick = function(){
        if ( self.state === 'done' || self.state === 'error'
                 || self.state === 'pause' ){
            // 上傳完成反症、上傳失敗和暫停狀態(tài)下可以刪除
            self.changeState( 'del' );
        }else if ( self.state === 'sign' ){
            console.log( '文件正在掃描中辛块,不能刪除' );
        }else if ( self.state === 'uploading' ){
            console.log( '文件正在上傳中,不能刪除' );
        }
    };

};

再接下來是Upload.prototype.changeState方法惰帽,它負(fù)責(zé)切換狀態(tài)之后的具體行為憨降,包括改變按鈕的innerHTML,以及調(diào)用插件開始一些“真正”的操作:

Upload.prototype.changeState = function( state ){

    switch( state ){
        case 'sign':
            this.plugin.sign();
            this.button1.innerHTML = '掃描中该酗,任何操作無效';
            break;
        case 'uploading':
            this.plugin.uploading();
            this.button1.innerHTML = '正在上傳授药,點(diǎn)擊暫停';
            break;
        case 'pause':
            this.plugin.pause();
            this.button1.innerHTML = '已暫停士嚎,點(diǎn)擊繼續(xù)上傳';
            break;
        case 'done':
            this.plugin.done();
            this.button1.innerHTML = '上傳完成';
            break;
        case 'error':
            this.button1.innerHTML = '上傳失敗';
            break;
        case 'del':
            this.plugin.del();
            this.dom.parentNode.removeChild( this.dom );
            console.log( '刪除完成' );
            break;
    }

    this.state = state;
};

最后我們來進(jìn)行一些測(cè)試工作:

var uploadObj = new Upload( 'JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐' );

uploadObj.init();

window.external.upload = function( state ){    // 插件調(diào)用JavaScript的方法
    uploadObj.changeState( state );
};

window.external.upload( 'sign' );     // 文件開始掃描

setTimeout(function(){
    window.external.upload( 'uploading' );    // 1秒后開始上傳
}, 1000 );

setTimeout(function(){
    window.external.upload( 'done' );    // 5秒后上傳完成
}, 5000 );

至此就完成了一個(gè)簡(jiǎn)單的文件上傳程序的編寫。當(dāng)然這仍然是一個(gè)反例悔叽,這里的缺點(diǎn)跟電燈例子中的第一段代碼一樣莱衩,程序中充斥著if、else條件分支娇澎,狀態(tài)和行為都被耦合在一個(gè)巨大的方法里笨蚁,我們很難修改和擴(kuò)展這個(gè)狀態(tài)機(jī)。文件狀態(tài)之間的聯(lián)系如此復(fù)雜,這個(gè)問題顯得更加嚴(yán)重了。

4.狀態(tài)模式重構(gòu)文件上傳程序

狀態(tài)模式在文件上傳的程序中骏庸,是最優(yōu)雅的解決辦法之一。通過電燈的例子奋单,我們已經(jīng)熟知狀態(tài)模式的結(jié)構(gòu)了,下面就開始一步步地重構(gòu)它猫十。

第一步仍然是提供window.external.upload函數(shù)览濒,在頁(yè)面中模擬創(chuàng)建上傳插件,這部分代碼沒有改變:

window.external.upload = function( state ){
    console.log( state );     // 可能為sign拖云、uploading贷笛、done、error
};


var plugin = (function(){
    var plugin = document.createElement( 'embed' );
    plugin.style.display = 'none';

    plugin.type = 'application/txftn-webkit';

    plugin.sign = function(){
        console.log( '開始文件掃描' );
    }

    plugin.pause = function(){
        console.log( '暫停文件上傳' );
    };

    plugin.uploading = function(){
        console.log( '開始文件上傳' );
    };
    plugin.del = function(){
        console.log( '刪除文件上傳' );
    }

    plugin.done = function(){
        console.log( '文件上傳完成' );
    }

    document.body.appendChild( plugin );

    return plugin;
})();

第二步宙项,改造Upload構(gòu)造函數(shù)乏苦,在構(gòu)造函數(shù)中為每種狀態(tài)子類都創(chuàng)建一個(gè)實(shí)例對(duì)象:

var Upload = function( fileName ){
    this.plugin = plugin;
    this.fileName = fileName;
    this.button1 = null;
    this.button2 = null;
    this.signState = new SignState( this );    // 設(shè)置初始狀態(tài)為waiting
    this.uploadingState = new UploadingState( this );
    this.pauseState = new PauseState( this );
    this.doneState = new DoneState( this );
    this.errorState = new ErrorState( this );
    this.currState = this.signState;    // 設(shè)置當(dāng)前狀態(tài)
};

第三步,Upload.prototype.init方法無需改變杉允,仍然負(fù)責(zé)往頁(yè)面中創(chuàng)建跟上傳流程有關(guān)的DOM節(jié)點(diǎn)邑贴,并開始綁定按鈕的事件:

Upload.prototype.init = function(){
    var that = this;

    this.dom = document.createElement( 'div' );
    this.dom.innerHTML =
        '<span>文件名稱:'+ this.fileName +'</span>\
        <button data-action="button1">掃描中</button>\
        <button data-action="button2">刪除</button>';

    document.body.appendChild( this.dom );

    this.button1 = this.dom.querySelector( '[data-action="button1"]' );
    this.button2 = this.dom.querySelector( '[data-action="button2"]' );

    this.bindEvent();
};

第四步,負(fù)責(zé)具體的按鈕事件實(shí)現(xiàn)叔磷,在點(diǎn)擊了按鈕之后,Context并不做任何具體的操作奖磁,而是把請(qǐng)求委托給當(dāng)前的狀態(tài)類來執(zhí)行:

Upload.prototype.bindEvent = function(){
    var self = this;
    this.button1.onclick = function(){
        self.currState.clickHandler1();
    }
    this.button2.onclick = function(){
        self.currState.clickHandler2();
    }
};

第四步中的代碼有一些變化改基,我們把狀態(tài)對(duì)應(yīng)的邏輯行為放在Upload類中:

Upload.prototype.sign = function(){
    this.plugin.sign();
    this.currState = this.signState;
};

Upload.prototype.uploading = function(){
    this.button1.innerHTML = '正在上傳,點(diǎn)擊暫停';
    this.plugin.uploading();
    this.currState = this.uploadingState;
};

Upload.prototype.pause = function(){
    this.button1.innerHTML = '已暫停咖为,點(diǎn)擊繼續(xù)上傳';
    this.plugin.pause();
    this.currState = this.pauseState;
};

Upload.prototype.done = function(){
    this.button1.innerHTML = '上傳完成';
    this.plugin.done();
    this.currState = this.doneState;
};

Upload.prototype.error = function(){
    this.button1.innerHTML = '上傳失敗';
    this.currState = this.errorState;
};

Upload.prototype.del = function(){
    this.plugin.del();
    this.dom.parentNode.removeChild( this.dom );
};

第五步秕狰,工作略顯乏味,我們要編寫各個(gè)狀態(tài)類的實(shí)現(xiàn)躁染。值得注意的是鸣哀,我們使用了StateFactory,從而避免因?yàn)镴avaScript中沒有抽象類所帶來的問題吞彤。

var StateFactory = (function(){

    var State = function(){};

    State.prototype.clickHandler1 = function(){
        throw new Error( '子類必須重寫父類的clickHandler1方法' );
    }

    State.prototype.clickHandler2 = function(){
        throw new Error( '子類必須重寫父類的clickHandler2方法' );
    }

    return function( param ){

        var F = function( uploadObj ){
            this.uploadObj = uploadObj;
        };

        F.prototype = new State();

        for ( var i in param ){
            F.prototype[ i ] = param[ i ];
        }

        return F;
    }

})();

var SignState = StateFactory({
    clickHandler1: function(){
        console.log( '掃描中我衬,點(diǎn)擊無效...' );
},
    clickHandler2: function(){
        console.log( '文件正在上傳中叹放,不能刪除' );
    }
});

var UploadingState = StateFactory({
    clickHandler1: function(){
        this.uploadObj.pause();
    },
    clickHandler2: function(){
        console.log( '文件正在上傳中,不能刪除' );
    }
});

var PauseState = StateFactory({
    clickHandler1: function(){
        this.uploadObj.uploading();
    },
    clickHandler2: function(){
        this.uploadObj.del();
    }
});

var DoneState = StateFactory({
    clickHandler1: function(){
        console.log( '文件已完成上傳, 點(diǎn)擊無效' );
    },
    clickHandler2: function(){
        this.uploadObj.del();
    }
});

var ErrorState = StateFactory({
    clickHandler1: function(){
        console.log( '文件上傳失敗, 點(diǎn)擊無效' );
    },
    clickHandler2: function(){
        this.uploadObj.del();
    }
});

最后是測(cè)試時(shí)間:

var uploadObj = new Upload( 'JavaScript設(shè)計(jì)模式' );
uploadObj.init();

window.external.upload = function( state ){
    uploadObj[ state ]();
};

window.external.upload( 'sign' );

setTimeout(function(){
    window.external.upload( 'uploading' );    // 1秒后開始上傳
}, 1000 );

setTimeout(function(){
    window.external.upload( 'done' );    // 5秒后上傳完成
}, 5000 );

狀態(tài)模式的優(yōu)缺點(diǎn)

到這里我們已經(jīng)學(xué)習(xí)了兩個(gè)狀態(tài)模式的例子挠羔,現(xiàn)在是時(shí)候來總結(jié)狀態(tài)模式的優(yōu)缺點(diǎn)了井仰。狀態(tài)模式的優(yōu)點(diǎn)如下。

  • 狀態(tài)模式定義了狀態(tài)與行為之間的關(guān)系破加,并將它們封裝在一個(gè)類里俱恶。通過增加新的狀態(tài)類,很容易增加新的狀態(tài)和轉(zhuǎn)換范舀。

  • 避免Context無限膨脹合是,狀態(tài)切換的邏輯被分布在狀態(tài)類中,也去掉了Context中原本過多的條件分支锭环。

  • 用對(duì)象代替字符串來記錄當(dāng)前狀態(tài)端仰,使得狀態(tài)的切換更加一目了然。

  • Context中的請(qǐng)求動(dòng)作和狀態(tài)類中封裝的行為可以非常容易地獨(dú)立變化而互不影響田藐。

狀態(tài)模式的缺點(diǎn)是會(huì)在系統(tǒng)中定義許多狀態(tài)類荔烧,編寫20個(gè)狀態(tài)類是一項(xiàng)枯燥乏味的工作,而且系統(tǒng)中會(huì)因此而增加不少對(duì)象汽久。另外鹤竭,由于邏輯分散在狀態(tài)類中,雖然避開了不受歡迎的條件分支語(yǔ)句景醇,但也造成了邏輯分散的問題臀稚,我們無法在一個(gè)地方就看出整個(gè)狀態(tài)機(jī)的邏輯。

狀態(tài)模式中的性能優(yōu)化點(diǎn)

在這兩個(gè)例子中三痰,我們并沒有太多地從性能方面考慮問題吧寺,實(shí)際上,這里有一些比較大的優(yōu)化點(diǎn)散劫。

  • 有兩種選擇來管理state對(duì)象的創(chuàng)建和銷毀稚机。第一種是僅當(dāng)state對(duì)象被需要時(shí)才創(chuàng)建并隨后銷毀,另一種是一開始就創(chuàng)建好所有的狀態(tài)對(duì)象获搏,并且始終不銷毀它們赖条。如果state對(duì)象比較龐大,可以用第一種方式來節(jié)省內(nèi)存常熙,這樣可以避免創(chuàng)建一些不會(huì)用到的對(duì)象并及時(shí)地回收它們纬乍。但如果狀態(tài)的改變很頻繁,最好一開始就把這些state對(duì)象都創(chuàng)建出來裸卫,也沒有必要銷毀它們仿贬,因?yàn)榭赡芎芸鞂⒃俅斡玫剿鼈儭?/p>

  • 在這里的例子中,我們?yōu)槊總€(gè)Context對(duì)象都創(chuàng)建了一組state對(duì)象墓贿,實(shí)際上這些state對(duì)象之間是可以共享的茧泪,各Context對(duì)象可以共享一個(gè)state對(duì)象蜓氨,這也是享元模式的應(yīng)用場(chǎng)景之一。

狀態(tài)模式和策略模式的關(guān)系

狀態(tài)模式和策略模式像一對(duì)雙胞胎调炬,它們都封裝了一系列的算法或者行為语盈,它們的類圖看起來幾乎一模一樣,但在意圖上有很大不同缰泡,因此它們是兩種迥然不同的模式刀荒。

策略模式和狀態(tài)模式的相同點(diǎn)是,它們都有一個(gè)上下文棘钞、一些策略或者狀態(tài)類缠借,上下文把請(qǐng)求委托給這些類來執(zhí)行。

它們之間的區(qū)別是策略模式中的各個(gè)策略類之間是平等又平行的宜猜,它們之間沒有任何聯(lián)系泼返,所以客戶必須熟知這些策略類的作用,以便客戶可以隨時(shí)主動(dòng)切換算法姨拥;而在狀態(tài)模式中绅喉,狀態(tài)和狀態(tài)對(duì)應(yīng)的行為是早已被封裝好的,狀態(tài)之間的切換也早被規(guī)定完成叫乌,“改變行為”這件事情發(fā)生在狀態(tài)模式內(nèi)部柴罐。對(duì)客戶來說,并不需要了解這些細(xì)節(jié)憨奸。這正是狀態(tài)模式的作用所在革屠。

JavaScript版本的狀態(tài)機(jī)

前面兩個(gè)示例都是模擬傳統(tǒng)面向?qū)ο笳Z(yǔ)言的狀態(tài)模式實(shí)現(xiàn),我們?yōu)槊糠N狀態(tài)都定義一個(gè)狀態(tài)子類排宰,然后在Context中持有這些狀態(tài)對(duì)象的引用似芝,以便把currState設(shè)置為當(dāng)前的狀態(tài)對(duì)象。

狀態(tài)模式是狀態(tài)機(jī)的實(shí)現(xiàn)之一板甘,但在JavaScript這種“無類”語(yǔ)言中党瓮,沒有規(guī)定讓狀態(tài)對(duì)象一定要從類中創(chuàng)建而來。另外一點(diǎn)虾啦,JavaScript可以非常方便地使用委托技術(shù)麻诀,并不需要事先讓一個(gè)對(duì)象持有另一個(gè)對(duì)象。下面的狀態(tài)機(jī)選擇了通過Function.prototype.call方法直接把請(qǐng)求委托給某個(gè)字面量對(duì)象來執(zhí)行傲醉。

下面改寫電燈的例子,來展示這種更加輕巧的做法:

var Light = function(){
    this.currState = FSM.off;    // 設(shè)置當(dāng)前狀態(tài)
    this.button = null;
};

Light.prototype.init = function(){
       var button = document.createElement( 'button' ),
        self = this;

    button.innerHTML = '已關(guān)燈';
    this.button = document.body.appendChild( button );

    this.button.onclick = function(){
        self.currState.buttonWasPressed.call( self );    // 把請(qǐng)求委托給FSM狀態(tài)機(jī)
    }
};

var FSM = {
    off: {
        buttonWasPressed: function(){
            console.log( '關(guān)燈' );
            this.button.innerHTML = '下一次按我是開燈';
            this.currState = FSM.on;
        }
    },
    on: {
        buttonWasPressed: function(){
            console.log( '開燈' );
            this.button.innerHTML = '下一次按我是關(guān)燈';
            this.currState = FSM.off;
        }
    }
};

var light = new Light();
light.init();

接下來嘗試另外一種方法呻率,即利用下面的delegate函數(shù)來完成這個(gè)狀態(tài)機(jī)編寫硬毕。這是面向?qū)ο笤O(shè)計(jì)和閉包互換的一個(gè)例子,前者把變量保存為對(duì)象的屬性礼仗,而后者把變量封閉在閉包形成的環(huán)境中:

var delegate = function( client, delegation ){
    return {
        buttonWasPressed: function(){    // 將客戶的操作委托給delegation對(duì)象
            return delegation.buttonWasPressed.apply( client, arguments );
        }
    }
};

var FSM = {
    off: {
        buttonWasPressed: function(){
            console.log( '關(guān)燈' );
            this.button.innerHTML = '下一次按我是開燈';
            this.currState = this.onState;
        }
    },
    on: {
        buttonWasPressed: function(){
            console.log( '開燈' );
            this.button.innerHTML = '下一次按我是關(guān)燈';
            this.currState = this.offState;
        }
    }
};

var Light = function(){
    this.offState = delegate( this, FSM.off );
    this.onState = delegate( this, FSM.on );
    this.currState = this.offState;    // 設(shè)置初始狀態(tài)為關(guān)閉狀態(tài)
    this.button = null;
};

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
        self = this;
    button.innerHTML = '已關(guān)燈';
    this.button = document.body.appendChild( button );
    this.button.onclick = function(){
        self.currState.buttonWasPressed();
    }
};

var light = new Light();
light.init();

表驅(qū)動(dòng)的有限狀態(tài)機(jī)

其實(shí)還有另外一種實(shí)現(xiàn)狀態(tài)機(jī)的方法吐咳,這種方法的核心是基于表驅(qū)動(dòng)的逻悠。我們可以在表中很清楚地看到下一個(gè)狀態(tài)是由當(dāng)前狀態(tài)和行為共同決定的。這樣一來韭脊,我們就可以在表中查找狀態(tài)童谒,而不必定義很多條件分支,如圖16-6所示沪羔。

image

剛好GitHub上有一個(gè)對(duì)應(yīng)的庫(kù)實(shí)現(xiàn)饥伊,通過這個(gè)庫(kù),可以很方便地創(chuàng)建出FSM:

var fsm = StateMachine.create({
    initial: 'off',
    events: [
        { name: 'buttonWasPressed', from: 'off',   to: 'on'  },
        { name: 'buttonWasPressed',  from: 'on',  to: 'off' }
    ],
    callbacks: {
        onbuttonWasPressed: function( event, from, to ){
            console.log( arguments );
        }
    },
    error: function( eventName, from, to, args, errorCode, errorMessage ) {
        console.log( arguments );   // 從一種狀態(tài)試圖切換到一種不可能到達(dá)的狀態(tài)的時(shí)候
    }
  });

  button.onclick = function(){
     fsm.buttonWasPressed();
  }

實(shí)際項(xiàng)目中的其他狀態(tài)機(jī)

在實(shí)際開發(fā)中蔫饰,很多場(chǎng)景都可以用狀態(tài)機(jī)來模擬琅豆, 比如一個(gè)下拉菜單在hover動(dòng)作下有顯示、懸浮篓吁、隱藏等狀態(tài)茫因;一次TCP請(qǐng)求有建立連接、監(jiān)聽杖剪、關(guān)閉等狀態(tài)冻押;一個(gè)格斗游戲中人物有攻擊、防御盛嘿、跳躍洛巢、跌倒等狀態(tài)。

狀態(tài)機(jī)在游戲開發(fā)中也有著廣泛的用途孩擂,特別是游戲AI的邏輯編寫狼渊。在之前介紹的街頭霸王游戲里,游戲主角Ryu有走動(dòng)类垦、攻擊狈邑、防御、跌倒蚤认、跳躍等多種狀態(tài)米苹。這些狀態(tài)之間既互相聯(lián)系又互相約束。比如Ryu在走動(dòng)的過程中如果被攻擊砰琢,就會(huì)由走動(dòng)狀態(tài)切換為跌倒?fàn)顟B(tài)蘸嘶。在跌倒?fàn)顟B(tài)下,Ryu既不能攻擊也不能防御陪汽。同樣训唱,Ryu也不能在跳躍的過程中切換到防御狀態(tài),但是可以進(jìn)行攻擊挚冤。這種場(chǎng)景就很適合用狀態(tài)機(jī)來描述况增。代碼如下:

var FSM = {
    walk: {
        attack: function(){
            console.log( '攻擊' );
        },
        defense: function(){
            console.log( '防御' );
        },
        jump: function(){
            console.log( '跳躍' );
        }
    },

    attack: {
        walk: function(){
            console.log( '攻擊的時(shí)候不能行走' );
        },
        defense: function(){
            console.log( '攻擊的時(shí)候不能防御' );
        },
        jump: function(){
            console.log( '攻擊的時(shí)候不能跳躍' );
        }
    }
}

小結(jié)

這里通過幾個(gè)例子,講解了狀態(tài)模式在實(shí)際開發(fā)中的應(yīng)用训挡。狀態(tài)模式也許是被大家低估的模式之一澳骤。實(shí)際上歧强,通過狀態(tài)模式重構(gòu)代碼之后,很多雜亂無章的代碼會(huì)變得清晰为肮。雖然狀態(tài)模式一開始并不是非常容易理解摊册,但我們有必須去好好掌握這種設(shè)計(jì)模式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颊艳,一起剝皮案震驚了整個(gè)濱河市茅特,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌籽暇,老刑警劉巖温治,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異戒悠,居然都是意外死亡熬荆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門绸狐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卤恳,“玉大人,你說我怎么就攤上這事寒矿⊥涣眨” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵符相,是天一觀的道長(zhǎng)拆融。 經(jīng)常有香客問我,道長(zhǎng)啊终,這世上最難降的妖魔是什么镜豹? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蓝牲,結(jié)果婚禮上趟脂,老公的妹妹穿的比我還像新娘。我一直安慰自己例衍,他們只是感情好昔期,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佛玄,像睡著了一般硼一。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梦抢,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天欠动,我揣著相機(jī)與錄音,去河邊找鬼惑申。 笑死具伍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的圈驼。 我是一名探鬼主播人芽,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼绩脆!你這毒婦竟也來了萤厅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤靴迫,失蹤者是張志新(化名)和其女友劉穎惕味,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玉锌,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡名挥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了主守。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禀倔。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖参淫,靈堂內(nèi)的尸體忽然破棺而出救湖,到底是詐尸還是另有隱情,我是刑警寧澤涎才,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布鞋既,位于F島的核電站,受9級(jí)特大地震影響耍铜,放射性物質(zhì)發(fā)生泄漏邑闺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一业扒、第九天 我趴在偏房一處隱蔽的房頂上張望检吆。 院中可真熱鬧,春花似錦程储、人聲如沸蹭沛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)摊灭。三九已至,卻和暖如春败徊,著一層夾襖步出監(jiān)牢的瞬間帚呼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留煤杀,地道東北人眷蜈。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像沈自,于是被迫代替她去往敵國(guó)和親酌儒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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