從零實現(xiàn)一個簡單的Vue框架赔硫,掌握MVVM框架原理

--文末附視頻教程

本文主要學(xué)習(xí)掌握 Vue 雙向綁定的核心部分原理盐肃。
代碼為簡化版砸王,相對比較簡陋谦铃。也未考慮數(shù)組等其他處理。
歡迎一起學(xué)習(xí)交流瘪菌。

一、準(zhǔn)備工作

1. 什么是 MVVM 框架诵肛?

MVVM 是 Model-View-ViewModel 的簡寫怔檩,雙向數(shù)據(jù)綁定蓄诽,即視圖影響模型仑氛,模型影響數(shù)據(jù)调衰。它本質(zhì)上就是MVC 的改進(jìn)版。

  • Model(模型)是數(shù)據(jù)訪問層,例如后臺接口傳遞的數(shù)據(jù)
  • View(視圖)是用戶在屏幕上看到的頁面的結(jié)構(gòu)趋箩、布局叫确、外觀(UI)
  • ViewModel(視圖模型)負(fù)責(zé)將 View 的變化同步到 Model芍锦,或 Model 的變化轉(zhuǎn)化為 View娄琉。

2. Vue 怎么實現(xiàn)雙向數(shù)據(jù)綁定

Vue2.x 是通過 Object.defineProperty 實現(xiàn)的雙向數(shù)據(jù)綁定,該方法不支持 ie8 及以下版本票腰。
相關(guān)語法直接查看mdn文檔杏慰。
其中,屬性描述符有很多個轰胁,下面簡單說明一下常用的幾個赃阀,具體詳細(xì)內(nèi)容直接查看文檔吟税。
下面是給定義 obj 對象定義一個名稱為 Vue 的屬性肠仪。

Object.defineProperty(obj, 'vue', {
  configurable: true,
  writable: true,
  enmerbale: true,
  value: 'Hello, Vue',
  get() {
    return 'Hello, Vue'
  }
  set(val) {
    console.log(val)
  }
})

configurable: 指定屬性是否可以配置异旧,如果不設(shè)置為true,則無法刪除該屬性荤崇,例如:delete obj.vue='react'無效

writable: 指定屬性是否能被賦值運算符改變术荤,如果不設(shè)置為true每篷,則給 vue 屬性賦值焦读,例如:obj.vue='react'無效

enmerbale: 指定屬性是否可以被枚舉到矗晃,如果不設(shè)置為true,使用 for...in... 或 Object.keys 是讀不到該屬性的

value: 指定屬性對應(yīng)的值仓技,與 get 屬性沖突浑彰,一起使用會報錯

get: 訪問該屬性時郭变,如果有設(shè)置 get 方法,會執(zhí)行這個方法并返回

set: 修改該屬性值時周伦,如果有設(shè)置 set 方法专挪,會執(zhí)行這個方法片排,并把新的值作為參數(shù)傳進(jìn)入 object.vue = 'hello, Vuex'

3. 流程圖

流程圖

4. 流程分析

這里我們先看看代碼實現(xiàn)率寡,大概了解一下整個過程冶共,最后再對整個過程進(jìn)行分析捅僵。

結(jié)合代碼家卖、注釋上荡、過程分析可以更好的理解整個過程榛臼。

二窜司、開始實現(xiàn)

參考 Vue2.x 源碼實現(xiàn)塞祈,與實際 Vue 的實現(xiàn)有差別议薪,但原理上差不多媳友。建議看完可以繼續(xù)深入學(xué)習(xí) Vue 實際源碼

1. Vue 入口

// 模擬 Vue 的入口
function MVVM(options) {
    var vm = this;
    vm.$options = options || {};
    vm._data = vm.$options.data;
    /**
     * initState 主要對數(shù)據(jù)對處理
     * 實現(xiàn) observe醇锚,即對 data/computed 等做響應(yīng)式處理以及將數(shù)據(jù)代理到 vm 實例上
     */
    initState(vm)
        // 編譯模版
    this.$compile = new Compile(options.el || document.body, this)
}

2. 模版編譯

這里的 Compile 只是簡單的模版編譯,與 Vue 實際Compile 有較大區(qū)別看靠,實際的 Compile 實現(xiàn)比較復(fù)雜挟炬,需要經(jīng)過 parse嗦哆、optimize老速、generate 三個階段處理烁峭。

  • parse: 使用正則解析template中的vue的指令(v-xxx) 變量等等 形成抽象語法樹AST
  • optimize: 標(biāo)記一些靜態(tài)節(jié)點,用作后面的性能優(yōu)化缩挑,在diff的時候直接略過
  • generate: 把第一部生成的AST 轉(zhuǎn)化為渲染函數(shù) render function
function Compile(el, vm) {
  this.$vm = vm;
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);

  if (this.$el) {
    // 將原生節(jié)點轉(zhuǎn)為文檔碎片節(jié)點供置,提高操作效率
    this.$fragment = this.node2Fragment(this.$el);
    // 編譯模版內(nèi)容芥丧,同時進(jìn)行依賴收集
    this.compile(this.$fragment);
    // 將處理后的 dom 樹掛載到真實 dom 節(jié)點中
    this.$el.appendChild(this.$fragment);
  }
}
// compile 相關(guān)方法實現(xiàn)
Compile.prototype = {
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    /**
     * 將原生節(jié)點拷貝到 fragment续担,
     * 每次循環(huán)都會把 el 中的第一個節(jié)點取出來追加到 fragment 后面活孩,直到 el 沒有字節(jié)點
     */
    let child;
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
  
    return fragment;
  },
  compile: function (el) {
    const childNodes = el.childNodes
    // childNodes 不是標(biāo)準(zhǔn)數(shù)組憾儒,通過 Array.from 把 childNodes 轉(zhuǎn)成數(shù)組并遍歷處理每一個節(jié)點起趾。
    Array.from(childNodes).forEach(node => {
      // 利用閉包機(jī)制诗舰,保存文本節(jié)點最初的文本,后面更新根據(jù)最初的文本進(jìn)行替換更新训裆。
      const text = node.textContent;
      // 元素節(jié)點眶根,對元素屬性綁定對指令進(jìn)行處理
      if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      // 文本節(jié)點并且包含 {{xx}} 字符串對文本蜀铲,模版內(nèi)容替換
      else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(text)) {
        this.compileText(node, RegExp.$1.trim(), text);
      }
      // 遞歸編譯子節(jié)點的內(nèi)容
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  },

  compileElement: function (node) {
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr => {
      const attrName = attr.name;
      // 判斷屬性是否是一個指令,例如: v-text 等
      if (this.isDirective(attrName)) {
        const exp = attr.value;
        const dir = attrName.substring(2);
        // 事件指令
        if (this.isEventDirective(dir)) {
          compileUtil.eventHandler(node, this.$vm, exp, dir);
        }
        // 普通指令
        else {
          compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
        }
        node.removeAttribute(attrName);
      }
    });
  },

  compileText: function (node, exp) {
    // compileUtil.text(node, this.$vm, exp);
    // 利用閉包機(jī)制汛闸,保存文本節(jié)點最初的文本蝙茶,后面更新根據(jù)最初的文本進(jìn)行替換更新。
    const vm = this.$vm
    let text = node.textContent
    const updaterFn = updater.textUpdater

    let value = text.replace(/\{\{(.*)\}\}/, compileUtil._getVMVal(vm, exp))
    updaterFn && updaterFn(node, value);

    new Watcher(vm, exp, function (value) {
      updaterFn && updaterFn(node, text.replace(/\{\{(.*)\}\}/, value));
    });
  },
  // ... 省略
};

指令集合處理

// 指令處理集合
const compileUtil = {
  text: function (node, vm, exp) {
    this.update(node, vm, exp, 'text');
  },
    // ... 省略
  update: function (node, vm, exp, dir) {
    // 針對不同的指令使用不同的函數(shù)渲染诸老、更新數(shù)據(jù)。
    const updaterFn = updater[dir + 'Updater'];
    // 這里取值别伏,然后進(jìn)行初次的內(nèi)容渲染
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    new Watcher(vm, exp, function (value, oldValue) {
      updaterFn && updaterFn(node, value, oldValue);
    });
  },
    // ... 省略
};
  
const updater = {
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
  },
  // ... 省略
};

3. 響應(yīng)式對象

initState

initState 方法主要是對 props蹄衷、methodsdata厘肮、computedwathcer 等屬性做了初始化操作愧口。這里我們主要實現(xiàn)對 datacomputed 的操作。

function initState(vm) {
  const opts = vm.$options
  // 初始化 data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
}

initData

主要實現(xiàn)以下兩個操作:

  1. 調(diào)用 observe 方法觀測整個 data 的變化类茂,把 data 也變成響應(yīng)式耍属,可以通過 vm._data.xxx 訪問到定義 data 返回函數(shù)中對應(yīng)的屬性。

  2. 對定義 data 函數(shù)返回對象的遍歷巩检,通過 proxy 把每一個值 vm._data.xxx 都代理到 vm.xxx 上厚骗。

function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' ?
    data.call(vm, vm) :
    data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[I]
    if (methods && hasOwn(methods, key)) {
      console.log(`Method "${key}" has already been defined as a data property.`, vm)
    }
    if (props && hasOwn(props, key)) {
      console.log(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm)
    } else if (!isReserved(key)) {
      // 數(shù)據(jù)代理,實現(xiàn) vm.xxx -> vm._data.xxx兢哭,相當(dāng)于 vm 上面多了 xxx 這個屬性
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true)
}

proxy

把每一個值 vm._data.xxx 都代理到 vm.xxx 上领舰。

這是一個公用的方法。這里我們只是對 data 定義對屬性做里代理迟螺。實際上 vue 還通過這個方法對 props 也做了代理冲秽,proxy(vm, '_props', key)

// 數(shù)據(jù)代理矩父,proxy(vm, '_data', key)锉桑。
function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function proxyGetter() {
      // initData 里把 vm._data 處理成響應(yīng)式對象。
      // 這里返回 this['_data'][key]窍株,實現(xiàn) vm[key] -> vm._data[key]
      return this[sourceKey][key]
    },
    set: function proxySetter(val) {
      // 這里修改 vm[key] 實際上是修改了 this['_data'][key]
      this[sourceKey][key] = val
    }
  })
}

observe

observe 的功能就是用來監(jiān)測數(shù)據(jù)的變化刨仑。

function observe(value) {
  if (!isObject(value)) {
    return
  }
  return new Observer(value);
}

Observer

Observer 是一個類,它的作用是給對象的屬性添加 getter 和 setter夹姥,用于依賴收集和派發(fā)更新

class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    this.walk(value)
  }
  walk (obj) {
    // 遍歷 data 對象的 key 調(diào)用 defineReactive 方法創(chuàng)建響應(yīng)式對象
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[I])
    }
  }
}

defineReactive

defineReactive 的功能就是定義一個響應(yīng)式對象,給對象動態(tài)添加 getter 和 setter辙诞,getter 做的事情是依賴收集辙售,setter 做的事情是派發(fā)更新。

function defineReactive (obj, key) {
  // 初始化 Dep飞涂,用于依賴收集
  const dep = new Dep()

  let val = obj[key]

  // 對子對象遞歸調(diào)用 observe 方法旦部,這樣就保證了無論 obj 的結(jié)構(gòu)多復(fù)雜祈搜,
  // 它的所有子屬性也能變成響應(yīng)式的對象,
  // 這樣我們訪問或修改 obj 中一個嵌套較深的屬性士八,也能觸發(fā) getter 和 setter容燕。
  // 使 foo.bar 等多層的對象也可以實現(xiàn)響應(yīng)式。
  let childOb = observe(val)
  // Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // Dep.target 指向 watcher
      if (Dep.target) {
        // 依賴收集婚度,每個使用到 data 里的值的地方蘸秘,都會調(diào)用一次 get,然后就會被收集到一個數(shù)組中蝗茁。
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    set: function reactiveSetter (newVal) {
      // 當(dāng)值沒有變化時醋虏,直接返回
      if (newVal === val) {
        return
      }
      // 對 val 設(shè)置新的
      val = newVal
      // 如果新傳入的值時一個對象,需要重新進(jìn)行 observe哮翘,給對象的屬性做響應(yīng)式處理颈嚼。
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

4.依賴收集、派發(fā)更新

Dep

Dep 是整個 getter 依賴收集的核心饭寺,這里需要特別注意的是它有一個靜態(tài)屬性 target阻课,這是一個全局唯一 Watcher,這是一個非常巧妙的設(shè)計艰匙,因為在同一時間只能有一個全局的 Watcher 被計算限煞,另外它的自身屬性 subsWatcher 的數(shù)組。

Dep 實際上就是對 Watcher 的一種管理旬薯,Dep 脫離 Watcher 單獨存在是沒有意義的晰骑。

class Dep {
  static target;
  constructor () {
    // 存放 watcher 的地方
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
    // 派發(fā)更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null

Watcher

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.expOrFn = expOrFn;
    this.depIds = {};
    // 判斷 expOrFn 是不是一個函數(shù),如果不是函數(shù)會通過 parsePath 把它變成一個函數(shù)绊序。
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // parsePath 把 expOrFn 變成一個函數(shù)
      this.getter = parsePath(expOrFn) || function noop (a, b, c) {}
    }
    // 取值硕舆,觸發(fā)依賴收集。
    this.value = this.get()
  }
  get() {
    // 這里 Dep.target 指向 watcher 本身骤公,然后會取值抚官,取值觸發(fā)對應(yīng)屬性的 getter 方法。
    // 此時 getter 方法里面使用的 Dep.target 就有值了阶捆。
    // 通過一系列的代碼執(zhí)行 dep.depend() -> Dep.target.addDep(dep) -> dep.addSub(watcher) 
    // 最后把 watcher 存到 subs 數(shù)組里凌节,完成依賴收集。
    // 最后把 Dep.target 刪除洒试,保證來 Dep.target 在同一時間內(nèi)只有唯一一個倍奢。
    Dep.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    Dep.target = null;
    return value
  }
  addDep(dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this);
      this.depIds[dep.id] = dep;
    }
  }
  update() {
    // this.value 是 watcher 緩存的值,用來與改變后的值進(jìn)行對比垒棋,如果前后值沒有變化卒煞,就不進(jìn)行更新。
    const value = this.get()
    const oldValue = this.value
    if (value !== oldValue) {
      // 緩存新的值叼架,下次操作用
      this.value = value
      // 以 vm 為 cb 的 this 值畔裕,調(diào)用 cb衣撬。
      // cb 就是 在 new watcher 使傳入的更新函數(shù)。會把新的值傳入通過更新函數(shù)扮饶,更新到視圖上具练。
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

三、過程分析

  1. new MVVM() 的時候甜无,首先扛点,會對 dataprops 毫蚓、computed 進(jìn)行初始化占键,使它們變成響應(yīng)式的對象。
  2. 響應(yīng)式是通過使用 Object.defineProperty 給對象的屬性設(shè)置 get元潘、set畔乙,為屬性提供 getter、setter 方法翩概,一旦對象擁有了 getter 和 setter牲距,我們可以簡單地把這個對象稱為響應(yīng)式對象。钥庇。
  3. 當(dāng)我們訪問了該屬性的時候會觸發(fā) getter 方法牍鞠,當(dāng)我們對該屬性做修改的時候會觸發(fā) setter 方法。
  4. 在 getter 方法里做依賴的收集评姨。因為在使用屬性的時候难述,就會觸發(fā) getter,這時就會把這個使用記錄起來吐句,后面屬性有改動的時候胁后,就會根據(jù)這個收集的記錄進(jìn)行更新。
  5. 在 setter 方法里做派發(fā)更新嗦枢。因為在對屬性做修改的時候會觸發(fā)這個setter攀芯,這時就可以根據(jù)之前在 getter 里面收集的記錄,去做對應(yīng)的更新文虏。
  6. getter 的實現(xiàn)中侣诺,是通過 Dep 實現(xiàn)依賴收集的。getter 方法中調(diào)用了 Dep.depend() 進(jìn)行收集氧秘,Dep.depend() 中又調(diào)用了 Dep.target.addDep(this) 年鸳。
  7. 這里 Dep.target 是個非常巧妙的設(shè)計,因為在同一時間 Dep.target 只指向一個 Watcher丸相,使得同一時間內(nèi)只能有一個全局的 Watcher 被計算阻星。
  8. Dep.target.addDep(this) 等于調(diào)用 Watcher.addDep(dep) ,里面又調(diào)用了 dep.addSub(this) 把這個全局唯一的 watcher 添加到 dep.subs 數(shù)組中,收集了起來妥箕,并且 watcher 本身也通過 depIds 收集持有的 Dep 實例。
  9. 上面只是定義了一個流程更舞,但是需要訪問數(shù)據(jù)對象才能觸發(fā) getter 使這個流程運轉(zhuǎn)起來畦幢。那什么時候觸發(fā)呢?
  10. Vue 會通過 compile 把模版編譯成 render 函數(shù)缆蝉,并在 render 函數(shù)中訪問數(shù)據(jù)對象觸發(fā) getter宇葱。這里我們是直接在 compile 的時候訪問數(shù)據(jù)對象觸發(fā) getter。
  11. compile 負(fù)責(zé)內(nèi)容的渲染與數(shù)據(jù)更新刊头。compile 編譯模版中的內(nèi)容黍瞧,把模版中的 {{xx}} 字符串替換成對應(yīng)的屬性值時會訪問數(shù)據(jù)對象觸發(fā) getter,不過此時還沒有 watcher原杂,沒有依賴收集印颤。
  12. compile 接下來會實例化 Watcher,實例化過程會再去取一次值穿肄,此時觸發(fā)到 getter 才會進(jìn)行依賴收集年局。具體看 Watcher 的 構(gòu)造函數(shù)與 get 方法實現(xiàn)。
  13. 到這里咸产,頁面渲染完成矢否,依賴收集也完成。
  14. 接下來會監(jiān)控數(shù)據(jù)的變化脑溢,數(shù)據(jù)如果發(fā)生變化僵朗,就會觸發(fā)屬性值的 setter 方法,setter 方法除了把值設(shè)置為新的值之外屑彻,還會進(jìn)行派發(fā)更新验庙。執(zhí)行 dep.notify(),循環(huán)調(diào)用 subs 里面保存的 watcherupdate 方法進(jìn)行更新酱酬。

獲取視頻教程+源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壶谒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子膳沽,更是在濱河造成了極大的恐慌汗菜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挑社,死亡現(xiàn)場離奇詭異陨界,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)痛阻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門菌瘪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事俏扩∶庸ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵录淡,是天一觀的道長捌木。 經(jīng)常有香客問我,道長嫉戚,這世上最難降的妖魔是什么刨裆? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮彬檀,結(jié)果婚禮上帆啃,老公的妹妹穿的比我還像新娘。我一直安慰自己窍帝,他們只是感情好努潘,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盯桦,像睡著了一般慈俯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拥峦,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天贴膘,我揣著相機(jī)與錄音,去河邊找鬼略号。 笑死刑峡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的玄柠。 我是一名探鬼主播突梦,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼羽利!你這毒婦竟也來了宫患?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤这弧,失蹤者是張志新(化名)和其女友劉穎娃闲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匾浪,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡皇帮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛋辈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片属拾。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出渐白,到底是詐尸還是另有隱情尊浓,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布纯衍,位于F島的核電站眠砾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏托酸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一柒巫、第九天 我趴在偏房一處隱蔽的房頂上張望励堡。 院中可真熱鬧,春花似錦堡掏、人聲如沸应结。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹅龄。三九已至,卻和暖如春亭畜,著一層夾襖步出監(jiān)牢的瞬間扮休,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工拴鸵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留玷坠,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓劲藐,卻偏偏與公主長得像八堡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子聘芜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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