vue源碼解析六 -- 依賴(lài)收集刁憋,派發(fā)更新和nextTick

依賴(lài)收集

上篇文章中我們講了響應(yīng)對(duì)象,這次看看他是如何來(lái)做依賴(lài)收集的墓毒,首先我們看看一下依賴(lài)收集的源碼

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

解釋1

  • vue中做依賴(lài)收集主要是通過(guò)Dep來(lái)做的吓揪,首先判斷有沒(méi)有dep.target,這個(gè)target就是一個(gè)watcher實(shí)例蚁鳖,
    如果有那么調(diào)用dep.depend();這個(gè)函數(shù)在dep的源碼里看我們知道其實(shí)是調(diào)用了watcher實(shí)例的addDep方法磺芭,那么addDep的方法主要做的事情是把dep實(shí)例的id和實(shí)例給到newDepIds,newDeps做一份備份,但是最重要的還是調(diào)用dep.addSub(this)醉箕;

  • dep.addSub是dep實(shí)例上的一個(gè)方法钾腺,我們?nèi)ニ脑创a里面看到這個(gè)方法的作用是把watcher實(shí)例push到this.subs數(shù)組,此時(shí)我們可以看到這是一個(gè)典型的觀察者模式讥裤,把所有的數(shù)據(jù)對(duì)應(yīng)的watcher都放到this.subs里面為了就是notify來(lái)更新數(shù)據(jù)放棒,接下來(lái)我們就來(lái)講如何派發(fā)更新的

解釋2

上面我們看到是如何做依賴(lài)收集,那么現(xiàn)在我們來(lái)看vue中是如何派發(fā)更新的己英,顯然派發(fā)更新的代碼在setter里面间螟,首先比較的是舊值和新設(shè)置的值是不是一樣的,是一樣的直接return。然后調(diào)用自定義的setter,然后執(zhí)行了!shallow && observe(newVal)厢破,目的是防止新設(shè)置的值是對(duì)象荣瑟,如果是對(duì)象的把對(duì)象變成響應(yīng)式的。最后調(diào)用notify非常重要

  • 我們看一個(gè)notify做的事情摩泪,他主要是遍歷了this.subs讓其中的watcher對(duì)象都執(zhí)行update方法笆焰,我們?cè)谶M(jìn)入到watcher源碼里面看看update這個(gè)函數(shù)做了什么事情。這個(gè)函數(shù)進(jìn)行了兩次判斷见坑,判斷有沒(méi)有開(kāi)啟lazy,判斷是不是同步watcher最后執(zhí)行queueWatcher(this),為什么要執(zhí)行這個(gè)了嚷掠,因?yàn)関ue的更新是合并的,同一個(gè)數(shù)據(jù)對(duì)象的多次改變指揮觸發(fā)一次watcher,并且這么做是要把vue數(shù)據(jù)的更改到dom的渲染變成異步荞驴。要在下一次的nextTick時(shí)候觸發(fā)不皆,所以下面我們來(lái)看看queueWatcher做的事情

queueWatcher源碼

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

// Async edge case #6566 requires saving the timestamp when event listeners are
// attached. However, calling performance.now() has a perf overhead especially
// if the page has thousands of event listeners. Instead, we take a timestamp
// every time the scheduler flushes and use that for all event listeners
// attached during that flush.
export let currentFlushTimestamp = 0

// Async edge case fix requires storing an event listener's attach timestamp.
let getNow: () => number = Date.now

// Determine what event timestamp the browser is using. Annoyingly, the
// timestamp can either be hi-res (relative to page load) or low-res
// (relative to UNIX epoch), so in order to compare time we have to use the
// same timestamp type when saving the flush timestamp.
// All IE versions use low-res event timestamps, and have problematic clock
// implementations (#9632)
if (inBrowser && !isIE) {
  const performance = window.performance
  if (
    performance &&
    typeof performance.now === 'function' &&
    getNow() > document.createEvent('Event').timeStamp
  ) {
    // if the event timestamp, although evaluated AFTER the Date.now(), is
    // smaller than it, it means the event is using a hi-res timestamp,
    // and we need to use the hi-res version for event listener timestamps as
    // well.
    getNow = () => performance.now()
  }
}

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

/**
 * Queue a kept-alive component that was activated during patch.
 * The queue will be processed after the entire tree has been patched.
 */
export function queueActivatedComponent (vm: Component) {
  // setting _inactive to false here so that a render function can
  // rely on checking whether it's in an inactive tree (e.g. router-view)
  vm._inactive = false
  activatedChildren.push(vm)
}

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

解釋

  • 這個(gè)函數(shù)首先判斷has對(duì)象有沒(méi)有watcher.id.最開(kāi)始肯定是沒(méi)有直接進(jìn)來(lái),然后判斷flushing從源碼可以看到是false然后把watcher實(shí)例push到queue隊(duì)列中熊楼,然后判斷waiting最開(kāi)始是false直接進(jìn)來(lái)判斷config.async根據(jù)代碼查看是true,所以最后執(zhí)行的是 nextTick(flushSchedulerQueue)霹娄,flushSchedulerQueue是一個(gè)回調(diào)函數(shù)等會(huì)在說(shuō)
  • nextTick從源碼可以看到每次調(diào)用他的時(shí)候,他都會(huì)把傳入的回調(diào)此時(shí)也就是flushSchedulerQueue鲫骗,push到callbacks數(shù)組里面然后調(diào)用timerFunc项棠,這個(gè)函數(shù)是根據(jù)瀏覽器的支持狀況二不一樣,從代碼里面看到如果支持promise那么就用promise來(lái)模擬異步挎峦,如果不支持的話(huà)判斷是否支持MutationObserver 香追,在判斷是否支持setImmediate,都不支持的話(huà)降級(jí)為setTimeout坦胶,但是無(wú)論哪種都是異步執(zhí)行的透典。
  • 不管此時(shí)有多少個(gè)數(shù)組被改變,他都會(huì)收集flushSchedulerQueue到callback里面在下一個(gè)nextTick時(shí)候在執(zhí)行
  • flushSchedulerQueue做的事情是首先把queue隊(duì)列排序從小到大顿苇,目的注釋已經(jīng)說(shuō)了峭咒,下面是翻譯的
    1.組件的更新由父到子;因?yàn)楦附M件的創(chuàng)建過(guò)程是先于子的纪岁,所以watcher的創(chuàng)建也是先父后子凑队,執(zhí)行順序也應(yīng)該保持先父后子。
    2.用戶(hù)的自定義watcher要優(yōu)先于渲染watcher執(zhí)行幔翰;因?yàn)橛脩?hù)自定義watcher是在渲染watcher之前創(chuàng)建的漩氨。
    3.如果一個(gè)組件在父組件的watcher執(zhí)行期間被銷(xiāo)毀,那么它對(duì)應(yīng)的watcher執(zhí)行都可以被跳過(guò)遗增,所以父組件的watcher應(yīng)該先執(zhí)行叫惊。
    然后has[id]設(shè)置為null,為下次做準(zhǔn)備。關(guān)鍵的就是執(zhí)行watcher.run()做修,這個(gè)是觸發(fā)更新的主要函數(shù)霍狰,run函數(shù)先通過(guò)this.get()得到它當(dāng)前的值抡草,然后做判斷,如果滿(mǎn)足新舊值不等蔗坯、新值是對(duì)象類(lèi)型康震、deep模式任何一個(gè)條件,則執(zhí)行watcher的回調(diào)宾濒,注意回調(diào)函數(shù)執(zhí)行的時(shí)候會(huì)把第一個(gè)和第二個(gè)參數(shù)傳入新值value和舊值oldValue签杈,這就是當(dāng)我們添加自定義watcher的時(shí)候能在回調(diào)函數(shù)的參數(shù)中拿到新舊值的原因。渲染watcher在執(zhí)行this.get()時(shí)候就執(zhí)行了updateComponent = () => {vm._update(vm._render(), hydrating)}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鼎兽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铣除,更是在濱河造成了極大的恐慌谚咬,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尚粘,死亡現(xiàn)場(chǎng)離奇詭異择卦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)郎嫁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)秉继,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人泽铛,你說(shuō)我怎么就攤上這事尚辑。” “怎么了盔腔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵杠茬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我弛随,道長(zhǎng)瓢喉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任舀透,我火速辦了婚禮栓票,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘愕够。我一直安慰自己走贪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布惑芭。 她就那樣靜靜地躺著厉斟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪强衡。 梳的紋絲不亂的頭發(fā)上擦秽,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼感挥。 笑死缩搅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的触幼。 我是一名探鬼主播硼瓣,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼置谦!你這毒婦竟也來(lái)了堂鲤?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤媒峡,失蹤者是張志新(化名)和其女友劉穎瘟栖,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體谅阿,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡半哟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了签餐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寓涨。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氯檐,靈堂內(nèi)的尸體忽然破棺而出戒良,到底是詐尸還是另有隱情,我是刑警寧澤冠摄,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布蔬墩,位于F島的核電站,受9級(jí)特大地震影響耗拓,放射性物質(zhì)發(fā)生泄漏拇颅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一乔询、第九天 我趴在偏房一處隱蔽的房頂上張望樟插。 院中可真熱鬧,春花似錦竿刁、人聲如沸黄锤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鸵熟。三九已至,卻和暖如春负甸,著一層夾襖步出監(jiān)牢的瞬間流强,已是汗流浹背痹届。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留打月,地道東北人队腐。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像奏篙,于是被迫代替她去往敵國(guó)和親柴淘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354