JavaScript設計模式之享元模式

享元(flyweight)模式是一種用于性能優化的模式,“fly”在這里是蒼蠅的意思,意為蠅量級。享元模式的核心是運用共享技術來有效支持大量細粒度的對象。

如果系統中因為創建了大量類似的對象而導致內存占用過高,享元模式就非常有用了。在JavaScript中,瀏覽器特別是移動端的瀏覽器分配的內存并不算多,如何節省內存就成了一件非常有意義的事情。

享元模式的概念初聽起來并不太好理解,所以在深入講解之前,我們先看一個例子。

初識享元模式

假設有個內衣工廠,目前的產品有50種男式內衣和50種女士內衣,為了推銷產品,工廠決定生產一些塑料模特來穿上他們的內衣拍成廣告照片。 正常情況下需要50個男模特和50個女模特,然后讓他們每人分別穿上一件內衣來拍照。不使用享元模式的情況下,在程序里也許會這樣寫:

var Model = function( sex, underwear){
    this.sex = sex;
    this.underwear= underwear;
};

Model.prototype.takePhoto = function(){
    console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};

for ( var i = 1; i <= 50; i++ ){
    var maleModel = new Model( 'male', 'underwear' + i );
    maleModel.takePhoto();
};

for ( var j = 1; j <= 50; j++ ){
    var femaleModel= new Model( 'female', 'underwear' + j );
    femaleModel.takePhoto();
};

要得到一張照片,每次都需要傳入sex和underwear參數,如上所述,現在一共有50種男內衣和50種女內衣,所以一共會產生100個對象。如果將來生產了10000種內衣,那這個程序可能會因為存在如此多的對象已經提前崩潰。

下面我們來考慮一下如何優化這個場景。雖然有100種內衣,但很顯然并不需要50個男模特和50個女模特。其實男模特和女模特各自有一個就足夠了,他們可以分別穿上不同的內衣來拍照。

現在來改寫一下代碼,既然只需要區別男女模特,那我們先把underwear參數從構造函數中移除,構造函數只接收sex參數:

var Model = function( sex ){
    this.sex = sex;
};

Model.prototype.takePhoto = function(){
    console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};

分別創建一個男模特對象和一個女模特對象:

var maleModel = new Model( 'male' ),
    femaleModel = new Model( 'female' );

給男模特依次穿上所有的男裝,并進行拍照:

for ( var i = 1; i <= 50; i++ ){
    maleModel.underwear = 'underwear' + i;
    maleModel.takePhoto();
};

同樣,給女模特依次穿上所有的女裝,并進行拍照:

for ( var j = 1; j <= 50; j++ ){
    femaleModel.underwear = 'underwear' + j;
    femaleModel.takePhoto();
};

可以看到,改進之后的代碼,只需要兩個對象便完成了同樣的功能。

內部狀態與外部狀態

上面的這個例子便是享元模式的雛形,享元模式要求將對象的屬性劃分為內部狀態與外部狀態(狀態在這里通常指屬性)。享元模式的目標是盡量減少共享對象的數量,關于如何劃分內部狀態和外部狀態,下面的幾條經驗提供了一些指引。

  • 內部狀態存儲于對象內部。

  • 內部狀態可以被一些對象共享。

  • 內部狀態獨立于具體的場景,通常不會改變。

  • 外部狀態取決于具體的場景,并根據場景而變化,外部狀態不能被共享。

這樣一來,我們便可以把所有內部狀態相同的對象都指定為同一個共享的對象。而外部狀態可以從對象身上剝離出來,并儲存在外部。

剝離了外部狀態的對象成為共享對象,外部狀態在必要時被傳入共享對象來組裝成一個完整的對象。雖然組裝外部狀態成為一個完整對象的過程需要花費一定的時間,但卻可以大大減少系統中的對象數量,相比之下,這點時間或許是微不足道的。因此,享元模式是一種用時間換空間的優化模式。

在上面的例子中,性別是內部狀態,內衣是外部狀態,通過區分這兩種狀態,大大減少了系統中的對象數量。通常來講,內部狀態有多少種組合,系統中便最多存在多少個對象,因為性別通常只有男女兩種,所以該內衣廠商最多只需要2個對象。

使用享元模式的關鍵是如何區別內部狀態和外部狀態。可以被對象共享的屬性通常被劃分為內部狀態,如同不管什么樣式的衣服,都可以按照性別不同,穿在同一個男模特或者女模特身上,模特的性別就可以作為內部狀態儲存在共享對象的內部。而外部狀態取決于具體的場景,并根據場景而變化,就像例子中每件衣服都是不同的,它們不能被一些對象共享,因此只能被劃分為外部狀態。

享元模式的通用結構

上面的示例初步展示了享元模式的威力,但這還不是一個完整的享元模式,在這個例子中還存在以下兩個問題。

  • 我們通過構造函數顯式new出了男女兩個model對象,在其他系統中,也許并不是一開始就需要所有的共享對象。

  • 給model對象手動設置了underwear外部狀態,在更復雜的系統中,這不是一個最好的方式,因為外部狀態可能會相當復雜,它們與共享對象的聯系會變得困難。

我們通過一個對象工廠來解決第一個問題,只有當某種共享對象被真正需要時,它才從工廠中被創建出來。對于第二個問題,可以用一個管理器來記錄對象相關的外部狀態,使這些外部狀態通過某個鉤子和共享對象聯系起來。

文件上傳的例子

在微云上傳模塊的開發中,我們曾經借助享元模式提升了程序的性能。下面我們就講述這個例子。

1.對象爆炸

在微云上傳模塊的開發中,我曾經經歷過對象爆炸的問題。微云的文件上傳功能雖然可以選擇依照隊列,一個一個地排隊上傳,但也支持同時選擇2000個文件。每一個文件都對應著一個JavaScript上傳對象的創建,在第一版開發中,的確往程序里同時new了2000個upload對象,結果可想而知,Chrome中還勉強能夠支撐,IE下直接進入假死狀態。

微云支持好幾種上傳方式,比如瀏覽器插件、Flash和表單上傳等,為了簡化例子,我們先假設只有插件和Flash這兩種。不論是插件上傳,還是Flash上傳,原理都是一樣的,當用戶選擇了文件之后,插件和Flash都會通知調用Window下的一個全局JavaScript函數,它的名字是startUpload,用戶選擇的文件列表被組合成一個數組files塞進該函數的參數列表里,代碼如下:

var id = 0;

window.startUpload = function( uploadType, files ){    // uploadType區分是控件還是flash
    for ( var i = 0, file; file = files[ i++ ]; ){
        var uploadObj = new Upload( uploadType, file.fileName, file.fileSize );
        uploadObj.init( id++ );    // 給upload對象設置一個唯一的id
    }
};

當用戶選擇完文件之后,startUpload函數會遍歷files數組來創建對應的upload對象。接下來定義Upload構造函數,它接受3個參數,分別是插件類型、文件名和文件大小。這些信息都已經被插件組裝在files數組里返回,代碼如下:

var Upload = function( uploadType, fileName, fileSize ){
    this.uploadType = uploadType;
    this.fileName = fileName;
    this.fileSize = fileSize;
    this.dom= null;
};

Upload.prototype.init = function( id ){
    var that = this;
    this.id = id;
    this.dom = document.createElement( 'div' );
    this.dom.innerHTML =
               '<span>文件名稱:'+ this.fileName +', 文件大小: '+ this.fileSize +'</span>' +
               '<button class="delFile">刪除</button>';

    this.dom.querySelector( '.delFile' ).onclick = function(){
        that.delFile();
    }
    document.body.appendChild( this.dom );
};

同樣為了簡化示例,我們暫且去掉了upload對象的其他功能,只保留刪除文件的功能,對應的方法是Upload.prototype.delFile。該方法中有一個邏輯:當被刪除的文件小于3000 KB時,該文件將被直接刪除。否則頁面中會彈出一個提示框,提示用戶是否確認要刪除該文件,代碼如下:

Upload.prototype.delFile = function(){
    if ( this.fileSize < 3000 ){
        return this.dom.parentNode.removeChild( this.dom );
    }

    if ( window.confirm( '確定要刪除該文件嗎? ' + this.fileName ) ){
        return this.dom.parentNode.removeChild( this.dom );
    }
};

接下來分別創建3個插件上傳對象和3個Flash上傳對象:

startUpload( 'plugin', [
    {
        fileName: '1.txt',
        fileSize: 1000
    },
    {
        fileName: '2.html',
        fileSize: 3000
    },
    {
        fileName: '3.txt',
        fileSize: 5000
    }
]);

startUpload( 'flash', [
    {
        fileName: '4.txt',
        fileSize: 1000
    },
    {
        fileName: '5.html',
        fileSize: 3000
    },
    {
        fileName: '6.txt',
        fileSize: 5000
    }
]);

當點擊刪除最后一個文件時,可以看到彈出了是否確認刪除的提示,如圖12-1所示。


image

2.享元模式重構文件上傳

上面的代碼是第一版的文件上傳,在這段代碼里有多少個需要上傳的文件,就一共創建了多少個upload對象,接下來我們用享元模式重構它。

首先,我們需要確認插件類型uploadType是內部狀態,那為什么單單uploadType是內部狀態呢?前面講過,劃分內部狀態和外部狀態的關鍵主要有以下幾點。

  • 內部狀態儲存于對象內部。

  • 內部狀態可以被一些對象共享。

  • 內部狀態獨立于具體的場景,通常不會改變。

  • 外部狀態取決于具體的場景,并根據場景而變化,外部狀態不能被共享。

在文件上傳的例子里,upload對象必須依賴uploadType屬性才能工作,這是因為插件上傳、Flash上傳、表單上傳的實際工作原理有很大的區別,它們各自調用的接口也是完全不一樣的,必須在對象創建之初就明確它是什么類型的插件,才可以在程序的運行過程中,讓它們分別調用各自的start、pause、cancel、del等方法。

實際上在微云的真實代碼中,雖然插件和Flash上傳對象最終創建自一個大的工廠類,但它們實際上根據uploadType值的不同,分別是來自于兩個不同類的對象。(在目前的例子中,為了簡化代碼,我們把插件和Flash的構造函數合并成了一個。)

一旦明確了uploadType,無論我們使用什么方式上傳,這個上傳對象都是可以被任何文件共用的。而fileName和fileSize是根據場景而變化的,每個文件的fileName和fileSize都不一樣,fileName和fileSize沒有辦法被共享,它們只能被劃分為外部狀態。

4.剝離外部狀態

明確了uploadType作為內部狀態之后,我們再把其他的外部狀態從構造函數中抽離出來, Upload構造函數中只保留uploadType參數:

var Upload = function( uploadType){
    this.uploadType = uploadType;
};

Upload.prototype.init函數也不再需要,因為upload對象初始化的工作被放在了upload- Manager.add函數里面,接下來只需要定義Upload.prototype.del函數即可:

Upload.prototype.delFile = function( id ){
    uploadManager.setExternalState( id, this );  // (1)

    if ( this.fileSize < 3000 ){
        return this.dom.parentNode.removeChild( this.dom );
    }

    if ( window.confirm( '確定要刪除該文件嗎? ' + this.fileName ) ){
        return this.dom.parentNode.removeChild( this.dom );
    }
};

在開始刪除文件之前,需要讀取文件的實際大小,而文件的實際大小被儲存在外部管理器uploadManager中,所以在這里需要通過uploadManager.setExternalState方法給共享對象設置正確的fileSize,上段代碼中的(1)處表示把當前id對應的對象的外部狀態都組裝到共享對象中。

4.工廠進行對象實例化

接下來定義一個工廠來創建upload對象,如果某種內部狀態對應的共享對象已經被創建過,那么直接返回這個對象,否則創建一個新的對象:

var UploadFactory = (function(){
    var createdFlyWeightObjs = {};

    return {
        create: function( uploadType){
            if ( createdFlyWeightObjs [ uploadType] ){
                return createdFlyWeightObjs [ uploadType];
            }

            return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
        }
    }
})();

5.管理器封裝外部狀態

現在我們來完善前面提到的uploadManager對象,它負責向UploadFactory提交創建對象的請求,并用一個uploadDatabase 對象保存所有upload對象的外部狀態,以便在程序運行過程中給upload共享對象設置外部狀態,代碼如下:

var uploadManager = (function(){
    var uploadDatabase = {};

    return {
        add: function( id, uploadType, fileName, fileSize ){
            var flyWeightObj = UploadFactory.create( uploadType );

            var dom = document.createElement( 'div' );
            dom.innerHTML =
                    '<span>文件名稱:'+ fileName +', 文件大小: '+ fileSize +'</span>' +
                    '<button class="delFile">刪除</button>';

            dom.querySelector( '.delFile' ).onclick = function(){
                flyWeightObj.delFile( id );
            }

            document.body.appendChild( dom );

            uploadDatabase[ id ] = {
                fileName: fileName,
                fileSize: fileSize,
                dom: dom
            };

            return flyWeightObj ;
        },
        setExternalState: function( id, flyWeightObj ){
            var uploadData = uploadDatabase[ id ];
            for ( var i in uploadData ){
                flyWeightObj[ i ] = uploadData[ i ];
            }
        }
    }
})();

然后是開始觸發上傳動作的startUpload函數:

var id = 0;

window.startUpload = function( uploadType, files ){
   for ( var i = 0, file; file = files[ i++ ]; ){
        var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
   }
};

最后是測試時間,運行下面的代碼后,可以發現運行結果跟用享元模式重構之前一致:

startUpload( 'plugin', [
    {
        fileName: '1.txt',
        fileSize: 1000
    },
    {
        fileName: '2.html',
        fileSize: 3000
    },
    {
        fileName: '3.txt',
        fileSize: 5000
    }
]);

startUpload( 'flash', [
    {
        fileName: '4.txt',
        fileSize: 1000
    },
    {
        fileName: '5.html',
        fileSize: 3000
    },
    {
        fileName: '6.txt',
        fileSize: 5000
    }
]);

享元模式重構之前的代碼里一共創建了6個upload對象,而通過享元模式重構之后,對象的數量減少為2,更幸運的是, 就算現在同時上傳2000個文件,需要創建的upload對象數量依然是2。

享元模式的適用性

享元模式是一種很好的性能優化方案,但它也會帶來一些復雜性的問題,從前面兩組代碼的比較可以看到,使用了享元模式之后,我們需要分別多維護一個factory對象和一個manager對象,在大部分不必要使用享元模式的環境下,這些開銷是可以避免的。

享元模式帶來的好處很大程度上取決于如何使用以及何時使用,一般來說,以下情況發生時便可以使用享元模式。

  • 一個程序中使用了大量的相似對象。

  • 由于使用了大量對象,造成很大的內存開銷。

  • 對象的大多數狀態都可以變為外部狀態。

  • 剝離出對象的外部狀態之后,可以用相對較少的共享對象取代大量對象。

可以看到,文件上傳的例子完全符合這四點。

再談內部狀態和外部狀態

如果順利的話,通過前面的例子我們已經了解了內部狀態和外部狀態的概念以及享元模式的工作原理。我們知道,實現享元模式的關鍵是把內部狀態和外部狀態分離開來。有多少種內部狀態的組合,系統中便最多存在多少個共享對象,而外部狀態儲存在共享對象的外部,在必要時被傳入共享對象來組裝成一個完整的對象。現在來考慮兩種極端的情況,即對象沒有外部狀態和沒有內部狀態的時候。

1.沒有內部狀態的享元

在文件上傳的例子中,我們分別進行過插件調用和Flash調用,即startUpload( 'plugin', [] )和startUpload( flash, [] ),導致程序中創建了內部狀態不同的兩個共享對象。也許你會奇怪,在文件上傳程序里,一般都會提前通過特性檢測來選擇一種上傳方式,如果瀏覽器支持插件就用插件上傳,如果不支持插件,就用Flash上傳。那么,什么情況下既需要插件上傳又需要Flash上傳呢?

實際上這個需求是存在的,很多網盤都提供了極速上傳(控件)與普通上傳(Flash)兩種模式,如果極速上傳不好使(可能是沒有安裝控件或者控件損壞),用戶還可以隨時切換到普通上傳模式,所以這里確實是需要同時存在兩個不同的upload共享對象。

但不是每個網站都必須做得如此復雜,很多小一些的網站就只支持單一的上傳方式。假設我們是這個網站的開發者,不需要考慮極速上傳與普通上傳之間的切換,這意味著在之前的代碼中作為內部狀態的uploadType屬性是可以刪除掉的。

在繼續使用享元模式的前提下,構造函數Upload就變成了無參數的形式:

var Upload = function(){};

其他屬性如fileName、fileSize、dom依然可以作為外部狀態保存在共享對象外部。在uploadType作為內部狀態的時候,它可能為控件,也可能為Flash,所以當時最多可以組合出兩個共享對象。而現在已經沒有了內部狀態,這意味著只需要唯一的一個共享對象。現在我們要改寫創建享元對象的工廠,代碼如下:

var UploadFactory = (function(){
    var uploadObj;
    return {
        create: function(){
            if ( uploadObj ){
                return uploadObj;
            }
            return uploadObj = new Upload();
        }
    }
})();

管理器部分的代碼不需要改動,還是負責剝離和組裝外部狀態。可以看到,當對象沒有內部狀態的時候,生產共享對象的工廠實際上變成了一個單例工廠。雖然這時候的共享對象沒有內部狀態的區分,但還是有剝離外部狀態的過程,我們依然傾向于稱之為享元模式。

2.沒有外部狀態的享元

網上許多資料中,經常把Java或者C#的字符串看成享元,這種說法是否正確呢?我們看看下面這段Java代碼,來分析一下:

// Java代碼

public class Test {

    public static void main( String args[] ){
        String a1 = new String( "a" ).intern();
        String a2 = new String( "a" ).intern();
        System.out.println( a1 == a2 );    // true
    }
}

在這段Java代碼里,分別new了兩個字符串對象a1和a2。intern是一種對象池技術, new String("a").intern()的含義如下。

如果值為a的字符串對象已經存在于對象池中,則返回這個對象的引用。

反之,將字符串a的對象添加進對象池,并返回這個對象的引用。

所以a1 == a2的結果是true,但這并不是使用了享元模式的結果,享元模式的關鍵是區別內部狀態和外部狀態。享元模式的過程是剝離外部狀態,并把外部狀態保存在其他地方,在合適的時刻再把外部狀態組裝進共享對象。這里并沒有剝離外部狀態的過程,a1和a2指向的完全就是同一個對象,所以如果沒有外部狀態的分離,即使這里使用了共享的技術,但并不是一個純粹的享元模式。

對象池

我們在前面已經提到了Java中String的對象池,下面就來學習這種共享的技術。對象池維護一個裝載空閑對象的池子,如果需要對象的時候,不是直接new,而是轉從對象池里獲取。如果對象池里沒有空閑對象,則創建一個新的對象,當獲取出的對象完成它的職責之后, 再進入池子等待被下次獲取。

對象池的原理很好理解,比如我們組人手一本《JavaScript權威指南》,從節約的角度來講,這并不是很劃算,因為大部分時間這些書都被閑置在各自的書架上,所以我們一開始就只買一本,或者一起建立一個小型圖書館(對象池),需要看書的時候就從圖書館里借,看完了之后再把書還回圖書館。如果同時有三個人要看這本書,而現在圖書館里只有兩本,那我們再馬上去書店買一本放入圖書館。

對象池技術的應用非常廣泛,HTTP連接池和數據庫連接池都是其代表應用。在Web前端開發中,對象池使用最多的場景大概就是跟DOM有關的操作。很多空間和時間都消耗在了DOM節點上,如何避免頻繁地創建和刪除DOM節點就成了一個有意義的話題。

1.對象池實現

假設我們在開發一個地圖應用, 地圖上經常會出現一些標志地名的小氣泡,我們叫它toolTip。如圖12-2所示。


image

在搜索我家附近地圖的時候,頁面里出現了2個小氣泡。當我再搜索附近的蘭州拉面館時,頁面中出現了6個小氣泡。按照對象池的思想,在第二次搜索開始之前,并不會把第一次創建的2個小氣泡刪除掉,而是把它們放進對象池。這樣在第二次的搜索結果頁面里,我們只需要再創建4個小氣泡而不是6個。

先定義一個獲取小氣泡節點的工廠,作為對象池的數組成為私有屬性被包含在工廠閉包里,這個工廠有兩個暴露對外的方法,create表示獲取一個div節點,recover表示回收一個div節點:

var toolTipFactory = (function(){
    var toolTipPool = [];    // toolTip對象池

    return {
        create: function(){
            if ( toolTipPool.length === 0 ){    // 如果對象池為空
                var div = document.createElement( 'div' );    // 創建一個dom
                document.body.appendChild( div );
                return div;
            }else{    // 如果對象池里不為空
                return toolTipPool.shift();  // 則從對象池中取出一個dom
            }
        },
        recover: function( tooltipDom ){
            return toolTipPool.push( tooltipDom );    // 對象池回收dom
        }
    }
})();

現在把時鐘撥回進行第一次搜索的時刻,目前需要創建2個小氣泡節點,為了方便回收,用一個數組ary來記錄它們:

var ary = [];

for ( var i = 0, str; str = [ 'A', 'B' ][ i++ ]; ){
    var toolTip = toolTipFactory.create();
    toolTip.innerHTML = str;
    ary.push( toolTip );
};

如果你愿意稍稍測試一下,可以看到頁面中出現了innerHTML分別為A和B的兩個div節點。

接下來假設地圖需要開始重新繪制,在此之前要把這兩個節點回收進對象池:

for ( var i = 0, toolTip; toolTip = ary[ i++ ]; ){
    toolTipFactory.recover( toolTip );
};

再創建6個小氣泡:

for ( var i = 0, str; str = [ 'A', 'B', 'C', 'D', 'E', 'F' ][ i++ ]; ){
    var toolTip = toolTipFactory.create();
    toolTip.innerHTML = str;
};

現在再測試一番,頁面中出現了內容分別為A、B、C、D、E、F的6個節點,上一次創建好的節點被共享給了下一次操作。對象池跟享元模式的思想有點相似,雖然innerHTML的值A、B、C、D等也可以看成節點的外部狀態,但在這里我們并沒有主動分離內部狀態和外部狀態的過程。

2.通用對象池實現

我們還可以在對象池工廠里,把創建對象的具體過程封裝起來,實現一個通用的對象池:

var objectPoolFactory = function( createObjFn ){
    var objectPool = [];

    return {
        create: function(){
            var obj = objectPool.length === 0 ?
                createObjFn.apply( this, arguments ) : objectPool.shift();

            return obj;
        },
        recover: function( obj ){
            objectPool.push( obj );
        }
    }
};

現在利用objectPoolFactory來創建一個裝載一些iframe的對象池:

var iframeFactory = objectPoolFactory( function(){
    var iframe = document.createElement( 'iframe' );
       document.body.appendChild( iframe );

    iframe.onload = function(){
        iframe.onload = null;    // 防止iframe重復加載的bug
        iframeFactory.recover( iframe );    // iframe加載完成之后回收節點
    }

    return iframe;

 });

 var iframe1 = iframeFactory.create();
 iframe1.src = 'http:// baidu.com';

 var iframe2 = iframeFactory.create();
 iframe2.src = 'http:// QQ.com';

 setTimeout(function(){
      var iframe3 = iframeFactory.create();
     iframe3.src = 'http:// 163.com';
 }, 3000 );

對象池是另外一種性能優化方案,它跟享元模式有一些相似之處,但沒有分離內部狀態和外部狀態這個過程。本章用享元模式完成了一個文件上傳的程序,其實也可以用對象池+事件委托來代替實現。

小結

享元模式是為解決性能問題而生的模式,這跟大部分模式的誕生原因都不一樣。在一個存在大量相似對象的系統中,享元模式可以很好地解決大量對象帶來的性能問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容