Vue patch(Vue2.5 patch 方法源碼解析)

  • 當(dāng)通過 crateComponent 創(chuàng)建了組件 VNode,接下來會走到 vm._update蚊丐,執(zhí)行 vm._update,執(zhí)行 vm.path 去把 VNode 轉(zhuǎn)換成真正的 DOM 節(jié)點(diǎn)。但這些只是針對于一個普通的 VNode 節(jié)點(diǎn),現(xiàn)在來看一下組件的 VNode 會有哪些不一樣的地方怜械。

  • patch 過程會調(diào)用 createElm 創(chuàng)建元素節(jié)點(diǎn),看一下 createElm 的實現(xiàn)傅事,定義在 src/core/vdom/patch.js 中:

      function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      if (isDef(vnode.elm) && isDef(ownerArray)) {
        // This vnode was used in a previous render!
        // now it's used as a new node, overwriting its elm would cause
        // potential patch errors down the road when it's used as an insertion
        // reference node. Instead, we clone the node on-demand before creating
        // associated DOM element for it.
        vnode = ownerArray[index] = cloneVNode(vnode)
      }
    
      vnode.isRootInsert = !nested // for transition enter check
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }
    
      ...
    }
    
    

createComponent

  • 這里有個關(guān)鍵的邏輯缕允,這里會判斷 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果為 true 則直接結(jié)束蹭越,接下來就先看一下 createComponent 方法:

      function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
          const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
          if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */, parentElm, refElm)
          }
          // after calling the init hook, if the vnode is a child component
          // it should've created a child instance and mounted it. the child
          // component also has set the placeholder vnode's elm.
          // in that case we can just return the element and be done.
          if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            if (isTrue(isReactivated)) {
              reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
            }
            return true
          }
        }
      }
    
  • createComponent 函數(shù)中障本,首先對 vnode.data 做一些判斷操作:

      let i = vnode.data
      if (isDef(i)) {
        // ...
         if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */, parentElm, refElm)
        }
      }
    
  • 如果 VNode 是一個組件 VNode,那么這個條件會被滿足响鹃,并且得到 i 就是 init 鉤子函數(shù)驾霜,之前提到過,在創(chuàng)建組件 VNode 的時候合并鉤子函數(shù)就是包含 init 鉤子函數(shù)买置,它定義在 src/core/vdom/create-component.js

      init (
      vnode: VNodeWithData,
      hydrating: boolean,
      parentElm: ?Node,
      refElm: ?Node
      ): ?boolean {
        if (
          vnode.componentInstance &&
          !vnode.componentInstance._isDestroyed &&
          vnode.data.keepAlive
        ) {
          // kept-alive components, treat as a patch
          const mountedNode: any = vnode // work around flow
          componentVNodeHooks.prepatch(mountedNode, mountedNode)
        } else {
          const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance,
            parentElm,
            refElm
          )
          child.$mount(hydrating ? vnode.elm : undefined, hydrating)
      }
    
  • init 鉤子函數(shù)執(zhí)行也比較簡單粪糙,首先不考慮 keepAlive 的情況,它是通過 createComponentInstanceForVnode 創(chuàng)建一個 Vue 的實例忿项,然后調(diào)用 $mount 方法掛載子組件蓉冈,看一下 createComponentInstanceForVnode 的實現(xiàn):

      export function createComponentInstanceForVnode (
        vnode: any, // we know it's MountedComponentVNode but flow doesn't
        parent: any, // activeInstance in lifecycle state
        parentElm?: ?Node,
        refElm?: ?Node
        ): Component {
          const options: InternalComponentOptions = {
            _isComponent: true,
            parent,
            _parentVnode: vnode,
            _parentElm: parentElm || null,
            _refElm: refElm || null
          }
          // check inline-template render functions
          const inlineTemplate = vnode.data.inlineTemplate
          if (isDef(inlineTemplate)) {
            options.render = inlineTemplate.render
            options.staticRenderFns = inlineTemplate.staticRenderFns
          }
          return new vnode.componentOptions.Ctor(options)
      }
    
  • createComponentInstanceForVnode 函數(shù)構(gòu)造的一個內(nèi)容組件的參數(shù),然后執(zhí)行 new vnode.componentOptions.Ctor(options)轩触。其中這里的 vnode.componentOptions.Ctor 對應(yīng)的就是子組件的構(gòu)造函數(shù)寞酿,前面的小章節(jié)中分析了它實際上就是繼承于 Vue 的一個構(gòu)造器 Sub, 相當(dāng)于 new Sub(options) 這里的參數(shù)需要注意一下: _ isComponent: true 表示它是一個組件,parent 表示當(dāng)前激活的組件實例脱柱。

  • 所以子組件的實例化實際上就是在這個時機(jī)執(zhí)行的伐弹,并且它會執(zhí)行實例的 _init 方法,代碼在 src/core/instance/init.js 中

      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        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)
        }
    
        // 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
          )
        }
        // ...
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    
  • 首先是合并 options 的過程變化榨为,_isComponent 為 true掸茅,然后會走到 initInternalComponent 過程,先看一下這個函數(shù):

      export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
        const opts = vm.$options = Object.create(vm.constructor.options)
        // doing this because it's faster than dynamic enumeration.
        const parentVnode = options._parentVnode
        opts.parent = options.parent
        opts._parentVnode = parentVnode
        opts._parentElm = options._parentElm
        opts._refElm = options._refElm
    
        const vnodeComponentOptions = parentVnode.componentOptions
        opts.propsData = vnodeComponentOptions.propsData
        opts._parentListeners = vnodeComponentOptions.listeners
        opts._renderChildren = vnodeComponentOptions.children
        opts._componentTag = vnodeComponentOptions.tag
    
        if (options.render) {
          opts.render = options.render
          opts.staticRenderFns = options.staticRenderFns
        }
      }
    
  • 在這個過程中柠逞,要記住幾點(diǎn): opts.parent = options.parent昧狮、opts._parentVnode = parentVnode, opts._parentElm = options._parentElm, opts._refElm = options._refElm 它們就是之前通過 createComponentInstanceForVnode 函數(shù)傳入的幾個參數(shù)合并到了內(nèi)容選項 $optons 中了。
    在看一下 _init 函數(shù)最后的執(zhí)行的代碼:

      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    
  • 由于組件初始化的時候是不傳 el 的板壮,因此組件是自己接管了 逗鸣、$mount 的過程,這個過程的主要流程在 $mount 章節(jié)中已經(jīng)介紹過了,現(xiàn)在看組件 init 的過程撒璧,componentVnodeHooks (代碼在: src/core/vdom/create-component.js) 的 init 鉤子函數(shù)透葛,在完成實例化的 init 后,接著會執(zhí)行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)卿樱。這里 hydrating 為 true 一般是服務(wù)器渲染的情況僚害,現(xiàn)在只考慮客戶端渲染,所以這里 $mount 相當(dāng)于執(zhí)行了 child.$mount(undefined, false), 它最終會調(diào)用 mountComponent 方法繁调,從而執(zhí)行了 vm._render() 方法:

      Vue.prototype._render = function (): VNode {
        const vm: Component = this
        const { render, _parentVnode } = vm.$options
    
        ...
    
        // set parent vnode. this allows render functions to have access
        // to the data on the placeholder node.
        vm.$vnode = _parentVnode
        // render self
        let vnode
        try {
          vnode = render.call(vm._renderProxy, vm.$createElement)
        } catch (e) {
          handleError(e, vm, `render`)
          // return error render result,
          // or previous vnode to prevent render error causing blank component
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production') {
            if (vm.$options.renderError) {
              try {
                vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
              } catch (e) {
                handleError(e, vm, `renderError`)
                vnode = vm._vnode
              }
            } else {
              vnode = vm._vnode
            }
          } else {
            vnode = vm._vnode
          }
        }
    
        ...
        // set parent
        vnode.parent = _parentVnode
        return vnode
      }
    
  • 上面這里只保留了一些部分關(guān)鍵的代碼萨蚕, _parentVnode 就是當(dāng)前組件的父 VNode,而 render 函數(shù)生成的 vnode 當(dāng)前組件渲染 vnode蹄胰,vnode 的 parent 指向了 _parentVnode岳遥,其實也就是 vm.$vnode,他們是一種父子的關(guān)系裕寨。

  • 在執(zhí)行完 vm._render 生成 VNode 之后浩蓉,接下來會執(zhí)行 vm._update 去渲染 VNode,看一下 _update 的定義宾袜,在 src/core/instance/lifecycle.js 中:

      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate')
        }
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        const prevActiveInstance = activeInstance
        activeInstance = vm
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(
            vm.$el, vnode, hydrating, false /* removeOnly */,
            vm.$options._parentElm,
            vm.$options._refElm
          )
          // no need for the ref nodes after initial patch
          // this prevents keeping a detached DOM tree in memory (#5851)
          vm.$options._parentElm = vm.$options._refElm = null
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        activeInstance = prevActiveInstance
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }
    
  • _update 過程中有幾個關(guān)鍵的地方(代碼)捻艳,首先是 vm._vnode = vnode 的邏輯,這個 vnode 是通過 vm._render() 返回的組件渲染 VNode庆猫,vm._vnode 和 vm.$vnode 的關(guān)系就是父子關(guān)系认轨,用代碼就是 vm.vnode.parent === vm.$vnode。

  • 這段代碼也是很有意思:

      export let activeInstance: any = null
    
      const prevActiveInstance = activeInstance
        activeInstance = vm
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(
            vm.$el, vnode, hydrating, false /* removeOnly */,
            vm.$options._parentElm,
            vm.$options._refElm
          )
          // no need for the ref nodes after initial patch
          // this prevents keeping a detached DOM tree in memory (#5851)
          vm.$options._parentElm = vm.$options._refElm = null
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        activeInstance = prevActiveInstance
    
    
  • 這個 activeInstance 作用就是保持當(dāng)前上下文的 Vue 實例阅悍,它是在 lifecycle 模塊的全局變量好渠,定義 export let activeInstance: any = null,并且在之前調(diào)用 createComponentInstanceForVnode 方法的時候從 lifecycle 模塊獲取节视,并且作為參數(shù)傳入的拳锚。
    因為 JavaScript 是一個單線程,Vue 整個初始化時一個深度便遍歷的過程寻行,咋實例化子組件的過程中霍掺,它需要知道當(dāng)前上下文的 Vue 實例是什么,并把它作為子組件的父 Vue 實例拌蜘。之前有提到過對組組件的實例過程會先調(diào)用 initInternalComponent(vm, options) 合并 options杆烁,把 parent 存儲到 vm.$options 中,在$mount 之前會調(diào)用 initLifecycle(vm) 方法:


    export function initLifecycle (vm: Component) {
      const options = vm.$options

      // locate first non-abstract parent
      let parent = options.parent
      if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
          parent = parent.$parent
        }
        parent.$children.push(vm)
      }

      vm.$parent = parent
      ...
    }

  • 上面可以看到 vm.$parent 就是用來保留當(dāng)前 vm 的父實例简卧,并且通過 parent.$children.push(vm) 來把當(dāng)前的 vm 存儲到父實例的 $children 中兔魂。

  • 在 vm._update 的過程中,把當(dāng)前的 vm 賦值給 activeInstance举娩,同時通過 const prevActiveInstance = activeInstance 用 prevActiveInstance 保留上一次的 activeInstance析校。實際上构罗,prevActiveInstance 和當(dāng)前的 vm 是一個父子關(guān)系,當(dāng)一個 vm 實例完成它的所有子樹的 patch 或者 update 過程后智玻,activeInstance 會回到它的父實例遂唧,這個就美的保證了 crateComponentInstanceForVnode 整個深度遍歷過程中,在實例化子組件的時候能傳入當(dāng)前子組件的父 Vue 實例吊奢,并在 _init 的過程中盖彭,通過 vm.$parent 把父子關(guān)系保留。

  • 回到 _update页滚,最后就是調(diào)用 patch 渲染 VNode 召边。


    vm.$el = vm.__patch__(
      vm.$el, vnode, hydrating, false /* removeOnly */,
      vm.$options._parentElm,
      vm.$options._refElm
    )
    function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }

        let isInitialPatch = false
        const insertedVnodeQueue = []

        if (isUndef(oldVnode)) {
          // empty mount (likely as component), create new root element
          isInitialPatch = true
          createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
          ...
        }
        ...
      }

  • 這里就又回到了本文開始的地方,之前分析過負(fù)責(zé)渲染 DOM 的函數(shù)是 createElm逻谦,注意這里只傳了兩個參數(shù)掌实,所以對應(yīng)的 parentElm 是 undefined陪蜻。再看一下它的定義:

      function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      ...
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }

      const data = vnode.data
      const children = vnode.children
      const tag = vnode.tag
      if (isDef(tag)) {
        if (process.env.NODE_ENV !== 'production') {
          if (data && data.pre) {
            creatingElmInVPre++
          }
          if (isUnknownElement(vnode, creatingElmInVPre)) {
            warn(
              'Unknown custom element: <' + tag + '> - did you ' +
              'register the component correctly? For recursive components, ' +
              'make sure to provide the "name" option.',
              vnode.context
            )
          }
        }

        vnode.elm = vnode.ns
          ? nodeOps.createElementNS(vnode.ns, tag)
          : nodeOps.createElement(tag, vnode)
        setScope(vnode)

        /* istanbul ignore if */
        if (__WEEX__) {
          ...
        } else {
          createChildren(vnode, children, insertedVnodeQueue)
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }

        if (process.env.NODE_ENV !== 'production' && data && data.pre) {
          creatingElmInVPre--
        }
      } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text)
        insert(parentElm, vnode.elm, refElm)
      } else {
        vnode.elm = nodeOps.createTextNode(vnode.text)
        insert(parentElm, vnode.elm, refElm)
      }
    }

  • 這里我們傳入的 vnode 是組件渲染的 vnode邦马,也就是之前說的 vm._vnode,如果組件的根節(jié)點(diǎn)是普通元素宴卖,那么 vm._vnode 也是普通的 vnode滋将,這里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值就是 false,接下來的過程就和 我之前寫過的 createComponent(Vue 創(chuàng)建組件) 是一樣的症昏。先創(chuàng)建一個父節(jié)點(diǎn)占位符随闽,然后在遍歷所有的子 VNode 遞推調(diào)用 crateElm,在遍歷過程中肝谭,如果遇到子 VNode 是一個組件的 VNode掘宪,則重復(fù)本文開始的過程,通過一個遞歸的方式就可以完成整個組件數(shù)的構(gòu)建攘烛。

  • 由于這個時候傳入的 parentElm 是空魏滚,所以對組件的插入,在 createComponent 有這個一段邏輯在 patch.js 文件中:

      function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
          const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
          if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */, parentElm, refElm)
          }
          // after calling the init hook, if the vnode is a child component
          // it should've created a child instance and mounted it. the child
          // component also has set the placeholder vnode's elm.
          // in that case we can just return the element and be done.
          if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            if (isTrue(isReactivated)) {
              reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
            }
            return true
          }
        }
      }
    
  • 在完成組件整個 parch 過程后坟漱,最后執(zhí)行 nsert(parentElm, vnode.elm, refElm) 完成組件的 DOM 插入鼠次,如果組件 patch 過程中又創(chuàng)建了子組件,那么 DOM 的插入順序是先子后父芋齿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腥寇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子觅捆,更是在濱河造成了極大的恐慌赦役,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件栅炒,死亡現(xiàn)場離奇詭異掂摔,居然都是意外死亡庸论,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門棒呛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來聂示,“玉大人,你說我怎么就攤上這事簇秒∮愫恚” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵趋观,是天一觀的道長扛禽。 經(jīng)常有香客問我,道長皱坛,這世上最難降的妖魔是什么编曼? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮剩辟,結(jié)果婚禮上掐场,老公的妹妹穿的比我還像新娘。我一直安慰自己贩猎,他們只是感情好熊户,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吭服,像睡著了一般嚷堡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艇棕,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天蝌戒,我揣著相機(jī)與錄音,去河邊找鬼沼琉。 笑死北苟,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刺桃。 我是一名探鬼主播粹淋,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瑟慈!你這毒婦竟也來了桃移?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葛碧,失蹤者是張志新(化名)和其女友劉穎借杰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體进泼,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蔗衡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年纤虽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绞惦。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡逼纸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出济蝉,到底是詐尸還是另有隱情杰刽,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布王滤,位于F島的核電站贺嫂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雁乡。R本人自食惡果不足惜第喳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望踱稍。 院中可真熱鬧曲饱,春花似錦、人聲如沸寞射。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桥温。三九已至,卻和暖如春梁丘,著一層夾襖步出監(jiān)牢的瞬間侵浸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工氛谜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掏觉,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓值漫,卻偏偏與公主長得像澳腹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子杨何,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354