Vue 雙向數(shù)據(jù)綁定原理

# 前言

MVVM 是與 MVC 進化出來的,區(qū)別在與將view層的數(shù)據(jù)變動直接響應到viewModel層上而不是響應給model,其表現(xiàn)上最大的區(qū)別就在于雙向數(shù)據(jù)綁定功能

# 雙向數(shù)據(jù)綁定原理簡介

發(fā)布者-訂閱者模式(Backbone.js) 一般通過sub, pub的方式實現(xiàn)數(shù)據(jù)和視圖的綁定監(jiān)聽婿失,更新數(shù)據(jù)方式通常做法是 vm.set('property', value)
臟值檢查(Angular.js) 通過一定的事件觸發(fā)檢測數(shù)據(jù)是否有變更來決定是否需要更新視圖函卒。如用戶操作ng-click,XHR事件竿开,瀏覽器Location變更事件 ( [圖片上傳失敗...(image-aed435-1561973477623)]

timeout , [圖片上傳失敗...(image-9ee74c-1561973477623)]

digest() 或 $apply()等
數(shù)據(jù)劫持(vue.js)

Vue 的雙向數(shù)據(jù)綁定的原理關心兩個要點

  1. 數(shù)據(jù)劫持: 通過Object.definedProperty()劫持并監(jiān)聽數(shù)據(jù)變動
  2. 觀察者模式: 通過 發(fā)布者-訂閱者 模式實現(xiàn)數(shù)據(jù)更新

# Object.definedProperty()

這是一個原生JavaScript標準庫中的一個方法焙畔,被稱作是對象屬性的精確添加和修改掸读。用于直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性宏多, 并返回這個對象儿惫。

Object.defineProperty(obj, prop, descriptor)

image

從圖上可以看得出來,這個“精確添加和修改”的方法最重要的是屬性操作符的配置伸但。需要提到的一點是肾请,屬性操作符分類:存取操作符數(shù)據(jù)操作符。上圖也有表示更胖,指定時兩中操作符只能選擇其一铛铁,不能混用,否者報錯却妨。

被操作的屬性如果不存在饵逐,則會創(chuàng)建這個屬性梢灭;如果已存在恰矩,則執(zhí)行更新。

添加屬性

常用的 var obj = {}; obj.a = 1; 這種添加屬性的辦法實質(zhì)上就是執(zhí)行的如下操作囱修。需要強調(diào)的是:直接賦值的操作符設置的都是true捞烟,而這些操作符他們本身的默認值都是false薄声,注意區(qū)分。

var obj = {};
// 在對象中添加一個屬性與數(shù)據(jù)描述符的示例
Object.defineProperty(o, "attr", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

解讀】本例的意思是 給對象obj添加屬性attr题画,它的值為37默辨,并且設置屬性attr的值可以被修改(writable),加入到可枚舉隊列中以便for-in / for-of等操作可以遍歷到(enumerable)婴程,設置配置屬性可以修改廓奕,且該屬性可以被delete obj.attr 刪除(configurable)抱婉,所以有以下操作

obj.attr = 20;
console.log( obj.attr );  // 20
delete obj.attr;
console.log(obj.attr); // undefined

修改屬性(writable)

正常情況下屬性都需要修改功能档叔,需要設置操作符writable值為true桌粉。當設置為false時,并不會拋出錯誤衙四,但是值不會被修改铃肯。在嚴格模式下,會拋出錯誤throw Error:xx is read-only

var o = {}; // Creates a new object

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // logs 37
o.a = 25; // No error thrown
console.log(o.a); // logs 37\. 

是否可被for...in和Object.keys()獲却浮(enumerable)
var o = {};
Object.defineProperty(o, "a", { value : 1, enumerable: true });
Object.defineProperty(o, "b", { value : 2, enumerable: false });
Object.defineProperty(o, "c", { value : 3 });  // enumerable 默認是false
o.d = 4; // 采用默認賦值的方式押逼,屬性操作符enumerable為true(configurable也是true)

for (var i of o) {    
  console.log(i);  // 打印 'a' 和 'd'
}
Object.keys(o); // ["a", "d"]

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false

設置屬性不可被刪除 (configurable)

configurable特性表示對象的屬性是否可以被刪除,以及除writable特性外的其他特性是否可以被修改惦界√舾瘢【這里并不是說設置configurable之后就不能設置其他操作符,而是不能修改沾歪,如下enumerable屬性】

var o = {};
Object.defineProperty(o, "a", { 
  get : function(){return 1;}, 
  enumerable: true,
  configurable: false 
});
// 以下例子會拋出異常漂彤,因為屬性a的值不可被修改了
Object.defineProperty(o, "a", {value : 12});

console.log(o.a);   // logs 1
delete o.a;   // 不會拋出錯誤,但執(zhí)行不成功
console.log(o.a);   // logs 1

當然灾搏,如果把configurable設置為true挫望,delete o.a 就能將a屬性正確刪除

能實現(xiàn) 數(shù)據(jù)劫持的存取操作符 get/set

get()set()Object.definedProperty()中非常重要的兩個操作符,當屬性被訪問時狂窑,get方法被執(zhí)行媳板,傳入的參數(shù)列表為空。當屬性被修改時泉哈,只有一個參數(shù)蛉幸,即新值。注意參數(shù)列表雖然沒體現(xiàn)但默認都帶有this對象丛晦,并且需要注意this有可能是本身屬性巨缘,也有可能是繼承屬性。另外采呐,訪問器屬性的會"覆蓋"同名的普通屬性若锁,因為訪問器屬性會被優(yōu)先訪問,與其同名的普通屬性則會被忽略斧吐。

| obj.attr = 10這種直接賦值操作對應的 getter 和 setter 邏輯如下

var obj = { attr: 10 }

Object.definedProperty(o, 'attr', {
  enumerable : true,
  configurable : true,
  get: function() {
    console.log('get方法被調(diào)用了');
    return this.attr
  },
  set: function(newValue) {
    console.log('set方法被調(diào)用了又固,參數(shù)是: ' + newVal);
    this.attr = newValue
  }
})

obj.attr;  // get方法被調(diào)用了
obj.attr = 3;   // set方法被調(diào)用了,參數(shù)是: 3

| 數(shù)據(jù)劫持的實現(xiàn)
? 正因為getset的存在煤率,我們可以在其中自行添加一些處理邏輯

// 屬性操作符配置
var pattern = {
    enumerable : true,
    configurable : true,
    get: function () {
        return 'I alway return this string,whatever you have assigned';
    },
    set: function (newVal) {
        this.myname = 'this is my name string';
    }
};

function TestDefineSetAndGet() {
  Object.defineProperty(this, 'attr', pattern);
};

var instance = new TestDefineSetAndGet();
instance.attr = 'baby';

console.log(instance.attr); // I alway return this string,whatever you have assigned
console.log(instance.myname);  // this is my name string

# 極簡的雙向數(shù)據(jù)綁定

image

此例實現(xiàn)的效果是:隨文本框輸入文字的變化仰冠,span 中會同步顯示相同的文字內(nèi)容;在js或控制臺顯式的修改 obj.hello 的值蝶糯,視圖會相應更新洋只。這樣就實現(xiàn)了 model => view 以及 view => model 的雙向綁定。

# Vue 實現(xiàn)雙向數(shù)據(jù)綁定

整理了一下,要實現(xiàn)mvvm的雙向綁定识虚,就必須要實現(xiàn)以下幾點:
1肢扯、實現(xiàn)一個數(shù)據(jù)監(jiān)聽器Observer,能夠?qū)?shù)據(jù)對象的所有屬性進行監(jiān)聽担锤,如有變動可拿到最新值并通知訂閱者
2蔚晨、實現(xiàn)一個指令解析器Compile,對每個元素節(jié)點的指令進行掃描和解析肛循,根據(jù)指令模板替換數(shù)據(jù)铭腕,以及綁定相應的更新函數(shù)
3、實現(xiàn)一個Watcher多糠,作為連接Observer和Compile的橋梁累舷,能夠訂閱并收到每個屬性變動的通知,執(zhí)行指令綁定的相應回調(diào)函數(shù)夹孔,從而更新視圖
4被盈、mvvm入口函數(shù),整合以上三者

image
|實現(xiàn)Observer - 監(jiān)聽每個數(shù)據(jù)的變化

現(xiàn)在我們知道可以利用Obeject.defineProperty()來監(jiān)聽屬性變動析蝴, 那么將需要observe的數(shù)據(jù)對象進行遞歸遍歷害捕,包括子屬性對象的屬性,都加上 setter和getter闷畸。這樣的話尝盼,給這個對象的某個值賦值,就會觸發(fā)setter佑菩,那么就能監(jiān)聽到了數(shù)據(jù)變化盾沫。相關的代碼可以是這樣的

var data = {name: 'Crain'}
observe(data);
data.name = "Ocaka";  // 監(jiān)聽到數(shù)據(jù)變化  Crain => Ocaka (set方法中打印)

function observe() {
  if (!data || typeof data !== 'object')  return;

  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  })
}

function defineReactive(data, key, val) {
  observe(val);  // 深度遞歸
  Object.defineProperty(data, key, {
    enumerable: true,  // 可以被枚舉
    configurable: false,  // 不能再被defined
    get: function() {
      return val;
    },
    set: function(newVal) {
      console.log('監(jiān)聽到變化 ', val, '=>', newVal)
      val = newVal
    }

  })
}

| 將數(shù)據(jù)變化通知訂閱者

我們已經(jīng)可以監(jiān)聽每個數(shù)據(jù)的變化了,那么監(jiān)聽到變化之后就是怎么通知訂閱者了殿漠,所以接下來我們需要實現(xiàn)一個消息訂閱器赴精,很簡單,維護一個數(shù)組绞幌,用來收集訂閱者蕾哟,數(shù)據(jù)變動觸發(fā)notify,再調(diào)用訂閱者的update方法莲蜘,代碼改善之后是這樣:

// ..省略
function definedReactive(data, key, val) {
  var dep = new Dep(); // 實例化一個訂閱器
  observe(val);  // 深度遞歸
  Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('監(jiān)聽到變化 ', val, '=>', newVal);
            val = newVal;
            dep.notify(); // 通知所有訂閱者
        }
    });
}

// Dep訂閱器構(gòu)造函數(shù)
function Dep() {
  this.subs = [];  // 收集訂閱者
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update();
    })
  }
}

那么問題來了谭确,誰是訂閱者?怎么往訂閱器添加訂閱者票渠?
沒錯逐哈,上面的思路整理中我們已經(jīng)明確訂閱者應該是Watcher, 而且var dep = new Dep();是在 defineReactive方法內(nèi)部定義的,所以想通過dep添加訂閱者问顷,就必須要在閉包內(nèi)操作昂秃,所以我們可以在 getter里面動手腳:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在閉包內(nèi)添加watcher禀梳,所以通過Dep定義一個全局target屬性,暫存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 這里會觸發(fā)屬性的getter肠骆,從而添加訂閱者
        Dep.target = null;
    }
}

這里已經(jīng)實現(xiàn)了一個Observer了算途,已經(jīng)具備了監(jiān)聽數(shù)據(jù)和數(shù)據(jù)變化通知訂閱者的功能。那么接下來就是實現(xiàn)Compile了

| 實現(xiàn) Compile

compile主要做的事情是解析模板指令哗戈,將模板中的變量替換成數(shù)據(jù)郊艘,然后初始化渲染頁面視圖荷科,并將每個指令對應的節(jié)點綁定更新函數(shù)唯咬,添加監(jiān)聽數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動畏浆,收到通知胆胰,更新視圖,如圖所示:

image

因為遍歷解析的過程有多次操作dom節(jié)點刻获,為提高性能和效率蜀涨,會先將跟節(jié)點el轉(zhuǎn)換成文檔碎片fragment進行解析編譯操作,解析完成蝎毡,再將fragment添加回原來的真實dom節(jié)點中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 將原生節(jié)點拷貝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法將遍歷所有節(jié)點及其子節(jié)點厚柳,進行掃描解析編譯,調(diào)用對應的指令渲染函數(shù)進行數(shù)據(jù)渲染沐兵,并調(diào)用對應的指令更新函數(shù)進行綁定别垮,詳看代碼及注釋說明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表達式文本
            // 按元素節(jié)點方式編譯
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍歷編譯子節(jié)點
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 規(guī)定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令為 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令處理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化視圖
        updaterFn && updaterFn(node, vm[exp]);
        // 實例化訂閱者,此操作會在對應的屬性消息訂閱器中添加了該訂閱者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦屬性值有變化扎谎,會收到通知執(zhí)行此更新函數(shù)碳想,更新視圖
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函數(shù)
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

這里通過遞歸遍歷保證了每個節(jié)點及子節(jié)點都會解析編譯到,包括了{{}}表達式聲明的文本節(jié)點毁靶。指令的聲明規(guī)定是通過特定前綴的節(jié)點屬性來標記胧奔,如<span v-text="content" other-attrv-text便是指令,而other-attr不是指令预吆,只是普通的屬性龙填。
監(jiān)聽數(shù)據(jù)、綁定更新函數(shù)的處理是在compileUtil.bind()這個方法中拐叉,通過new Watcher()添加回調(diào)來接收數(shù)據(jù)變化的通知

至此岩遗,一個簡單的Compile就完成了。接下來要看看Watcher這個訂閱者的具體實現(xiàn)了

| 實現(xiàn)Watcher

Watcher訂閱者作為Observer和Compile之間通信的橋梁巷嚣,主要做的事情是:
1喘先、在自身實例化時往屬性訂閱器(dep)里面添加自己
2、自身必須有一個update()方法
3廷粒、待屬性變動dep.notice()通知時窘拯,能調(diào)用自身的update()方法红且,并觸發(fā)Compile中綁定的回調(diào),則功成身退涤姊。
如果有點亂暇番,可以回顧下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此處為了觸發(fā)屬性的getter,從而在dep添加自己思喊,結(jié)合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 屬性值變化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 執(zhí)行Compile中綁定的回調(diào)壁酬,更新視圖
        }
    },
    get: function() {
        Dep.target = this;    // 將當前訂閱者指向自己
        var value = this.vm[exp];    // 觸發(fā)getter,添加自己到屬性訂閱器中
        Dep.target = null;    // 添加完畢恨课,重置
        return value;
    }
};
// 這里再次列出Observer和Dep舆乔,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在閉包內(nèi)添加watcher,所以可以在Dep定義一個全局target屬性剂公,暫存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 調(diào)用訂閱者的update方法希俩,通知變化
        });
    }
};

實例化Watcher的時候,調(diào)用get()方法纲辽,通過Dep.target = watcherInstance標記訂閱者是當前watcher實例颜武,強行觸發(fā)屬性定義的getter方法,getter方法執(zhí)行的時候拖吼,就會在屬性的訂閱器dep添加當前watcher實例鳞上,從而在屬性值有變化的時候,watcherInstance就能收到更新通知吊档。

ok, Watcher也已經(jīng)實現(xiàn)了篙议。

最后來講講MVVM入口文件的相關邏輯和實現(xiàn)吧,相對就比較簡單了~

| 實現(xiàn) MVVM

MVVM作為數(shù)據(jù)綁定的入口籍铁,整合Observer涡上、Compile和Watcher三者,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化拒名,通過Compile來解析編譯模板指令吩愧,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數(shù)據(jù)變化 -> 視圖更新增显;視圖交互變化(input) -> 數(shù)據(jù)model變更的雙向綁定效果雁佳。

一個簡單的MVVM構(gòu)造器是這樣子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是這里有個問題,從代碼中可看出監(jiān)聽的數(shù)據(jù)對象是options.data同云,每次需要更新視圖糖权,則必須通過var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 這樣的方式來改變數(shù)據(jù)。

顯然不符合我們一開始的期望炸站,我們所期望的調(diào)用方式應該是這樣的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';

所以這里需要給MVVM實例添加一個屬性代理的方法星澳,使訪問vm的屬性代理為訪問vm._data的屬性,改造后的代碼如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 屬性代理旱易,實現(xiàn) vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

這里主要還是利用了Object.defineProperty()這個方法來劫持了vm實例對象的屬性的讀寫權(quán)禁偎,使讀寫vm實例的屬性轉(zhuǎn)成讀寫了vm._data的屬性值腿堤,達到魚目混珠的效果

參考文獻

MDN-Object.definedProperty()
# 剖析Vue原理&實現(xiàn)雙向綁定MVVM

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市如暖,隨后出現(xiàn)的幾起案子笆檀,更是在濱河造成了極大的恐慌,老刑警劉巖盒至,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酗洒,死亡現(xiàn)場離奇詭異,居然都是意外死亡枷遂,警方通過查閱死者的電腦和手機樱衷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來登淘,“玉大人箫老,你說我怎么就攤上這事封字∏荩” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵阔籽,是天一觀的道長流妻。 經(jīng)常有香客問我,道長笆制,這世上最難降的妖魔是什么绅这? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮在辆,結(jié)果婚禮上证薇,老公的妹妹穿的比我還像新娘。我一直安慰自己匆篓,他們只是感情好浑度,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸦概,像睡著了一般箩张。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窗市,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天先慷,我揣著相機與錄音,去河邊找鬼咨察。 笑死论熙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的摄狱。 我是一名探鬼主播脓诡,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼素跺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了誉券?” 一聲冷哼從身側(cè)響起指厌,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎踊跟,沒想到半個月后踩验,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡商玫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年箕憾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拳昌。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡袭异,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出炬藤,到底是詐尸還是另有隱情御铃,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布沈矿,位于F島的核電站上真,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏羹膳。R本人自食惡果不足惜睡互,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陵像。 院中可真熱鬧就珠,春花似錦、人聲如沸醒颖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽图贸。三九已至蹂季,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疏日,已是汗流浹背偿洁。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沟优,地道東北人涕滋。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像挠阁,于是被迫代替她去往敵國和親宾肺。 傳聞我的和親對象是個殘疾皇子溯饵,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361

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