Vue原理解析(七):全面深入理解響應(yīng)式原理(下)-數(shù)組進階篇

上一篇:全面深入理解響應(yīng)式原理(上)-對象基礎(chǔ)篇

  sayHi(friend)
  
  function sayHi(friend) {
    if(friend.status === '不太理解響應(yīng)式且還沒有看過上一篇') {
      console.log(`
        建議看下上一篇脓鹃,因為算是響應(yīng)式的基礎(chǔ)了啃匿,
        不然可能這篇看起來會費點勁吟逝。
      `)
    } else if(friend.status === '之前看過上一篇了') {
      console.log(`
        也可以瞟一眼瘦赫,為了和這一篇相契合丽啡,部分地方做了增刪改辱匿。
      `)
    } else if(friend.status === '我是大牛刃永,就來看看你理解的怎么樣') {
      console.log(`
        大佬货矮!里邊請~
      `)
    }
  }

我們首先來看下改變數(shù)組的兩種方式:

export default {
  data() {
    list: [1, 2, 3]
  },
  methods: {
    changeArr1() {  // 方式一:重新賦值
      this.list = [4, 5, 6]
    },
    changeArr2() {  // 方式二:方法改變
      this.list.push(7)
    }
  }
}

對于這兩種改變數(shù)據(jù)的方式,vue內(nèi)部的實現(xiàn)并不相同斯够。

方式一:重新賦值

  • 實現(xiàn)原理和對象是一樣的囚玫,再vm._render()時有用到list,就將依賴收集起來雳刺,重新賦值后走對象派發(fā)更新的那一套劫灶。

方式二:方法改變

  • 走對象的那一套就不行了,因為并不是重新賦值掖桦,雖然改變了數(shù)組自身但并不會觸發(fā)set本昏,原有的響應(yīng)式系統(tǒng)根本感知不到,所以我們接下來就分析枪汪,vue是如何解決使用數(shù)組方法改變自身觸發(fā)視圖的涌穆。

Dep收集依賴的位置

上一篇它的聲音并不大怔昨,現(xiàn)在我們來重新認識它。Dep類的主要作用就是管理依賴宿稀,在響應(yīng)式系統(tǒng)中會有兩個地方要實例化它趁舀,當然它們都會進行依賴的收集,首先是之前具體包裝的時候:

function defineReactive(obj, key, val) {
  const dep = new Dep()  // 自動依賴管理器
  ...
  Object.defineProperty(obj, key, {
    get() {...},
    set() {...}
  })
}

這里它會對每個讀取到的key都進行依賴收集祝沸,無論是對象/數(shù)組/原始類型矮烹,如果是通過重新賦值觸發(fā)set就會使用這里收集到的依賴進行更新,筆者這里就把它命名為自動依賴管理器罩锐,方便和之后的區(qū)分奉狈。

還有一個地方也會對它進行實例化就是Observer類中:

class Observer {
  constructor(value) {
    this.dep = new Dep() //  手動依賴管理器
    ...
  }
}

這個依賴管理器并不能通過set觸發(fā),而且是只會收集對象/數(shù)組的依賴涩惑。也就是說對象的依賴會被收集兩次仁期,一次在自動依賴管理器內(nèi),一次在這里竭恬,為什么要收集兩次跛蛋,本章之后說明。而最重要的是數(shù)組使用方法改變自身去觸發(fā)更新的依賴就是再這收集的痊硕,這個前提還是很有必要交代下的赊级。

數(shù)組的響應(yīng)式原理

數(shù)組響應(yīng)式數(shù)據(jù)的創(chuàng)建

數(shù)組示例:
export default {
  data() {
    return {
      list: [{
        name: 'cc',
        sex: 'man'
      }, {
        name: 'ww',
        sex: 'woman'
      }]
    }
  }
}

流程開始還是執(zhí)行observe方法,接下來我們更加詳細分析響應(yīng)式系統(tǒng):

function observe(value) {
  if (!isObject(value) { //不是數(shù)組或?qū)ο蟛沓瘢僖?    return
  }
  
  let ob
  if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  // 避免重復(fù)包裝
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

只要是響應(yīng)式的數(shù)據(jù)都會有一個__ob__的屬性此衅,它是在Observer類中掛載的,如果已經(jīng)有__ob__屬性就直接賦值給ob亭螟,不會再次去創(chuàng)建Observer實例,避免重復(fù)包裝骑歹。首次肯定沒__ob__屬性了预烙,所以再重新看下Observer類的定義:

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()  // 手動依賴管理器
    
    def(value, '__ob__', this)  // 掛載__ob__屬性,三個參數(shù)
    ...
  }
}

現(xiàn)在看Observer類會豐富很多道媚,首先定義一個手動依賴管理器扁掸,然后掛載一個不可枚舉的__ob__屬性到傳入的參數(shù)下,表示它的一個響應(yīng)式的數(shù)據(jù)最域,而且__ob__的值就是當前Observer類的實例谴分,它擁有實例上的所有屬性和方法,這很重要镀脂,我們接下來看下def是如何完成屬性掛載的:

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

其實就是一個簡單的封裝牺蹄,第四個參數(shù)不傳,enumerable項就是不可枚舉的了薄翅。接著看Observer類的定義:

class Observer {
  constructor(value) {
    ...
    if (Array.isArray(value)) {  // 數(shù)組
      ...
    } else {  // 對象
      this.walk(value)  // {list: [{...}, {...}]}
    }
  }
  
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

首次傳入還是對象的格式沙兰,所以會執(zhí)行walk遍歷的將對象每個屬性包裝為響應(yīng)式的氓奈,再來看下defineReactive方法:

function defineReactive(obj, key, val) { 

  const dep = new Dep()  // 自動依賴管理器
  
  val = obj[key]  // val為數(shù)組 [{...}, {...}]
  
  let childOb = observe(val)  // 返回Observer類實例
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {  // 依賴收集
      if (Dep.target) {
        dep.depend()  // 自動依賴管理器收集依賴
        if (childOb) {  // 只有對象或數(shù)組才有返回值
          childOb.dep.depend()  // 手動依賴管理器收集依賴
          if (Array.isArray(val)) { 如果是數(shù)組
            dependArray(val) // 將數(shù)組每一項包裝為響應(yīng)式
          }
        }
      }
      return value
    },
    set(newVal) {
      ...
    }
  }
}

首先遞歸執(zhí)行observe(val)會有一個返回值了,如果是對象或數(shù)組的話鼎天,childOb就是Observer類的實例舀奶。所以在get內(nèi)的childOb.dep.depend()執(zhí)行的就是Observer類里定義的dep進行依賴收集,收集的render watcher跟自動依賴管理器是一樣的斋射。接下來如果是數(shù)組就執(zhí)行dependArray方法:

function dependArray (value) {
  for (let e, i = 0, i < value.length; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()  // 是響應(yīng)式數(shù)據(jù)
    if (Array.isArray(e)) {  // 如果是嵌套數(shù)組
      dependArray(e)  // 遞歸調(diào)用自己
    }
  }
}

這個方法的作用就是遞歸的為每一項收集依賴育勺,這里每一項都必須要有__ob__屬性,然后執(zhí)行Observer類里的dep手動依賴收集器進行依賴收集罗岖。我們現(xiàn)在知道數(shù)組的依賴放哪了涧至,現(xiàn)在關(guān)心的是在哪里去更新這個收集到的依賴。

數(shù)組方法更新依賴

回到defineReactive方法呀闻,看看let childOb = observe(val)這句代碼:

function defineReactive(obj, key, val) { 
  ...
  
  val = obj[key]  // val為數(shù)組 [{...}, {...}]
  let childOb = observe(val)  // 看這句
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {...},
    set(newVal) {...}
  }
}

通過求值化借,val現(xiàn)在就是具體的數(shù)組,傳入到observe內(nèi)以數(shù)組的形式執(zhí)行捡多,我們又回到Observer類中:

class Observer {
  constructor(value) {
    ...
    if (Array.isArray(value)) {  // 數(shù)組
      
      const augment = hasProto // 第一句
        ? protoAugment 
        : copyAugment
      
      augment(value, arrayMethods, arrayKeys)  // 第二句
      
      this.observeArray(value)  // 第三句
      
    } else {  // 對象
      ...
    }
  }
  
  observeArray(items) {
    for (let i = 0, i < items.length; i++) {
      observe(items[i])
    }
  }
}

數(shù)組方法改變自身觸發(fā)視圖原理:首先覆蓋數(shù)組的__proto__隱式原型蓖康,借用數(shù)組原生的方法,定義vue內(nèi)部自定義的數(shù)組異變方法攔截原生方法垒手,再調(diào)用異變方法改變自身之后手動觸發(fā)依賴蒜焊。

有了這只指向月亮的手,我們現(xiàn)在就一起去往心中的月亮科贬。首先分析第一句:

const augment = hasProto ? protoAugment : copyAugment

--------------------------------------------------------

const hasProto = '__proto__' in {}

function protoAugment (target, src) {  // src為攔截器
  target.__proto__ = src
}

function copyAugment (target, src, keys) {  // src為攔截器
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

__proto__這個屬性并不是所有瀏覽器都有的泳梆,筆者之前也一直以為這是一個通用屬性,原來IE11才開始有這個屬性榜掌,通過'__protp__' in {}也可以快速判斷當前瀏覽瀏覽器是否IE10以上优妙?確實用過,好用憎账!

是否有__proto__屬性處理方法也不相同套硼,如果有的的話,直接在protoAugment方法內(nèi)使用攔截器覆蓋胞皱;如果沒有__proto__屬性邪意,那就在當前調(diào)用數(shù)組下掛載攔截器里的異變數(shù)組方法。

實現(xiàn)原理都是根據(jù)原型鏈的特性反砌,再數(shù)組使用原生方法之前加一個攔截器雾鬼,攔截器內(nèi)定義的都是可以改變數(shù)組自身的異變方法,如果攔截器內(nèi)沒有就向一層去找宴树。

接下來分析第二句策菜,也是整個數(shù)組方法實現(xiàn)的核心:

augment(value, arrayMethods, arrayKeys)

----------------------------------------------------------------------------

const arrayProto = Array.prototype  // 數(shù)組原型,有所有數(shù)組原生方法
const arrayMethods = Object.create(arrayProto)  // 創(chuàng)建空對象攔截器

const methodsToPatch = [  // 七個數(shù)組使用會改變自身的方法
  'push','pop','shift','unshift','splice','sort','reverse'
]

methodsToPatch.forEach(function (method) {  // 往攔截器下掛載異變方法

  const original = arrayProto[method]  // 過濾出七個數(shù)組原生原始方法
  
  def(arrayMethods, method, function mutator (...args) {  // 不定參數(shù)
  
    const result = original.apply(this, args)  // 借用原生方法,this就是調(diào)用的數(shù)組
    
    const ob = this.__ob__  // 之前Observer類下掛載的__ob__
    
    let inserted  // 臨時保存數(shù)組新增的值
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) {
      ob.observeArray(inserted)  // 執(zhí)行Observer類中的observeArray方法
    }
    ob.dep.notify()  // 觸發(fā)手動依賴收集器內(nèi)的依賴
    
    return result  // 返回數(shù)組執(zhí)行結(jié)果
  })
})

const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 
// 獲取攔截器內(nèi)掛載好的七個方法key的數(shù)組集合做入,用于沒有__proto__的情況

首先獲取數(shù)組的所有原生方法冒晰,從中過濾出七個調(diào)用可以改變自身的方法,然后創(chuàng)建攔截器在它下面掛載七個經(jīng)過異變的方法竟块,這個異變方法的使用效果和原生方法是一致的壶运,因為就是使用apply借用的,將執(zhí)行后的結(jié)果保存給result浪秘,比如:

const arr = [1, 2, 3]
const result = arr.push(4)

這個時候arr就變成了[1,2,3,4]蒋情,result保存的就是新數(shù)組的長度,既然模仿就模仿的像一點耸携。

接下來的賦值const ob = this.__ob__棵癣,之前定義的__ob__不僅僅是標記位,保存的也是Observer類的實例夺衍。

有三個操作數(shù)組的方法是會添加新值的狈谊,使用inserted變量保存新添的值。如果是使用splice方法沟沙,就將前面兩個表示位置的參數(shù)截取掉河劝。然后使用observeArray方法將新添加的參數(shù)包裝為響應(yīng)式的。

最后通知手動依賴管理器內(nèi)收集到的依賴派發(fā)更新矛紫,返回數(shù)組執(zhí)行后的結(jié)果赎瞎。

最后執(zhí)行第三句:

this.observeArray(value)

將數(shù)組內(nèi)的是數(shù)組或?qū)ο蟮拿恳豁椂及b成響應(yīng)式的。所以當數(shù)組再使用方法時颊咬,首先會去arrayMethods攔截器內(nèi)查找是否是異變方法盹憎,不是的話才去調(diào)用數(shù)組原生方法:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeArr1() {
      this.list.push(4)  // 調(diào)用攔截器里的異變方法
    },
    changeArr2() {
      this.list = this.list.concat(5) 
      // 調(diào)用原生方法犬庇,因為攔截器里沒有度迂,必須重新賦值因為不會改變自身
    }
  }
}

至此數(shù)組響應(yīng)式系統(tǒng)相關(guān)的也講解完畢节腐,整個響應(yīng)式系統(tǒng)也分析完了。我們來總結(jié)下吧麸澜,數(shù)組和對象它們收集依賴都是在get方法里哟绊,但是依賴存放位置并不同,對象是在defineReactive方法的dep內(nèi)痰憎,數(shù)組是Observer類中的dep里;依賴的觸發(fā)對象可以直接在set方法中派發(fā)更新攀涵,而數(shù)組是在自己定義的異變數(shù)組方法最后手動觸發(fā)的铣耘。

同樣數(shù)組響應(yīng)式也是不是完美的,它也有缺點:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeListItem() {  // 改變數(shù)組某一項
      this.list[1] = 5
    },
    changeListLength() {  // 改變數(shù)組長度
      this.list.length = 0
    }
  }
}

以上兩種方式都改變了數(shù)組以故,但響應(yīng)式是無法監(jiān)聽到的蜗细,因為不會觸發(fā)set也沒用使用數(shù)組方法去改變。不過大家還記得我們之前介紹的手動依賴管理器么?我們只要手動去通知它更新依賴就可以觸發(fā)視圖變更~

export default {
  data() {
    return {
      list: [1, 2, 3],
      info: { name: 'cc' }
    }
  },
  methods: {
    changeListItem() {  // 改變數(shù)組某一項
      this.list[1] = 5
      this.list.__ob__.dep.notify()  // 手動通知
    },
    changeListLength() {  // 改變數(shù)組長度
      this.list.length = 0
      this.list.__ob__.dep.notify()  // 手動通知
    },
    changeInfo() {
      this.info.sex = 'man'
      this.info.__ob__.dep.notify()  // 對象也可以
    }
  }
}

常規(guī)的對象增加屬性是不會被感知到的炉媒,也可以使用手動通知的形式觸發(fā)依賴踪区,知道這個原理還是很cool的~

官方填坑

上面的奇技淫巧并不被推薦使用,我們還是介紹下官方推薦的彌補響應(yīng)式不足的兩個API吊骤,$set$delete缎岗,其實它們只是處理一些情況,都不滿足的最后還是調(diào)了一下手動依賴管理器來實現(xiàn)白粉,只是進行了簡單的二次封裝传泊。

this.$set || Vue.set

function set(target, key, val) {
  if(Array.isArray(target)) {  // 數(shù)組
    target.length = Math.max(target.length, key)  // 最大值為長度
    target.splice(key, 1, val)  // 移除一位,異變方法派發(fā)更新
    return val
  }
  
  if(key in target && !(key in Object.prototype)) {  // key屬于target
    target[key] = val  // 賦值操作觸發(fā)set
    return val
  }
  
  if(!target.__ob__) {  // 普通對象賦值操作
    target[key] = val
    return val
  }
  
  defineReactive(target.__ob__.value, key, val)  // 將新值包裝為響應(yīng)式
  
  target.__ob__.dep.notify()  // 手動觸發(fā)通知
  
  return val
}

首先判斷target是否是數(shù)組鸭巴,是數(shù)組的話第二個參數(shù)就是長度了眷细,設(shè)置數(shù)組的長度,然后使用splice這個異變方法插入val鹃祖。
然后是判斷key是否屬于target溪椎,屬于的話就是賦值操作了,這個會觸發(fā)set去派發(fā)更新恬口。接下來如果target并不是響應(yīng)式數(shù)據(jù)校读,那就是普通對象,那就設(shè)置一個對應(yīng)key吧楷兽。最后以上情況都不滿足地熄,說明是在響應(yīng)式數(shù)據(jù)上新增了一個屬性,把新增的屬性轉(zhuǎn)為響應(yīng)式數(shù)據(jù)芯杀,然后通知手動依賴管理器派發(fā)更新端考。

this.$delete || Vue.delete

function del (target, key) {
  if (Array.isArray(target)) {  // 數(shù)組
    target.splice(key, 1)  // 移除指定下表
    return
  }
  
  if (!hasOwn(target, key)) {  // key不屬于target,再見
    return
  }
  
  delete target[key]  // 刪除對象指定key
  
  if (!target.__ob__) {  // 普通對象揭厚,再見
    return
  }
  target.__ob__.dep.notify()  // 手動派發(fā)更新
}

this.$delete就更加簡單了却特,首先如果是數(shù)組就使用異變方法splice移除指定下標值。如果target是對象但key不屬于它筛圆,再見裂明。然后刪除制定key的值,如果target不是響應(yīng)式對象太援,刪除的就是普通對象一個值闽晦,刪了就刪了。否則通知手動依賴管理器派發(fā)更新視圖提岔。

最后按照慣例我們還是以一道vue可能會被問到的面試題作為本章的結(jié)束~

面試官微笑而又不失禮貌的問道:

  • 請簡單描述下vue響應(yīng)式系統(tǒng)仙蛉?

懟回去:

  • 簡單來說就是使用Object.defineProperty這個API為數(shù)據(jù)設(shè)置getset。當讀取到某個屬性時碱蒙,觸發(fā)get將讀取它的組件對應(yīng)的render watcher收集起來荠瘪;當重置賦值時夯巷,觸發(fā)set通知組件重新渲染頁面。如果數(shù)據(jù)的類型是數(shù)組的話哀墓,還做了單獨的處理趁餐,對可以改變數(shù)組自身的方法進行重寫,因為這些方法不是通過重新賦值改變的數(shù)組篮绰,不會觸發(fā)set后雷,所以要單獨處理。響應(yīng)系統(tǒng)也有自身的不足阶牍,所以官方給出了$set$delete來彌補喷面。

上一篇: Vue原理解析(八):一起搞明白令人頭疼的diff算法

順手點個贊或關(guān)注唄,找起來也方便~

分享一個筆者自己寫的組件庫走孽,哪天可能會用的上了 ~ ↓

你可能會用的上的一個vue功能組件庫惧辈,持續(xù)完善中...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市磕瓷,隨后出現(xiàn)的幾起案子盒齿,更是在濱河造成了極大的恐慌,老刑警劉巖困食,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件边翁,死亡現(xiàn)場離奇詭異,居然都是意外死亡硕盹,警方通過查閱死者的電腦和手機符匾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘩例,“玉大人啊胶,你說我怎么就攤上這事《庀停” “怎么了焰坪?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長聘惦。 經(jīng)常有香客問我某饰,道長,這世上最難降的妖魔是什么善绎? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任黔漂,我火速辦了婚禮,結(jié)果婚禮上禀酱,老公的妹妹穿的比我還像新娘炬守。我一直安慰自己,他們只是感情好比勉,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般浩聋。 火紅的嫁衣襯著肌膚如雪观蜗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天衣洁,我揣著相機與錄音墓捻,去河邊找鬼。 笑死坊夫,一個胖子當著我的面吹牛砖第,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播环凿,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼梧兼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了智听?” 一聲冷哼從身側(cè)響起羽杰,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎到推,沒想到半個月后考赛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡莉测,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年颜骤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捣卤。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡忍抽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腌零,到底是詐尸還是另有隱情梯找,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布益涧,位于F島的核電站锈锤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏闲询。R本人自食惡果不足惜久免,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扭弧。 院中可真熱鬧阎姥,春花似錦、人聲如沸鸽捻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衣赶,卻和暖如春诊赊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背府瞄。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工碧磅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遵馆。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓鲸郊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親货邓。 傳聞我的和親對象是個殘疾皇子秆撮,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

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