Vue源碼解析二——從一個小例子開始逐步分析

每個Vue應(yīng)用都是從創(chuàng)建Vue實(shí)例開始的兔院,這里我們就以一個簡單的例子為基礎(chǔ),慢慢深究Vue的實(shí)現(xiàn)細(xì)節(jié)。

<div id="app">{{ a }}</div>
var vm = new Vue({
  el: '#app',
  data: { a: 1 }
})

當(dāng)我們重新設(shè)置a屬性時(vm.a = 2)眶蕉,視圖上顯示的值也會變成2。這么簡單的例子大家都知道啦唧躲,現(xiàn)在就看看使用Vue構(gòu)造函數(shù)初始化的時候都發(fā)生了什么造挽。

打開/src/core/instance/index.js文件,看到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)
}

由此可知首先執(zhí)行了this._init(options)代碼弄痹,_init方法在 src/core/instance/init.js文件中被添加到了Vue原型上饭入,我們看看該方法做了什么。

const vm: Component = this
// a uid
vm._uid = uid++

首先是定義了vm肛真,它的值就是this谐丢,即當(dāng)前實(shí)例。接著定義了一個實(shí)例屬性_uid蚓让,它是Vue組件的唯一標(biāo)識乾忱,每實(shí)例化一個Vue組件就會遞增。

接下來是在非生產(chǎn)環(huán)境下可以測試性能的一段代碼:

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
}

...

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    vm._name = formatComponentName(vm, false)
    mark(endTag)
    measure(`vue ${vm._name} init`, startTag, endTag)
}

省略了中間的代碼历极。這段代碼的執(zhí)行條件是:非生產(chǎn)環(huán)境窄瘟,config.performance為true 和 mark都存在的情況下。官方提供了performance的全局API趟卸。mark和measure在core/util/perf.js文件中蹄葱,其實(shí)就是window.performance.mark和window.performance.measure. 組件初始化的性能追蹤就是在代碼的開頭和結(jié)尾分別用mark打上標(biāo)記,然后通過measure函數(shù)對兩個mark進(jìn)行性能計算锄列。

再看看中間代碼新蟆,也就是被性能追蹤的代碼:

// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
    )
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
} else {
    vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

先是設(shè)置了_isVue實(shí)例屬性,作為一個標(biāo)志避免Vue實(shí)例被響應(yīng)系統(tǒng)觀測右蕊。

接下來是合并選項的處理琼稻,我們并沒有使用_isComponent屬性,所以上面的代碼會走else分支饶囚,掛載了實(shí)例屬性$options, 該屬性的生成通過調(diào)用了mergeOptions方法帕翻,接下來我們看看mergeOptions方法都干了些什么。

mergeOptions 函數(shù)來自于 core/util/options.js 文件, 該函數(shù)接受三個參數(shù)萝风。先來看一下_init函數(shù)中調(diào)用該函數(shù)時傳遞的參數(shù)分別是什么嘀掸。

vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)

后兩個參數(shù)都好理解,options是我們實(shí)例化時傳過來的參數(shù)

{
  el: '#app',
  data: { a: 1 }
}

vm就是當(dāng)前實(shí)例规惰。

重點(diǎn)看一下第一個參數(shù)睬塌,是調(diào)用方法生成的resolveConstructorOptions(vm.constructor)

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

傳的參數(shù)是vm.constructor,在我們例子中就是Vue構(gòu)造函數(shù),因為我們是直接調(diào)用的Vue創(chuàng)建的實(shí)例揩晴。那什么時候不是Vue構(gòu)造函數(shù)呢勋陪,在用Vue.extend()去創(chuàng)建子類,再用子類構(gòu)造實(shí)例的時候硫兰,vm.constructor就是子類而不是Vue構(gòu)造函數(shù)了诅愚。例如在官方文檔上的例子

// 創(chuàng)建構(gòu)造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 創(chuàng)建 Profile 實(shí)例,并掛載到一個元素上劫映。
new Profile().$mount('#mount-point')

vm.constructor就是Profile违孝。

再看if語句塊,是在Ctor.super為真的情況下執(zhí)行泳赋,super是子類才有的屬性雌桑,所以在我們的例子中是不執(zhí)行的,直接返回options祖今,即Vue.options, 它的值如下:

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

不記得options是如何形成的可以看一下Vue源碼解析一——骨架梳理〕镅啵現(xiàn)在三個參數(shù)已經(jīng)搞清楚了,就來看看mergeOptions方法發(fā)生了什么吧衅鹿。

檢查組件名是否合法

mergeOptions方法在core/util/options.js文件中撒踪,我們找到該方法,首先看一下方法上方的注釋:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */

合并兩個選項對象為一個新的對象大渤。在實(shí)例化和繼承中使用的核心實(shí)用程序制妄。實(shí)例化就是調(diào)用_init方法的時候,繼承也就是使用Vue.extend的時候”萌現(xiàn)在我們知道了該方法的作用耕捞,就來看一下該方法的具體實(shí)現(xiàn)吧

if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
}

在非生產(chǎn)環(huán)境下,會去校驗組件的名字是否合法烫幕,checkComponents函數(shù)就是用來干這個的俺抽,該函數(shù)也在當(dāng)前文件中,找到該函數(shù):

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

一個for in循環(huán)遍歷options.components较曼,以子組件的名字為參數(shù)調(diào)用validateComponentName方法磷斧,所以該方法才是檢測組件名是否合法的具體實(shí)現(xiàn)。源碼如下:

export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

該方法由兩個if語句塊組成捷犹,要想組件名合法弛饭,必須滿足這兩個if條件:

  1. 正則表達(dá)式/^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$/
  2. isBuiltInTag(name) || config.isReservedTag(name) 條件不成立

對于條件一就是要使用符合html5規(guī)范中的有效自定義元素名稱

條件二是使用了兩個方法來檢測的,isBuiltInTag方法用來檢測是否是內(nèi)置標(biāo)簽萍歉,在shared/util.js文件中定義

/**
 * Check if a tag is a built-in tag.
 */
export const isBuiltInTag = makeMap('slot,component', true)

isBuiltInTag方法是調(diào)用makeMap()生成的侣颂,看一下makeMap的定義:

/**
 * Make a map and return a function for checking if a key
 * is in that map.
 */
export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

該方法最后返回一個函數(shù),函數(shù)接收一個參數(shù)枪孩,如果參數(shù)在map中就返回true憔晒,否則返回undefined藻肄。map是根據(jù)調(diào)用makeMap方法時傳入的參數(shù)生成的,按照來處來看拒担,也就是

map = { slot: true, component: true }

由此可知slotcomponent 是作為Vue的內(nèi)置標(biāo)簽而存在的嘹屯,我們的組件命名不能使用它們。

還有一個方法config.isReservedTagcore/config.js文件中定義澎蛛,在platforms/web/runtime/index.js文件中被覆蓋

Vue.config.isReservedTag = isReservedTag

isReservedTag方法在platforms/web/util/element.js文件中抚垄,

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}

就是檢測是否是規(guī)定的html標(biāo)簽和svg標(biāo)簽蜕窿。到此組件名是否合法的檢測就結(jié)束了谋逻。

if (typeof child === 'function') {
    child = child.options
}

這里是一個判斷,如果child是一個function桐经,就取它的options靜態(tài)屬性毁兆。什么函數(shù)具有options屬性呢?Vue構(gòu)造函數(shù)和使用Vue.extend()創(chuàng)建的'子類'阴挣,這就允許我們在進(jìn)行選項合并的時候气堕,去合并一個 Vue 實(shí)例構(gòu)造者的選項了。

規(guī)范化Props

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

這是三個規(guī)范化選項的函數(shù)調(diào)用畔咧,分別是針對props, inject, directives茎芭。為什么會有規(guī)范化選項這一步呢?因為我們在使用選項的時候可以有多種不同的用法誓沸,比如props, 既可以是字符串?dāng)?shù)組也可以是對象:

props: ['test1', 'test2']

props: {
    test1: String,
    test2: {
        type: String,
        default: ''
    }
}

這方便了我們使用梅桩,但是Vue要對選項進(jìn)行處理,多種形式定然增加了復(fù)雜度拜隧,所以要處理成一種格式宿百,這就是該函數(shù)的作用。

我們分別來看具體是怎么規(guī)范化的洪添,首先是函數(shù)normalizeProps:

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    
  } else if (isPlainObject(props)) {
    
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

根據(jù)注釋我們知道props最后被規(guī)范成對象的形式了垦页。先大體看一下函數(shù)的結(jié)構(gòu):

  • 先是判斷props是否存在,如果不存在直接返回
  • if語句處理數(shù)組props
  • else if語句塊處理對象props
  • 最后如果既不是數(shù)組也不對象干奢,還不是生成環(huán)境痊焊,就發(fā)出類型錯誤的警告

數(shù)組類型的props是如何處理的呢?看一下代碼:

i = props.length
while (i--) {
    val = props[i]
    if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
    } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
    }
}

使用while循環(huán)處理每一項忿峻,如果是字符串宋光,先用camelize函數(shù)轉(zhuǎn)了一下該字符串,然后存儲在了res中炭菌,其值是{ type: null } 罪佳。camelize函數(shù)定義在shared/util.js中,其作用就是把連字符格式的字符串轉(zhuǎn)成駝峰式的黑低。比如:

test-a // testA

如果不是字符串類型就發(fā)出警告赘艳,所以數(shù)組格式的props中元素必須是字符串酌毡。

數(shù)組格式的規(guī)范化我們已經(jīng)了解了,如果我們傳的是

props: ['test-a', 'test2']

規(guī)范化之后就變成:

props: {
    testA: { type: null },
    test2: { type: null }
}

再來看看對象props是如何規(guī)范化的:

for (const key in props) {
    val = props[key]
    name = camelize(key)
    res[name] = isPlainObject(val)
        ? val
        : { type: val }
}

我們之前舉例說過props是對象的話它的屬性值有兩種寫法蕾管,一種屬性值直接是類型枷踏,還有一種屬性值是對象。這里的處理是如果是對象的不做處理掰曾,是類型的話就把它作為type的值旭蠕。所以如果我們傳的是:

props: {
    test1: String,
    test2: {
        type: String,
        default: ''
    }
}

規(guī)范化之后變成:

props: {
    test1: { type: String },
    test2: {
        type: String,
        default: ''
    }
}

這樣我們就了解了Vue是如何規(guī)范化Props的了

規(guī)范化inject

inject選項不常使用,我們先來看看官方文檔的介紹

// 父級組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

在子組件中并沒有定義foo屬性卻可以使用旷坦,就是因為使用inject注入了這個屬性掏熬,而這個屬性的值是來源于父組件。和props一樣秒梅,inject既可以是數(shù)組也可以是對象:

inject: ['foo']
inject: { foo },
inject: {
    bar: {
        from: 'foo',
        default: '--'
    }
}

為了方便處理旗芬,Vue也把它規(guī)范成了一種格式,就是對象:

/**
 * Normalize all injections into Object-based format
 */
function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    
  } else if (isPlainObject(inject)) {
    
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

函數(shù)開頭首先判斷inject屬性是否存在捆蜀,如果沒有傳就直接返回疮丛。

接著是數(shù)組類型的處理

for (let i = 0; i < inject.length; i++) {
    normalized[inject[i]] = { from: inject[i] }
}

for循環(huán)遍歷整個數(shù)組,將元素的值作為key辆它,{ from: inject[i] }作為值誊薄。所以如果是

inject: ['foo']

規(guī)范化之后:

inject: { foo: { from: 'foo' } }

然后是處理對象類型的inject:

for (const key in inject) {
    const val = inject[key]
    normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
}

使用for in循環(huán)遍歷對象,依然使用原來的key作為key锰茉,值的話要處理一下呢蔫,如果原來的值是對象,就用extend函數(shù)把{ from: key }和val混合一下洞辣,否則就用val作為from的值咐刨。

所以如果我們傳入的值是:

inject: {
    foo,
    bar: {
        from: 'foo',
        default: '--'
    }
}

處理之后變成:

inject: {
    foo: { from: 'foo' },
    bar: {
        from: 'foo',
        default: '--'
    }
}

最后,如果傳入的既不是數(shù)組也不是對象扬霜,在非生產(chǎn)環(huán)境下就會發(fā)出警告定鸟。

規(guī)范化Directives

/**
 * Normalize raw function directives into object format.
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

根據(jù)官方文檔自定義指令的介紹,我們知道注冊指令有函數(shù)和對象兩種形式:

directives: {
    'color-swatch': function (el, binding) {
        el.style.backgroundColor = binding.value
    },
    'color-swatch1': {
        bind: function (el, binding) {
            el.style.backgroundColor = binding.value
        }
    }
}

該方法就是要把第一種規(guī)范化成對象著瓶。

看一下方法體联予,for in 循環(huán)遍歷所有指令,如果值是函數(shù)類型材原,則把該值作為bind和update屬性的值沸久。所以第一種形式規(guī)范化之后就變成:

directives: {
    'color-swatch': {
        bind: function (el, binding) {
            el.style.backgroundColor = binding.value
        },
        update: function (el, binding) {
            el.style.backgroundColor = binding.value
        }
    }
}

現(xiàn)在我們就了解了三個用于規(guī)范化選項的函數(shù)的作用了。

規(guī)范化選項之后是這樣一段代碼:

// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
}

當(dāng)child是原始選項對象即沒有_base屬性時余蟹,進(jìn)行extendsmixins選項的處理卷胯。

如果child.extends存在,就遞歸調(diào)用mergeOptions函數(shù)將parent和child.extends進(jìn)行合并威酒,并將返回值賦給parent窑睁。

如果child.mixins存在挺峡,for循環(huán)遍歷child.mixins,也是遞歸調(diào)用mergeOptions函數(shù)將parent和每一項元素進(jìn)行合并担钮,并更新parent橱赠。

mergeOptions函數(shù)我們還沒有看完鼻听,先繼續(xù)往下看箭昵,這里造成的影響先不追究。之前所做的處理都是前奏狠轻,還沒有涉及選項合并苏遥,是為選項合并所做的鋪墊饼拍。接下來我們來看選項合并的處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市暖眼,隨后出現(xiàn)的幾起案子惕耕,更是在濱河造成了極大的恐慌纺裁,老刑警劉巖诫肠,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欺缘,居然都是意外死亡栋豫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門谚殊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧鸯,“玉大人,你說我怎么就攤上這事嫩絮〈灾” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵剿干,是天一觀的道長蜂怎。 經(jīng)常有香客問我,道長置尔,這世上最難降的妖魔是什么杠步? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮榜轿,結(jié)果婚禮上幽歼,老公的妹妹穿的比我還像新娘。我一直安慰自己谬盐,他們只是感情好甸私,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著飞傀,像睡著了一般皇型。 火紅的嫁衣襯著肌膚如雪泣刹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天犀被,我揣著相機(jī)與錄音椅您,去河邊找鬼。 笑死寡键,一個胖子當(dāng)著我的面吹牛掀泳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播西轩,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼员舵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了藕畔?” 一聲冷哼從身側(cè)響起马僻,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎注服,沒想到半個月后韭邓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡溶弟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年女淑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辜御。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸭你,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出擒权,到底是詐尸還是另有隱情袱巨,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布碳抄,位于F島的核電站愉老,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纳鼎。R本人自食惡果不足惜俺夕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贱鄙。 院中可真熱鬧劝贸,春花似錦、人聲如沸逗宁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞎颗。三九已至件甥,卻和暖如春捌议,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背引有。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工瓣颅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人譬正。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓宫补,卻偏偏與公主長得像,于是被迫代替她去往敵國和親曾我。 傳聞我的和親對象是個殘疾皇子粉怕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

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