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

# 前言

? MVVM 是與 MVC 進(jìn)化出來的,區(qū)別在與將view層的數(shù)據(jù)變動直接響應(yīng)到viewModel層上而不是響應(yīng)給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變更事件 ( location ),Timer事件(timeout , interval )觅丰,執(zhí)行digest() 或 $apply()等
數(shù)據(jù)劫持(vue.js)

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

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

# Object.definedProperty()

? 這是一個原生JavaScript標(biāo)準(zhǔn)庫中的一個方法饵溅,被稱作是對象屬性的精確添加和修改。用于直接在一個對象上定義一個新屬性妇萄,或者修改一個對象的現(xiàn)有屬性蜕企, 并返回這個對象。

Object.defineProperty(obj, prop, descriptor)

操作符含義:1/2/4的默認(rèn)值是false冠句,3/5/6默認(rèn)值是undefined

? 從圖上可以看得出來轻掩,這個“精確添加和修改”的方法最重要的是屬性操作符的配置。需要提到的一點是懦底,屬性操作符分類:存取操作符數(shù)據(jù)操作符唇牧。上圖也有表示绰播,指定時兩中操作符只能選擇其一豁延,不能混用,否者報錯轧葛。

? 被操作的屬性如果不存在杆查,則會創(chuàng)建這個屬性扮惦;如果已存在,則執(zhí)行更新亲桦。
?

添加屬性

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

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

解讀】本例的意思是 給對象obj添加屬性attr,它的值為37桃笙,并且設(shè)置屬性attr的值可以被修改(writable)氏堤,加入到可枚舉隊列中以便for-in / for-of等操作可以遍歷到(enumerable),設(shè)置配置屬性可以修改搏明,且該屬性可以被delete obj.attr 刪除(configurable)鼠锈,所以有以下操作

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

?

修改屬性(writable)

正常情況下屬性都需要修改功能,需要設(shè)置操作符writable值為true星著。當(dāng)設(shè)置為false時购笆,并不會拋出錯誤,但是值不會被修改虚循。在嚴(yán)格模式下同欠,會拋出錯誤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 默認(rèn)是false
o.d = 4; // 采用默認(rèn)賦值的方式,屬性操作符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

?

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

? configurable特性表示對象的屬性是否可以被刪除铺遂,以及除writable特性外的其他特性是否可以被修改衫哥。【這里并不是說設(shè)置configurable之后就不能設(shè)置其他操作符襟锐,而是不能修改撤逢,如下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

當(dāng)然蚊荣,如果把configurable設(shè)置為truedelete o.a 就能將a屬性正確刪除
?

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

? get()set()Object.definedProperty()中非常重要的兩個操作符莫杈,當(dāng)屬性被訪問時互例,get方法被執(zhí)行,傳入的參數(shù)列表為空筝闹。當(dāng)屬性被修改時媳叨,只有一個參數(shù),即新值丁存。注意參數(shù)列表雖然沒體現(xiàn)但默認(rèn)都帶有this對象肩杈,并且需要注意this有可能是本身屬性,也有可能是繼承屬性解寝。另外扩然,訪問器屬性的會"覆蓋"同名的普通屬性,因為訪問器屬性會被優(yōu)先訪問聋伦,與其同名的普通屬性則會被忽略夫偶。

| obj.attr = 10這種直接賦值操作對應(yīng)的 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ù)綁定


? 此例實現(xiàn)的效果是:隨文本框輸入文字的變化,span 中會同步顯示相同的文字內(nèi)容逾礁;在js或控制臺顯式的修改 obj.hello 的值说铃,視圖會相應(yīng)更新。這樣就實現(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ù)對象的所有屬性進(jìn)行監(jiān)聽,如有變動可拿到最新值并通知訂閱者
2焕刮、實現(xiàn)一個指令解析器Compile舶沿,對每個元素節(jié)點的指令進(jìn)行掃描和解析墙杯,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù)
3括荡、實現(xiàn)一個Watcher高镐,作為連接Observer和Compile的橋梁,能夠訂閱并收到每個屬性變動的通知一汽,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù)避消,從而更新視圖
4低滩、mvvm入口函數(shù)召夹,整合以上三者


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

? 現(xiàn)在我們知道可以利用Obeject.defineProperty()來監(jiān)聽屬性變動, 那么將需要observe的數(shù)據(jù)對象進(jìn)行遞歸遍歷恕沫,包括子屬性對象的屬性监憎,都加上 setter和getter。這樣的話婶溯,給這個對象的某個值賦值鲸阔,就會觸發(fā)setter,那么就能監(jiān)聽到了數(shù)據(jù)變化迄委。相關(guān)的代碼可以是這樣的

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)一個消息訂閱器叙身,很簡單渔扎,維護(hù)一個數(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)明確訂閱者應(yī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ù)翎苫,然后初始化渲染頁面視圖,并將每個指令對應(yīng)的節(jié)點綁定更新函數(shù)榨了,添加監(jiān)聽數(shù)據(jù)的訂閱者煎谍,一旦數(shù)據(jù)有變動,收到通知龙屉,更新視圖呐粘,如圖所示:


因為遍歷解析的過程有多次操作dom節(jié)點,為提高性能和效率转捕,會先將跟節(jié)點el轉(zhuǎn)換成文檔碎片fragment進(jìn)行解析編譯操作作岖,解析完成,再將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é)點五芝,進(jìn)行掃描解析編譯痘儡,調(diào)用對應(yīng)的指令渲染函數(shù)進(jìn)行數(shù)據(jù)渲染,并調(diào)用對應(yīng)的指令更新函數(shù)進(jìn)行綁定枢步,詳看代碼及注釋說明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表達(dá)式文本
            // 按元素節(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]);
        // 實例化訂閱者沉删,此操作會在對應(yīng)的屬性消息訂閱器中添加了該訂閱者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é)點都會解析編譯到矾瑰,包括了{{}}表達(dá)式聲明的文本節(jié)點。指令的聲明規(guī)定是通過特定前綴的節(jié)點屬性來標(biāo)記隘擎,如<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;    // 將當(dāng)前訂閱者指向自己
        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標(biāo)記訂閱者是當(dāng)前watcher實例孟害,強行觸發(fā)屬性定義的getter方法,getter方法執(zhí)行的時候戚绕,就會在屬性的訂閱器dep添加當(dāng)前watcher實例纹坐,從而在屬性值有變化的時候枝冀,watcherInstance就能收到更新通知舞丛。

ok, Watcher也已經(jīng)實現(xiàn)了,完整代碼果漾。
基本上vue中數(shù)據(jù)綁定相關(guān)比較核心的幾個模塊也是這幾個球切,猛戳這里 , 在src 目錄可找到vue源碼。

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

| 實現(xiàn) MVVM

MVVM作為數(shù)據(jù)綁定的入口吨凑,整合Observer、Compile和Watcher三者户辱,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化鸵钝,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁庐镐,達(dá)到數(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)用方式應(yīng)該是這樣的:
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的屬性值福压,達(dá)到魚目混珠的效果

參考文獻(xiàn)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掏秩,一起剝皮案震驚了整個濱河市绘证,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哗讥,老刑警劉巖嚷那,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異杆煞,居然都是意外死亡魏宽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門决乎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來队询,“玉大人,你說我怎么就攤上這事构诚“稣叮” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵范嘱,是天一觀的道長送膳。 經(jīng)常有香客問我,道長丑蛤,這世上最難降的妖魔是什么叠聋? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮受裹,結(jié)果婚禮上碌补,老公的妹妹穿的比我還像新娘。我一直安慰自己棉饶,他們只是感情好厦章,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著照藻,像睡著了一般袜啃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岩梳,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天囊骤,我揣著相機與錄音,去河邊找鬼冀值。 笑死也物,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的列疗。 我是一名探鬼主播滑蚯,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了告材?” 一聲冷哼從身側(cè)響起坤次,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斥赋,沒想到半個月后缰猴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡疤剑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年滑绒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隘膘。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡疑故,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弯菊,到底是詐尸還是另有隱情纵势,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布管钳,位于F島的核電站钦铁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蹋嵌。R本人自食惡果不足惜育瓜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栽烂。 院中可真熱鬧,春花似錦恋脚、人聲如沸腺办。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怀喉。三九已至,卻和暖如春船响,著一層夾襖步出監(jiān)牢的瞬間躬拢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工见间, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聊闯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓米诉,卻偏偏與公主長得像菱蔬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355