Vue3 源碼解析(六):響應式原理與 reactive

今天這篇文章是筆者會帶著大家一起深入剖析 Vue3 的響應式原理實現,以及在響應式基礎 API 中的 reactive 是如何實現的。對于 Vue 框架來說,其非侵入的響應式系統是最獨特的特性之一了,所以不論任何一個版本的 Vue,在熟悉其基礎用法后,響應式原理都是筆者最想優先了解的部分,也是閱讀源碼時必細細研究的部分。畢竟知己知彼百戰不殆,當你使用 Vue 時,掌握了響應式原理一定會讓你的 coding 過程更加游刃有余的。

Vue2 的響應式原理

在開始介紹 Vue3 的響應式原理前,我們先一起回顧一下 Vue2 的響應式原理。

當我們把一個普通選項傳入 Vue 實例的 data 選項中,Vue 將遍歷此對象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉為 getter/setter。而 Vue2 在處理數組時,也會通過原型鏈劫持會改變數組內元素的方法,并在原型鏈觀察新增的元素,以及派發更新通知。

vue2-observer

這里放上一張 Vue2 文檔中介紹響應式的圖片。對于文檔中有的描述筆者就不再贅述,而從 Vue2 的源碼角度來對照圖片說一說。在 Vue2 的源碼中的 src/core 路徑下有一個 observer 模塊,它就是 Vue2 中處理響應式的地方了。在這個模塊下 observer 負責將對象、數組轉換成響應式的,即圖中的紫色部分,處理 Data 的 getter 及 setter。當 data 中的選項被訪問時,會觸發 getter,此時 observer 目錄下的 wather.js 模塊就會開始工作,它的任務就是收集依賴,我們收集到的依賴是一個個 Dep 類的實例化對象。而 data 中的選項變更時,會觸發 setter 的調用,而在 setter 的過程中,觸發 dep 的 notify 函數,派發更新事件,由此實現數據的響應監聽。

Vue3 的響應式變化

在簡單回顧了 Vue2 的響應式原理后,我們會有一個疑惑,Vue3 的響應式原理與 Vue2 相比有什么不同呢?

在 Vue3 中響應式系統最大的區別就是,數據模型是被代理的 JavaScript 對象了。不論是我們在組件的 data 選項中返回一個普通的JavaScript 對象,還是使用 composition api 創建一個 reactive 對象,Vue3 都會將該對象包裹在一個帶有 get 和 set 處理程序的 Proxy 中。

Proxy 對象用于創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值等)。

其基礎語法類似于:

const p = new Proxy(target, handler)

Proxy 相比較于 Object.defineProperty 究竟有什么優勢呢?這個問題讓我們先從 Object.defineProperty 的弊端說起。

從 Object 的角度來說,由于 Object.defineProperty 是對指定的 key 生成 getter/setter 以進行變化追蹤,那么如果這個 key 一開始不存在我們定義的對象上,響應式系統就無能為力了,所以在 Vue2 中無法檢測對象的 property 的添加或移除。而對于這個缺陷,Vue2 提供了 vm.$set 和全局的 Vue.set API 讓我們能夠向對象添加響應式的 property。

從數組的角度來說,當我們直接利用索引設置一個數組項時,或者當我們修改數組長度時,Vue2 的響應式系統都不能監聽到變化,解決的方法也如上,使用上面提及的 2 個 api。

而這些問題在 ES6 的新特性 Proxy 面前通通都是不存在的,Proxy 對象能夠利用 handler 陷阱在 get、set 時捕獲到任何變動,也能監聽對數組索引的改動以及 數組 length 的改動。

而依賴收集和派發更新的方式在 Vue3 中也變得不同,在這里我先快速的整體描述一下:在 Vue3 中,通過 track 的處理器函數來收集依賴,通過 trigger 的處理器函數來派發更新,每個依賴的使用都會被包裹到一個副作用(effect)函數中,而派發更新后就會執行副作用函數,這樣依賴處的值就被更新了。

響應式基礎 reactive 的實現

既然這是一個源碼分析的文章,咱們還是從源碼的角度來分析響應式究竟是如何實現的。所以筆者會先分析響應式基礎的 API —— reactive ,相信通過講解 reactive 的實現,大家會對 Proxy 有更深刻的認識。

reactive

二話不說,直接看源碼。下面是 reactive API 的函數,函數的參數接受一個對象,通過 createReactiveObject 函數處理后,直接返回一個 proxy 對象。

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 如果試圖去觀察一個只讀的代理對象,會直接返回只讀版本
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 創建一個代理對象并返回
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

在第三行能看到通過判斷 target 中是否有 ReactiveFlags 中的 IS_READONLY key 確定對象是否為只讀對象。ReactiveFlags 枚舉會在源碼中不斷的與我們見面,所以有必要提前介紹一下 ReactiveFlags:

export const enum ReactiveFlags {
  SKIP = '__v_skip', // 是否跳過響應式 返回原始對象
  IS_REACTIVE = '__v_isReactive', // 標記一個響應式對象
  IS_READONLY = '__v_isReadonly', // 標記一個只讀對象
  RAW = '__v_raw' // 標記獲取原始值
}

在 ReactiveFlags 枚舉中有 4 個枚舉值,這四個枚舉值的含義都在注釋里。對于 ReactiveFlags 的使用是代理對象對 handler 中的 trap 陷阱非常好的應用,對象中并不存在這些 key,而通過 get 訪問這些 key 時,返回值都是通過 get 陷阱的函數內處理的。介紹完 ReactiveFlags 后我們繼續往下看。

createReactiveObject

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
)

先看 createReactiveObject 函數的簽名,該函數接受 5 個參數:

  • target:目標對象,想要生成響應式的原始對象。
  • isReadonly:生成的代理對象是否只讀。
  • baseHandlers:生成代理對象的 handler 參數。當 target 類型是 Array 或 Object 時使用該 handler。
  • collectionHandlers:當 target 類型是 Map、Set、WeakMap、WeakSet 時使用該 handler。
  • proxyMap:存儲生成代理對象后的 Map 對象。

這里需要注意的是 baseHandlers 和 collectionHandlers 的區別,這兩個參數會根據 target 的類型進行判斷,最終選擇將哪個參數傳入 Proxy 的構造函數,當做 handler 參數使用。

接著我們開始看 createReactiveObject 的邏輯部分:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果目標不是對象,直接返回原始值
  if (!isObject(target)) {
    return target
  }
  // 如果目標已經是一個代理,直接返回
  // 除非對一個響應式對象執行 readonly
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 目標已經存在對應的代理對象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 只有白名單里的類型才能被創建響應式對象
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

在該函數的邏輯部分,可以看到基礎數據類型并不會被轉換成代理對象,而是直接返回原始值。

并且會將已經生成的代理對象緩存進傳入的 proxyMap,當這個代理對象已存在時不會重復生成,會直接返回已有對象。

也會通過 TargetType 來判斷 target 目標對象的類型,Vue3 僅會對 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他對象會被標記為 INVALID,并返回原始值。

當目標對象通過類型校驗后,會通過 new Proxy() 生成一個代理對象 proxy,handler 參數的傳入也是與 targetType 相關,并最終返回已生成的 proxy 對象。

所以回顧 reactive api,我們可能會得到一個代理對象,也可能只是獲得傳入的 target 目標對象的原始值。

Handlers 的組成

在 @vue/reactive 庫中有 baseHandlers 和 collectionHandlers 兩個模塊,分別生成 Proxy 代理的 handlers 中的 trap 陷阱。

例如在上面生成 reactive 的 api 中 baseHandlers 的參數傳入了一個 mutableHandlers 對象,這個對象是這樣的:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

通過變量名我們能知道 mutableHandlers 中存在 5 個 trap 陷阱。而在 baseHandlers 中,get 和 set 都是通過工廠函數生成的,以便于適配除 reactive 外的其他 api,例如 readonly、shallowReactive、shallowReadonly 等。

baseHandlers 是處理 Array、Object 的數據類型的,這也是我們絕大部分時間使用 Vue3 時使用的類型,所以筆者接下來著重的講一下baseHandlers 中的 get 和 set 陷阱。

get 陷阱

上一段提到 get 是由一個工廠函數生成的,先來看一下 get 陷阱的種類。

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

get 陷阱有 4 個類型,分別對應不同的響應式 API,從名稱中就可以知道對應的 API 名稱,非常一目了然。而所有的 get 都是由 createGetter 函數生成的。所以接下來我們著重看一下 createGetter 的邏輯。

還是老規矩,先從函數簽名看起。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {}
}

createGetter 有 isReadonly 和 shallow 兩個參數,讓使用 get 陷阱的 api 按需使用。而函數的內部返回了一個 get 函數,使用高階函數的方式返回將會傳入 handlers 中 get 參數的函數。

接著看 createGetter 的邏輯:

// 如果 get 訪問的 key 是 '__v_isReactive',返回 createGetter 的 isReadonly 參數取反結果
if (key === ReactiveFlags.IS_REACTIVE) {
  return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
  // 如果 get 訪問的 key 是 '__v_isReadonly',返回 createGetter 的 isReadonly 參數
  return isReadonly
} else if (
  // 如果 get 訪問的 key 是 '__v_raw',并且 receiver 與原始標識相等,則返回原始值
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
    ).get(target)
) {
  return target
}

從這段 createGetter 邏輯中,筆者專門介紹過的 ReactiveFlags 枚舉在這就取得了妙用。其實目標對象中并沒有這些 key,但是在 get 中Vue3 就對這些 key 做了特殊處理,當我們在對象上訪問這幾個特殊的枚舉值時,就會返回特定意義的結果。而可以關注一下 ReactiveFlags.IS_REACTIVE 這個 key 的判斷方式,為什么是只讀標識的取反呢?因為當一個對象的訪問能觸發這個 get 陷阱時,說明這個對象必然已經是一個 Proxy 對象了,所以只要不是只讀的,那么就可以認為是響應式對象了。

接著看 get 的后續邏輯。

繼續判斷 target 是否是一個數組,如果代理對象不是只讀的,并且 target 是一個數組,并且訪問的 key 在數組需要特殊處理的方法里,就會直接調用特殊處理的數組函數執行結果,并返回。

arrayInstrumentations 是一個對象,對象內保存了若干個被特殊處理的數組方法,并以鍵值對的形式存儲。

我們之前說過 Vue2 以原型鏈的方式劫持了數組,而在這里也有類似地作用,而數組的部分我們準備放在后續的文章中再介紹,下面是需要特殊處理的數組。

  • 對索引敏感的數組方法
    • includes、indexOf、lastIndexOf
  • 會改變自身長度的數組方法,需要避免 length 被依賴收集,因為這樣可能會造成循環引用
    • push、pop、shift、unshift、splice
// 判斷 taeget 是否是數組
const targetIsArray = isArray(target)
// 如果不是只讀對象,并且目標對象是個數組,訪問的 key 又在數組需要劫持的方法里,直接調用修改后的數組方法執行
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

// 獲取 Reflect 執行的 get 默認結果
const res = Reflect.get(target, key, receiver)

// 如果是 key 是 Symbol,并且 key 是 Symbol 對象中的 Symbol 類型的 key
// 或者 key 是不需要追蹤的 key: __proto__,__v_isRef,__isVue
// 直接返回 get 結果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
  return res
}

// 不是只讀對象,執行 track 收集依賴
if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

// 如果是 shallow 淺層響應式,直接返回 get 結果
if (shallow) {
  return res
}

if (isRef(res)) {
  // 如果是 ref ,則返回解包后的值 - 當 target 是數組,key 是 int 類型時,不需要解包
  const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  return shouldUnwrap ? res.value : res
}

if (isObject(res)) {
  // 將返回的值也轉換成代理,我們在這里做 isObject 的檢查以避免無效值警告。
  // 也需要在這里惰性訪問只讀和星影視對象,以避免循環依賴。
  return isReadonly ? readonly(res) : reactive(res)
}

// 不是 object 類型則直接返回 get 結果
return res

在處理完數組后,我們對 target 執行 Reflect.get 方法,獲得默認行為的 get 返回值。

之后判斷 當前 key 是否是 Symbol,或者是否是不需要追蹤的 key,如果是的話直接返回 get 的結果 res。

下面??幾個 key 是不需要被依賴收集或者返回響應式結果的。

  • __proto__
  • _v_isRef
  • __isVue

接著判斷當前代理對象是否是只讀對象,如果不是只讀的話,則運行筆者上文提及的 tarck 處理器函數收集依賴。

如果是 shallow 的淺層響應式,則不需要將內部的屬性轉換成代理,直接返回 res。

如果 res 是一個 Ref 類型的對象,就會自動解包返回,這里就能解釋官方文檔中提及的 ref 在 reactive 中會自動解包的特性了。而需要注意的是,當 target 是一個數組類型,并且 key 是 int 類型時,即使用索引訪問數組元素時,不會被自動解包。

如果 res 是一個對象,就會將該對象轉成響應式的 Proxy 代理對象返回,再結合我們之前分析的緩存已生成的 proxy 對象,可以知道這里的邏輯并不會重復生成相同的 res,也可以理解文檔中提及的當我們訪問 reactive 對象中的 key 是一個對象時,它也會自動的轉換成響應式對象,而且由于在此處生成 reactive 或者 readonly 對象是一個延遲行為,不需要在第一時間就遍歷 reactive 傳入的對象中的所有 key,也對性能的提升是一個幫助。

當 res 都不滿足上述條件時,直接返回 res 結果。例如基礎數據類型就會直接返回結果,而不做特殊處理。

至此,get 陷阱的邏輯全部結束了。

set 陷阱

與 createGetter 對應,set 也有一個 createSetter 的工廠函數,也是通過柯里化的方式返回一個 set 函數。

函數簽名都大同小異,那么接下來筆者直接帶大家盤邏輯。

set 的函數比較簡短,所以這次一次性把寫好注釋的代碼放上來,先看代碼再講邏輯。

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 當不是 shallow 模式時,判斷舊值是否是 Ref,如果是則直接更新舊值的 value
      // 因為 ref 有自己的 setter
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // shallow 模式不需要特殊處理,對象按原樣 set
    }
        
    // 判斷 target 中是否存在 key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect.set 獲取默認行為的返回值
    const result = Reflect.set(target, key, value, receiver)
    // 如果目標是原始對象原型鏈上的屬性,則不會觸發 trigger 派發更新
    if (target === toRaw(receiver)) {
      // 使用 trigger 派發更新,根據 hadKey 區別調用事件
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

在 set 的過程中會首先獲取新舊與舊值,當目前的代理對象不是淺層比較時,會判斷舊值是否是一個 Ref,如果舊值不是數組且是一個 ref類型的對象,并且新值不是 ref 對象時,會直接修改舊值的 value。

看到這里可能會有疑問,為什么要更新舊值的 value?如果你使用過 ref 這個 api 就會知道,每個 ref 對象的值都是放在 value 里的,而 ref 與 reactive 的實現是有區別的,ref 其實是一個 class 實例,它的 value 有自己的 set ,所以就不會在這里繼續進行 set 了。ref 的部分在后續的文章中會詳細講解。

在處理完 ref 類型的值后,會聲明一個變量 hadKey,判斷當前要 set 的 key 是否是對象中已有的屬性。

接下來調用 Reflect.set 獲取默認行為的 set 返回值 result。

然后會開始派發更新的過程,在派發更新前,需要保證 target 和原始的 receiver 相等,target 不能是一個原型鏈上的屬性。

之后開始使用 trigger 處理器函數派發更新,如果 hadKey 不存在,則是一個新增屬性,通過 TriggerOpTypes.ADD 枚舉來標記。這里可以看到開篇分析 Proxy 強于 Object.defineProperty 的地方,會監測到任何一個新增的 key,讓響應式系統更強大。

如果 key 是當前 target 上已經存在的屬性,則比較一下新舊值,如果新舊值不一樣,則代表屬性被更新,通過 TriggerOpTypes.SET 來標記派發更新。

在更新派發完后,返回 set 的結果 result,至此 set 結束。

總結

在今天的文章中,筆者先帶大家回顧了 Vue2 的響應式原理,又開始介紹 Vue3 的響應式原理,通過比較 Vue2 和 Vue3 的響應式系統的區別引出 Vue3 響應式系統的提升之處,尤其是其中最主要的調整將 Object.defineProperty 替換為 Proxy 代理對象。

為了讓大家屬性 Proxy 對響應式系統的影響,筆者著重介紹了響應式基礎 API:reactive。分析了 reactive 的實現,以及 reactive api 返回的 proxy 代理對象使用的 handlers 陷阱。并且對陷阱中我們最常用的 get 和 set 的源碼進行分析,相信大家在看完本篇文章以后,對 proxy 這個 ES2015 的新特性的使用又有了新的理解。

本文只是介紹 Vue3 響應式系統的第一篇文章,所以 track 收集依賴,trigger 派發更新的過程沒有詳細展開,在后續的文章中計劃詳細講解副作用函數 effect,以及 track 和 trigger 的過程,如果希望能詳細了解響應式系統的源碼,麻煩大家點個關注免得迷路。

最后,如果這篇文章能夠幫助到你了解 Vue3 中的響應式原理和 reactive 的實現,希望能給本文點一個喜歡??。如果想繼續追蹤后續文章,也可以關注我的賬號或 follow 我的 github,再次謝謝各位可愛的看官老爺。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容