帶你了解 React Fiber 的設(shè)計(jì)原理

轉(zhuǎn):https://www.qiyuandi.com/zhanzhang/zonghe/16899.html
如果各位同學(xué)想系統(tǒng)的了解 Fiber,我個(gè)人還是強(qiáng)烈推薦 React 團(tuán)隊(duì) Lin Clark 在 youtube 上的視頻,這個(gè)是理解 React Fiber 最好的一手資源空民。

www.youtube.com/watch?v=ZCu

這篇文章會(huì)努力從一個(gè)不一樣的風(fēng)格去講述 Fiber泛豪,把這個(gè)復(fù)雜的系統(tǒng)通過(guò)最簡(jiǎn)單最直接的語(yǔ)言表達(dá)出來(lái)。但是鸯绿,在看本文之前,我希望各位讀者能對(duì) React 15 的 diff 算法和虛擬 dom 有比較基本的了解。

為什么要學(xué)習(xí) Fiber

對(duì)于一個(gè)前端工程師而言琴昆,無(wú)論從功利的角度還是非功利的角度,你都有足夠多的動(dòng)機(jī)學(xué)習(xí) Fiber馆揉。從功利的角度而言:

  1. Fiber 被高級(jí)別前端工程師職位頻繁考察
  2. 是否了解 Fiber 是互聯(lián)網(wǎng)公司考察程序員是否有深入理解某一事物意愿的重要指標(biāo)之一

從非功利的角度而言: Fiber 可以幫你把前端崗位的三個(gè)重要知識(shí)系統(tǒng)化,體系化:

  1. 瀏覽器渲染機(jī)制
  2. Js 運(yùn)行在單線(xiàn)程上
  3. 前端性能優(yōu)化

為什么要有 Fiber

一言以蔽之抖拦,F(xiàn)iber 是用來(lái)優(yōu)化 React 性能的升酣。首先,我們先來(lái)看看 React15 和 React16 的對(duì)比态罪。

React 15

[圖片上傳失敗...(image-2ad861-1660720729968)]

React 16

[圖片上傳失敗...(image-9ebcca-1660720729968)]

在深入理解其原理之前噩茄,我們有必要在這里做一些知識(shí)鋪墊:我們都知道,js 是一門(mén)單線(xiàn)程語(yǔ)言复颈,準(zhǔn)確的說(shuō)绩聘,是 js 運(yùn)行在瀏覽器渲染進(jìn)程 (render process) 的主線(xiàn)程上 (main thread)。但是這個(gè)主線(xiàn)程不僅僅是為 js 服務(wù)耗啦,它還負(fù)責(zé)其他的事情凿菩,比如響應(yīng)用戶(hù)的輸入,以及頁(yè)面的渲染帜讲,一個(gè)人做三件事衅谷,每?jī)蓚€(gè)事都不能并行,這就產(chǎn)生了一個(gè)問(wèn)題似将,假如說(shuō)任何一件事?lián)屨贾骶€(xiàn)程過(guò)多的時(shí)間获黔,另外兩個(gè)事就只能等著蚀苛。換句話(huà)說(shuō),假如有一個(gè)計(jì)算密集型 (cpu密集型) 的 js 任務(wù)霸占著主線(xiàn)程玷氏,那么瀏覽器是一定沒(méi)有時(shí)間響應(yīng)用戶(hù)輸入以及渲染頁(yè)面的堵未,那么這個(gè)東西就會(huì)導(dǎo)致你的頁(yè)面性能不好,也就是盏触,卡渗蟹。 不幸的是,對(duì)于 react 15 而言耻陕,當(dāng)你使用 react 并且有一棵非常龐大的虛擬 dom 樹(shù)的時(shí)候拙徽,你的 diff 算法就是這樣一個(gè)連續(xù)不可被打斷的計(jì)算密集型 js 任務(wù)。

Fiber 是如何優(yōu)化的性能

stack reconciler 面臨的難題

瀏覽器渲染進(jìn)程有一個(gè)主線(xiàn)程诗宣,這個(gè)主線(xiàn)程既負(fù)責(zé)運(yùn)行 js膘怕,也負(fù)責(zé)頁(yè)面渲染(布局,繪制召庞,合并圖層)岛心。所以你要么運(yùn)行 js,要么渲染頁(yè)面篮灼,不能兩個(gè)同時(shí)進(jìn)行忘古。在 React 15 中,假如一棵虛擬 dom 樹(shù)很大很大诅诱,你進(jìn)行 diff 的時(shí)候就要有大量的 js 運(yùn)算髓堪,這種 cpu 密集型的操作會(huì)占住主線(xiàn)程不放,這時(shí)你的頁(yè)面就會(huì)卡娘荡,用戶(hù)的輸入也得不到響應(yīng)干旁。

[圖片上傳失敗...(image-64d7f3-1660720729968)]

Fiber 扮演的角色

fiber 就好比給 react 加了一個(gè)操作系統(tǒng)。 告訴它什么時(shí)候 diff炮沐,什么時(shí)候渲染争群,什么時(shí)候響應(yīng)用戶(hù)輸入。這好比操作系統(tǒng)的時(shí)間片輪轉(zhuǎn)法大年,也很像 generator 允許中斷這種機(jī)制换薄。

由于有了 Fiber,React 在每一幀當(dāng)中都有時(shí)間響應(yīng)用戶(hù)輸入和進(jìn)行頁(yè)面渲染翔试,因此看起來(lái)不卡轻要。

[圖片上傳失敗...(image-a1c287-1660720729968)]

Fiber Reconciler 的全過(guò)程

知識(shí)鋪墊

瀏覽器的一幀

[圖片上傳失敗...(image-14ef0f-1660720729968)]

這篇文章對(duì)瀏覽器原理不會(huì)有詳盡的展開(kāi),但是垦缅,一些必要的知識(shí)背景對(duì)理解 Fiber 事至關(guān)重要的伦腐。我們可以看到,瀏覽器渲染進(jìn)程 Renderer Process 的主線(xiàn)程 Main Thread 在一幀內(nèi)會(huì)有很多步驟失都,解析 HTML 成 dom 樹(shù)柏蘑,計(jì)算并往 dom 樹(shù)里面嵌入樣式幸冻,計(jì)算布局(各個(gè) dom 節(jié)點(diǎn)在屏幕的什么位置),生成操作系統(tǒng)層面的繪制命令咳焚,為每一個(gè)圖層的繪制生成順序并生成繪制命令的序列洽损,然后進(jìn)行圖層的合成(注意,這一步不一定是主線(xiàn)程做革半,有可能是合成幀線(xiàn)程做的)碑定。如果主線(xiàn)程在一幀內(nèi)還有額外的時(shí)間,那么又官,它會(huì)去執(zhí)行 requestIdleCallback 里面的回調(diào)函數(shù)延刘。

requestIdleCallback

requestIdleCallback 這個(gè) API 是 Fiber 中非常重要的一個(gè)概念。我們已經(jīng)知道瀏覽器渲染進(jìn)程的主線(xiàn)程負(fù)責(zé)很多件事六敬,下圖中所有的事情都是主線(xiàn)程來(lái)完成的碘赖,如果主線(xiàn)程還有時(shí)間,它會(huì)去執(zhí)行 requestIdleCallback 里面的任務(wù)外构。如果沒(méi)有時(shí)間普泡,requestIdleCallback 里面的回調(diào)會(huì)被延遲到下一幀執(zhí)行,如果下一幀還沒(méi)時(shí)間审编,就繼續(xù)再往后推撼班。

[圖片上傳失敗...(image-2376d7-1660720729968)]

我們可以和 requestAmimationFrame 這個(gè)函數(shù)做一個(gè)對(duì)比實(shí)驗(yàn)。實(shí)驗(yàn)的目的是驗(yàn)證 rIC 并不是每一幀都會(huì)被執(zhí)行垒酬。

        num = 300
        function f() {
            console.log('print')
            num -= 1
            if (num > 0)
                requestIdleCallback(f) // 換成 requestAmimationFrame 試試
        }
        f()

Js

復(fù)制

你會(huì)發(fā)現(xiàn)砰嘁,對(duì)于 rAF,程序會(huì)在 5s 后停止勘究,但對(duì)于 rIC般码,程序的運(yùn)行時(shí)間遠(yuǎn)遠(yuǎn)大于 5s。

react 會(huì)使用 requestIdleCallback 來(lái)逐個(gè)更新 Fiber Node 節(jié)點(diǎn)乱顾,如果瀏覽器沒(méi)時(shí)間留給 requestIdleCallback,那么更新過(guò)程會(huì)被暫停宫静,讓出主線(xiàn)程走净。

更新流程概述

從 diff 到更新真實(shí) dom,這個(gè)過(guò)程可以被分成 4 步孤里。

  1. 生成 UpdateQueue
  2. 更新 WIP Tree
  3. 生成 EffectList
  4. 更新真實(shí) dom

前三個(gè)階段并成為 render 階段伏伯,最后一個(gè)階段為 commit 階段。大家可以先思考一個(gè)問(wèn)題捌袜,對(duì)于 Fiber 算法而言说搅,哪一個(gè)階段可以使用 requestIdleCallback,也就是可以被打斷虏等?

[圖片上傳失敗...(image-d769c3-1660720729968)]

詳盡的更新流程

代碼

import React, { Component, PureComponent } from 'react';
class List extends Component {
    constructor(props) {
        super(props);
        this.state = { 
            list:[1,2,3]
         }
    }
    render() { 
        const {list}=this.state
        return ( <div>
            <button onClick={()=>{
                let {list}=this.state
                for(let i=0;i<list.length;i++){
                    list[i]*=list[i]
                }
                this.setState({
                    list
                })
            }}>^2</button>
            <Item num={list[0]} key={0}/>
            <Item num={list[1]} key={1}/>
            <Item num={list[2]} key={2}/>
        </div> );
    }
}
export default List;

Js

復(fù)制


class Item extends PureComponent {
    constructor(props) {
        super(props);
    }
    render() { 
        return (<div>
            {this.props.num}
        </div>  );
    }
}

export default Item;

Js

復(fù)制

代碼概述:

父組件

  1. 組件名:List
  2. state:list = [1,2,3]
  3. props:無(wú)

子組件:

  1. 組件名:Item
  2. state:無(wú)
  3. props:num = this.state.list[i]

[圖片上傳失敗...(image-6e2b03-1660720729967)]

List 組件的 setState

react 會(huì)把 setState 以某種數(shù)據(jù)結(jié)構(gòu)注入到 updateQueue 里面

[圖片上傳失敗...(image-3b6f17-1660720729967)]

當(dāng)我們點(diǎn)擊 button 觸發(fā) setState 以后弄唧,[1,2,3] 分別乘上自己變成 [1,4,9]适肠。

Fiber 樹(shù)

在 List 組件第一次渲染的時(shí)候,react 會(huì)用 jsx 生成好一棵 Fiber 樹(shù)放在內(nèi)存里面候引,這個(gè) Fiber 樹(shù)長(zhǎng)什么樣侯养?長(zhǎng)下面這樣:

[圖片上傳失敗...(image-826609-1660720729967)]

了解過(guò) React 15 的同學(xué)應(yīng)該知道,這個(gè)數(shù)據(jù)結(jié)構(gòu)和虛擬 dom 樹(shù)并沒(méi)有本質(zhì)的區(qū)別澄干,它最重要的區(qū)別就是它的指針變得更多了逛揩,但本質(zhì)上還是一個(gè)樹(shù)形結(jié)構(gòu)。至此麸俘,我們得到 Fiber 結(jié)構(gòu)的第一條信息:Fiber 是一棵鏈表樹(shù)辩稽。

Fiber 里面的每一個(gè)節(jié)點(diǎn)有指向它兒子,父節(jié)點(diǎn)和兄弟節(jié)點(diǎn)的指針从媚。

[圖片上傳失敗...(image-569f9a-1660720729967)]

為什么是鏈表樹(shù)

遞歸不好保護(hù)現(xiàn)場(chǎng)逞泄。事實(shí)上,react 團(tuán)隊(duì)也用 es6 generator 做過(guò)一些嘗試静檬,最后以失敗告終炭懊。結(jié)果證明,鏈表這種數(shù)據(jù)結(jié)構(gòu)是最好做中斷恢復(fù)的拂檩,這是因?yàn)殒湵碓谘h(huán)過(guò)程中侮腹,容易停止并保護(hù)現(xiàn)場(chǎng),讓主線(xiàn)程去響應(yīng)用戶(hù)輸入稻励,去做頁(yè)面渲染父阻。

Work In Progress 樹(shù)

react 會(huì)根據(jù) current tree 和 update queue 生成 work in progress tree。你可以簡(jiǎn)單的把 current tree 和 work in progress tree 理解為一次 setState 前后虛擬 dom 的 snapshot望抽。

React 會(huì)從觸發(fā) setState 的組件開(kāi)始更新加矛,由于 List 的 state 被改變,List 節(jié)點(diǎn)對(duì)應(yīng)的 effectTag 會(huì)置為 update煤篙,effectList 會(huì)生成一個(gè)鏈表節(jié)點(diǎn)(為了方便斟览,我這里用數(shù)組表示,大家知道 react 實(shí)際上用的是鏈表就可以了)

[圖片上傳失敗...(image-2b754a-1660720729967)]

[{
    instance: List
    type: ‘update’
    property: ‘state.list’
    value: [1,4,9]
}]

Js

復(fù)制

button 是 List 中的元素辑奈,但是它并沒(méi)有任何屬性的更新苛茂,WIP Tree 直接從 current Tree 復(fù)制節(jié)點(diǎn)。

第一個(gè) Item 由于 props.num 并沒(méi)有發(fā)生改變鸠窗,shouldComponentUpdate 里面的邏輯可以讓該組件不去觸發(fā)更新妓羊,Item 節(jié)點(diǎn)直接從 current tree 復(fù)制過(guò)來(lái)就可以了。

[圖片上傳失敗...(image-2d5eea-1660720729967)]

由于第一個(gè) Item 組件不需要觸發(fā)更新稍计,那么其子節(jié)點(diǎn)可以完全從 current Tree 復(fù)制過(guò)來(lái)躁绸。若 rIC 時(shí)間片未到期,React 會(huì)繼續(xù)更新 FiberNode。此時(shí)净刮,React 發(fā)現(xiàn)剥哑,第二個(gè) Item 的父元素的 state 發(fā)生了改變,也就是 Item 的 props 發(fā)生了改變庭瑰,同時(shí)星持,Item 里面 div 元素的 innerText 屬性發(fā)生了改變,因此第二個(gè) Item 的 FiberNode 的 effectList 變成

 [{
    instance: Item, 
    type: ‘update’,
    property: ‘props.num’
    value: 4
}]

Js

復(fù)制

div 的 FiberNode 對(duì)應(yīng)的 effectList 變?yōu)?/p>

[{
   name: div,   
   type: ‘update’,
   property: ‘innerText’
   value: 4
}]

Js

復(fù)制

[圖片上傳失敗...(image-26ed27-1660720729967)]

被打斷

如果此時(shí) requestIdleCallback 的時(shí)間片到期弹灭,那么 react 會(huì)把控制權(quán)交給瀏覽器督暂,瀏覽器此時(shí)去響應(yīng)用戶(hù)輸入,渲染頁(yè)面穷吮。

下一幀逻翁,React 再次在 rIC 拿到了控制權(quán),它繼續(xù)更新第三個(gè) Item捡鱼,查看父元素的 state 后得知自己的 props 有了更新八回,Item 的 effectList 變成

[{
    instance: Item, 
    type: ‘update’,
    property: ‘props.num’
    value: 9
}]

Js

復(fù)制

發(fā)現(xiàn)其子元素 div 的 innerText 發(fā)生了改變,因此這個(gè) div 的 effectList 變成

[{
    instance: div, 
    type: ‘update’,
    property: ‘innerText’
    value: 9
}]

Js

復(fù)制

[圖片上傳失敗...(image-6afdd7-1660720729967)]

effect List 如何拼接

會(huì)按照 dfs 的順序拼接 effectList驾诈,子節(jié)點(diǎn)的 effectList 在返回時(shí)拼接到父節(jié)點(diǎn)的頭部缠诅,兄弟節(jié)點(diǎn)會(huì)把第一個(gè)兄弟節(jié)點(diǎn)的 effectList 當(dāng)作頭部按順序進(jìn)行拼接,在本例中乍迄,

  1. 第二個(gè) div 返回管引,拼到第二個(gè) Item 上,
  2. div -> Item (1)
  3. 第三個(gè) div 返回闯两,拼到第三個(gè) Item 上
  4. div -> Item (2)
  5. 和 (2) 拼接起來(lái)
  6. div -> Item -> div -> Item
  7. 返回 List
  8. div -> Item -> div -> Item -> List

[圖片上傳失敗...(image-8c31c0-1660720729967)]

事實(shí)上褥伴,就如上一節(jié)所說(shuō),React 會(huì)收集所有被標(biāo)記了 effectTag 元素的 effectList漾狼,但為了抓主要矛盾重慢,在上面的講述中,我重點(diǎn)講述兩個(gè) div 的 effectList

[[
    instance: div, 
    type: ‘update’,
    property: ‘innerText’
    value: 4
},
{
    instance: div, 
    type: ‘update’,
    property: ‘innerText’
    value: 9
}]

Js

復(fù)制

React 會(huì)用 effectList 批量的更新真實(shí) dom逊躁,這一階段被稱(chēng)為 commit 階段似踱。它會(huì)一次性的調(diào)用更新真實(shí) dom 的方法,在本例中稽煤,為 dom.innerText = ‘xxx’

之前問(wèn)題的答案

至此為止核芽,我想有的同學(xué)應(yīng)該可以猜出 render 階段和 commit 階段到底誰(shuí)可以被打斷。答案是顯而易見(jiàn)的念脯,render 階段可以被打斷,commit 階段不可以被打斷弯淘。請(qǐng)注意绿店,這里的 render 和 react 內(nèi)部的那個(gè) render 并不是一個(gè)概念。而 commit 才是去更新真實(shí) dom 的階段。如果 commit 被打斷假勿,這就意味著 dom 的更新不是一次性的借嗽,那么,1转培,2恶导,3 可能會(huì)被更新成 1,4浸须,3 而中途被打斷惨寿。這個(gè)對(duì)于使用者而言,顯然是不可以接受的删窒。

尤大對(duì) React Fiber 的評(píng)價(jià)

雖然 React Fiber 這一設(shè)計(jì)被國(guó)外很多人認(rèn)為是高技術(shù)含量的體現(xiàn)裂垦,但是對(duì) Fiber 的評(píng)價(jià)卻褒貶不一,尤雨溪在的 Vue Conf 說(shuō)過(guò):如果我們可以把更新做得足夠快的話(huà)肌索,理論上就不需要時(shí)間分片了蕉拢。他說(shuō):React Fiber 本質(zhì)上是為了解決 React 更新低效率的問(wèn)題,不要期望 Fiber 能給你現(xiàn)有應(yīng)用帶來(lái)質(zhì)的提升, 如果性能問(wèn)題是自己造成的诚亚,自己的鍋還是得自己背.

參考資料

【1】Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 www.youtube.com/watch?v=ZCu

【2】React Fiber Architecture github.com/acdlite/rea…

【3】Facebook announces React Fiber, a rewrite of its React framework techcrunch.com/2017/04/18/…

【4】這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式 juejin.cn/post/684490…

【5】React Fiber 原理介紹 segmentfault.com/a/119000001…

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鳞疲,更是在濱河造成了極大的恐慌霎奢,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件份乒,死亡現(xiàn)場(chǎng)離奇詭異恕汇,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)或辖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門(mén)瘾英,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人颂暇,你說(shuō)我怎么就攤上這事缺谴。” “怎么了耳鸯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵湿蛔,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我县爬,道長(zhǎng)阳啥,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任财喳,我火速辦了婚禮察迟,結(jié)果婚禮上斩狱,老公的妹妹穿的比我還像新娘。我一直安慰自己扎瓶,他們只是感情好所踊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著概荷,像睡著了一般秕岛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上误证,一...
    開(kāi)封第一講書(shū)人閱讀 49,730評(píng)論 1 289
  • 那天继薛,我揣著相機(jī)與錄音,去河邊找鬼雷厂。 笑死惋增,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的改鲫。 我是一名探鬼主播诈皿,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼像棘!你這毒婦竟也來(lái)了稽亏?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缕题,失蹤者是張志新(化名)和其女友劉穎截歉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體烟零,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘪松,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锨阿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵睦。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墅诡,靈堂內(nèi)的尸體忽然破棺而出壳嚎,到底是詐尸還是另有隱情,我是刑警寧澤末早,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布烟馅,位于F島的核電站,受9級(jí)特大地震影響然磷,放射性物質(zhì)發(fā)生泄漏郑趁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一姿搜、第九天 我趴在偏房一處隱蔽的房頂上張望寡润。 院中可真熱鬧缺脉,春花似錦、人聲如沸悦穿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)栗柒。三九已至,卻和暖如春知举,著一層夾襖步出監(jiān)牢的瞬間瞬沦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工雇锡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逛钻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓锰提,卻偏偏與公主長(zhǎng)得像曙痘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子立肘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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