Vue源碼解析一——骨架梳理

大家都知道烤黍,閱讀源碼可以幫助自己成長。源碼解析的文章也看了不少傻盟,但是好記性不如爛筆頭速蕊,看過的東西過段時間就忘的差不多了,所以還是決定自己動手記一記娘赴。

首先看下項目目錄规哲,大致知道每個文件夾下面都是干什么的


Vue.png

當我們閱讀一個項目源碼的時候败去,首先看它的package.json文件纠修,這里包含了項目的依賴脖岛、執(zhí)行腳本等褥伴,可以幫助我們快速找到項目的入口乓序。

我們來看幾個重要字段:

// main和module指定了加載的入口文件裂逐,它們都指向運行時版的Vue钩骇,
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",

當打包工具遇到該模塊時:

  1. 如果已經(jīng)支持pkg.module字段蜀铲,會優(yōu)先使用es6模塊規(guī)范的版本泛啸,這樣可以啟用tree shaking機制
  2. 否則根據(jù)main字段的配置加載绿语,使用已經(jīng)編譯成CommonJS規(guī)范的版本。

webpack2+和rollup都已經(jīng)支持pkg.module, 會根據(jù)module字段的配置進行加載

接下來看一下scripts里面部分腳本配置:

"scripts": {
  // 構(gòu)建完整版umd模塊的Vue
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
  // 構(gòu)建運行時cjs模塊的Vue
  "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
  // 構(gòu)建運行時es模塊的Vue
  "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
  // 構(gòu)建web-server-renderer包
  "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
  "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer"
},

umd讓我們可以直接用script標簽來引用Vue

cjs形式的模塊是為browserify 和 webpack 1 提供的候址,他們在加載模塊的時候不能直接加載ES Module

webpack2+ 以及 Rollup可以直接加載ES Module吕粹,es形式的模塊是為它們服務(wù)的

接下來,我們將基于dev腳本進行分析

當我們執(zhí)行npm run dev命令時岗仑,

    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

可以看到配置文件是scripts/config.js匹耕,傳給配置文件的TARGET變量的值是‘web-full-dev’。

在配置文件的最后荠雕,是這樣一段代碼:

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

因為process.env.TARGET有值稳其,所以執(zhí)行的是if里面的代碼。根據(jù)process.env.TARGET === 'web-full-dev', 我們看到這樣一段配置:

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
    dest: resolve('dist/vue.js'), // 最終輸出文件
    format: 'umd', // umd模塊
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

現(xiàn)在我們知道了入口文件是'web/entry-runtime-with-compiler.js'舞虱,但是web是指的哪一個目錄呢欢际?在scripts下面有一個alias.js文件,里面定義了一些別名:

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  entries: resolve('src/entries'),
  sfc: resolve('src/sfc')
}

可以看到web是指的'src/platforms/web'矾兜,所以入口文件的全路徑就是src/platforms/web/entry-runtime-with-compiler.js

我們使用Vue的時候损趋,是用new關(guān)鍵字進行調(diào)用的,這說明Vue是一個構(gòu)造函數(shù),接下來我們就從入口文件開始扒一扒Vue構(gòu)造函數(shù)是咋個情況浑槽。

尋找Vue構(gòu)造函數(shù)的位置

打開入口文件src/platforms/web/entry-runtime-with-compiler.js蒋失,我們看到這樣一句代碼

import Vue from './runtime/index'

這說明Vue是從別的文件引進來的,接著打開./runtime/index文件桐玻,看到

import Vue from 'core/index'

說明這里也不是Vue的出生地篙挽,接著尋找。打開core/index镊靴,根據(jù)別名配置可以知道铣卡,core是指的'src/core'目錄。Vue依然是引入的

import Vue from './instance/index'

沒辦法偏竟,接著找煮落。在./instance/index下面看到這樣一段代碼

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

長吁一口氣,Vue構(gòu)造函數(shù)終于找到源頭了踊谋。最后我們再理一下這個路徑

src/platforms/web/entry-runtime-with-compiler.js.

——> src/platforms/web/runtime/index.js

——> src/core/index.js

——> src/core/instance/index.js

接下來我們從出生地開始一一來看

Vue構(gòu)造函數(shù)——實例屬性和方法

來看一下src/core/instance/index.js文件中的全部代碼:

/**
 * 在原型上添加了各種屬性和方法
 */
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// 定義Vue構(gòu)造函數(shù)
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
// 在Vue的原型上添加了_init方法蝉仇。在執(zhí)行new Vue()的時候,this._init(options)被執(zhí)行
stateMixin(Vue)
// 在vue的原型上定義了屬性: $data殖蚕、$props轿衔,方法:$set、$delete睦疫、$watch
eventsMixin(Vue)
// 在原型上添加了四個方法: $on $once $off $emit
lifecycleMixin(Vue)
// 在Vue.prototye上添加了三個方法:_update $forceUpdate $destory
renderMixin(Vue)
// 在原型上添加了方法:$nextTick _render _o _n _s _l _t _q _i _m _f _k _b _v _e _u _g _d _p

export default Vue

該文件主要是定義了Vue構(gòu)造函數(shù)害驹,然后又以Vue為參數(shù),執(zhí)行了initMixin蛤育、stateMixin裙秋、eventsMixin、lifecycleMixin缨伊、renderMixin這五個方法摘刑。

Vue構(gòu)造函數(shù)首先檢查了是不是用new關(guān)鍵字調(diào)用的,然后調(diào)用了_init方法刻坊。

接下來五個方法分別在Vue的原型上添加了各種屬性和方法枷恕。首先來看initMixin

initMixin

打開'./init'文件,找到initMixin方法谭胚,發(fā)現(xiàn)它其實只做了一件事:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ...
  }
}

就是在Vue.prototype上掛載了_init方法徐块,在執(zhí)行new Vue()的時候,該方法會執(zhí)行灾而。

stateMixin

export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') { // 不是生產(chǎn)環(huán)境胡控,設(shè)置set
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  // $data 和 $props是只讀屬性
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ...
  }

這個方法首先在Vue.prototype上定義了兩個只讀屬性$data$props。為什么是只讀屬性呢旁趟?因為為屬性設(shè)置set的時候有一個判斷昼激,不能是生產(chǎn)環(huán)境。

然后在原型上定義了三個方法:$set, $delete, $watch

eventsMixin

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}

  Vue.prototype.$once = function (event: string, fn: Function): Component {}

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}

  Vue.prototype.$emit = function (event: string): Component {}
}

這里面是在原型上掛載了四個方法,這幾個方法平時也都經(jīng)常用到橙困,肯定很熟悉

lifecycleMixin

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}

  Vue.prototype.$forceUpdate = function () {}

  Vue.prototype.$destroy = function () {}
}

添加了三個生命周期相關(guān)的實例方法:

  • _update:
  • $forceUpdate: 迫使Vue實例重新渲染瞧掺,包括其下的子組件
  • $destory: 完全銷毀一個實例, 觸發(fā)生命周期beforeDestroy和destroyed

renderMixin

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {}
}

首先是以Vue.prototype為參數(shù)調(diào)用了installRenderHelpers方法,來看一下這個方法干了啥:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

也是在原型上掛載了各種方法, 用于構(gòu)造render函數(shù)凡傅。

之后又在原型上掛載了兩個實例方法$nextTick_render

至此我們大致了解了instance/index.js里面的內(nèi)容辟狈,就是包裝了Vue.prototyp,在其上掛載了各種屬性和方法夏跷。

Vue構(gòu)造函數(shù)——掛載全局API

接下來來看/src/core/index文件

/** 
* 添加全局API哼转,在原型上添加了兩個屬性$isServer和$ssrContext,加了version版本屬性
*/
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 在 Vue 構(gòu)造函數(shù)上添加全局的API
initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

// 存儲了當前Vue的版本號
Vue.version = '__VERSION__'

export default Vue

首先是導(dǎo)入了構(gòu)造函數(shù)Vue和其他三個變量槽华,接下來就是以Vue構(gòu)造函數(shù)為參數(shù)調(diào)用了initGlobalAPI方法释簿,該方法來自./global-api/index。我們先把下面的內(nèi)容看完再回過頭來分析該方法硼莽。

接下來是在Vue.prototype上面掛載了兩個只讀屬性$isServer$ssrContext。之后又在Vue構(gòu)造函數(shù)上添加了FunctionalRenderContext屬性煮纵,根據(jù)注釋知道該屬性是在ssr中用到的懂鸵。

最后在Vue構(gòu)造函數(shù)上添加了靜態(tài)屬性version,其值是__VERSION__行疏,這是個什么鬼匆光?打開/scripts/config.js,可以看到這么一句代碼:

__VERSION__: version

而version的值在文件最上面可以看到:

process.env.VERSION || require('../package.json').version

所以最終的值就是Vue的版本酿联。

我們再回過頭來看一下initGlobalAPI函數(shù)终息,從函數(shù)名可以猜出它應(yīng)該是定義全局API的,其實也就是這樣贞让。

先看前部分代碼

// config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef) // 只讀屬性

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 上面意思就是輕易不要用周崭,有風險
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

先是定義了只讀屬性config。接著定義了util屬性喳张,并且在util上掛載了四個方法续镇。只不過util以及它下面的方法不被視為公共API的一部分,要避免使用销部,除非你可以控制風險摸航。

接著就是在Vue上添加了四個屬性:set、delete舅桩、nextTick酱虎、observable.

然后定義了一個空對象options

Vue.options = Object.create(null)

之后通過循環(huán)填充屬性:

ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

ASSET_TYPES的值通過查找對應(yīng)文件后知道為['component', 'directive', 'filter'],所以循環(huán)之后options對象變?yōu)椋?/p>

Vue.options = {
  components: Object.create(null),
  directives: Object.create(null),
  filters: Object.create(null)
}
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

這是在options上添加了_base屬性

接下來是這句代碼

// 將builtInComponents的屬性混合到Vue.options.components中
  extend(Vue.options.components, builtInComponents)

extend 來自于 shared/util.js 文件,代碼也很簡單

/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

builtInComponents 來自于 core/components/index.js 文件

import KeepAlive from './keep-alive'

export default {
  KeepAlive
}

現(xiàn)在為止擂涛,Vue.options變成

Vue.options = {
    components: {
        KeepAlive
    },
    directives: Object.create(null),
    filters: Object.create(null),
    _base: Vue
  }

在函數(shù)的最后读串,調(diào)用了四個方法:

  // 在Vue構(gòu)造函數(shù)上添加use方法,Vue.use()用來安裝Vue插件
  initUse(Vue)
  // 添加全局API:Vue.mixin()
  initMixin(Vue)
  // 添加Vue.cid靜態(tài)屬性 和 Vue.extend 靜態(tài)方法
  initExtend(Vue)
  // 添加靜態(tài)方法:Vue.component Vue.directive Vue.filter
  // 全局注冊組件、指令爹土、過濾器
  initAssetRegisters(Vue)

我們先大致了解這幾個方法的作用甥雕,至于具體實現(xiàn)以后再詳細分析。

第二個階段大體就了解完了胀茵,就是掛載靜態(tài)屬性和方法社露。

Vue平臺化包裝

接下來來看platforms/web/runtime/index.js文件,我們之前看的兩個文件是在core目錄下的琼娘,是Vue的核心文件峭弟,與平臺無關(guān)的。platforms下面的就是針對特定平臺對Vue進行包裝脱拼。主要分兩個平臺:web和weex, 我們看的是web平臺下的內(nèi)容瞒瘸。

首先是安裝特定平臺的工具函數(shù)

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

Vue.config我們之前見過,它代理的是/src/core/config.js文件拋出的內(nèi)容熄浓,現(xiàn)在是重寫了其中部分屬性情臭。

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

這是安裝平臺運行時的指令和組件。extend的作用我們都已經(jīng)知道了赌蔑。來看一下platformDirectives和platformComponents的內(nèi)容俯在。

platformDirectives:

import model from './model'
import show from './show'

export default {
  model,
  show
}

platformComponents:

import Transition from './transition'
import TransitionGroup from './transition-group'

export default {
  Transition,
  TransitionGroup
}

Vue.options之前已經(jīng)有過包裝,經(jīng)過這兩句代碼之后變成:

Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: Object.create(null),
    _base: Vue
}

繼續(xù)看下面的代碼

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

這是添加了兩個實例方法:__patch__$mount

看完之后我們就知道了該文件的作用

  1. 設(shè)置平臺化的Vue.config
  2. 在Vue.options上混合了兩個指令:modelshow
  3. 在Vue.options上混合了兩個組件:TransitionTransitionGroup
  4. 在Vue.prototye上添加了兩個方法:__patch__$mount

compiler

到目前為止娃惯,運行時版本的Vue已經(jīng)構(gòu)造完了跷乐。但是我們的入口是entry-runtime-with-compiler.js文件,從文件名可以看出來這里是多了一個compiler趾浅。我們來看看這個文件吧

// 獲取擁有指定ID屬性的元素的innerHTML
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( // 重寫了$mount方法
  el?: string | Element,
  hydrating?: boolean
): Component {}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

// 添加compile全局API
Vue.compile = compileToFunctions

export default Vue

這個文件主要是重寫了Vue.prototype.$mount方法愕提,添加了Vue.compile全局API

以上,我們從Vue構(gòu)造函數(shù)入手皿哨,大致梳理了項目的脈絡(luò)浅侨。理清楚了大體流程,之后再慢慢探索細節(jié)证膨。

最后編輯于
?著作權(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
  • 文/潘曉璐 我一進店門盖高,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慎陵,“玉大人,你說我怎么就攤上這事喻奥∠Γ” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵撞蚕,是天一觀的道長润梯。 經(jīng)常有香客問我,道長甥厦,這世上最難降的妖魔是什么纺铭? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮刀疙,結(jié)果婚禮上舶赔,老公的妹妹穿的比我還像新娘。我一直安慰自己谦秧,他們只是感情好竟纳,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疚鲤,像睡著了一般锥累。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上石咬,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音卖哎,去河邊找鬼鬼悠。 笑死,一個胖子當著我的面吹牛亏娜,可吹牛的內(nèi)容都是我干的焕窝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼维贺,長吁一口氣:“原來是場噩夢啊……” “哼它掂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起溯泣,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤虐秋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后垃沦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體客给,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年肢簿,在試婚紗的時候發(fā)現(xiàn)自己被綠了靶剑。 大學時的朋友給我發(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
  • 正文 我出身青樓再榄,卻偏偏與公主長得像狡刘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子困鸥,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355