高性能滾動 scroll 及頁面渲染優(yōu)化

最近在研究頁面渲染及web動畫的性能問題,以及拜讀《CSS SECRET》(CSS揭秘)這本大作渡八。

本文主要想談?wù)勴撁鎯?yōu)化之滾動優(yōu)化迂求。

主要內(nèi)容包括了為何需要優(yōu)化滾動事件,滾動與頁面渲染的關(guān)系欢嘿,節(jié)流與防抖衰琐,pointer-events:none 優(yōu)化滾動。因?yàn)楸疚纳婕傲撕芏嗪芏嗷A(chǔ)炼蹦,可以對照上面的知識點(diǎn)羡宙,選擇性跳到相應(yīng)地方閱讀。

滾動優(yōu)化的由來
滾動優(yōu)化其實(shí)也不僅僅指滾動(scroll 事件)掐隐,還包括了例如 resize 這類會頻繁觸發(fā)的事件狗热。簡單的看看:

var i = 0;
window.addEventListener('scroll',function(){
    console.log(i++);
},false);

輸出如下:


在綁定 scroll 、resize 這類事件時(shí)虑省,當(dāng)它發(fā)生時(shí)匿刮,它被觸發(fā)的頻次非常高,間隔很近探颈。如果事件中涉及到大量的位置計(jì)算熟丸、DOM 操作、元素重繪等工作且這些工作無法在下一個(gè) scroll 事件觸發(fā)前完成伪节,就會造成瀏覽器掉幀光羞。加之用戶鼠標(biāo)滾動往往是連續(xù)的绩鸣,就會持續(xù)觸發(fā) scroll 事件導(dǎo)致掉幀擴(kuò)大、瀏覽器 CPU 使用率增加纱兑、用戶體驗(yàn)受到影響全闷。

在滾動事件中綁定回調(diào)應(yīng)用場景也非常多,在圖片的懶加載萍启、下滑自動加載數(shù)據(jù)总珠、側(cè)邊浮動導(dǎo)航欄等中有著廣泛的應(yīng)用。

當(dāng)用戶瀏覽網(wǎng)頁時(shí)勘纯,擁有平滑滾動經(jīng)常是被忽視但卻是用戶體驗(yàn)中至關(guān)重要的部分局服。當(dāng)滾動表現(xiàn)正常時(shí),用戶就會感覺應(yīng)用十分流暢驳遵,令人愉悅淫奔,反之,笨重不自然卡頓的滾動堤结,則會給用戶帶來極大不舒爽的感覺唆迁。

滾動與頁面渲染的關(guān)系
為什么滾動事件需要去優(yōu)化?因?yàn)樗绊懥诵阅芫呵睢D撬绊懥耸裁葱阅苣靥圃穑款~……這個(gè)就要從頁面性能問題由什么決定說起。

我覺得搞技術(shù)一定要追本溯源瘾带,不要看到別人一篇文章說滾動事件會導(dǎo)致卡頓并說了一堆解決方案優(yōu)化技巧就如獲至寶奉為圭臬鼠哥,我們需要的不是拿來主義而是批判主義,多去源頭看看看政。

從問題出發(fā)朴恳,一步一步尋找到最后,就很容易找到問題的癥結(jié)所在允蚣,只有這樣得出的解決方法才容易記住于颖。

說教了一堆廢話,不喜歡的直接忽略哈嚷兔,回到正題森渐,要找到優(yōu)化的入口就要知道問題出在哪里,對于頁面優(yōu)化而言谴垫,那么我們就要知道頁面的渲染原理:

瀏覽器渲染原理我在我上一篇文章里也要詳細(xì)的講到章母,不過更多的是從動畫渲染的角度去講的:《【W(wǎng)eb動畫】CSS3 3D 行星運(yùn)轉(zhuǎn) && 瀏覽器渲染原理》

想了想,還是再簡單的描述下翩剪,我發(fā)現(xiàn)每次 review 這些知識點(diǎn)都有新的收獲乳怎,這次換一張圖,以 chrome 為例子,一個(gè) Web 頁面的展示蚪缀,簡單來說可以認(rèn)為經(jīng)歷了以下下幾個(gè)步驟:


**1. **JavaScript:一般來說秫逝,我們會使用 JavaScript 來實(shí)現(xiàn)一些視覺變化的效果。比如做一個(gè)動畫或者往頁面里添加一些 DOM 元素等询枚。
**2. **Style:計(jì)算樣式违帆,這個(gè)過程是根據(jù) CSS 選擇器,對每個(gè) DOM 元素匹配對應(yīng)的 CSS 樣式金蜀。這一步結(jié)束之后刷后,就確定了每個(gè) DOM 元素上該應(yīng)用什么 CSS 樣式規(guī)則。
**3. **Layout:布局渊抄,上一步確定了每個(gè) DOM 元素的樣式規(guī)則尝胆,這一步就是具體計(jì)算每個(gè) DOM 元素最終在屏幕上顯示的大小和位置。web 頁面中元素的布局是相對的护桦,因此一個(gè)元素的布局發(fā)生變化含衔,會聯(lián)動地引發(fā)其他元素的布局發(fā)生變化。比如二庵, 元素的寬度的變化會影響其子元素的寬度贪染,其子元素寬度的變化也會繼續(xù)對其孫子元素產(chǎn)生影響。因此對于瀏覽器來說催享,布局過程是經(jīng)常發(fā)生的杭隙。
**4. **Paint:繪制,本質(zhì)上就是填充像素的過程睡陪。包括繪制文字寺渗、顏色、圖像兰迫、邊框和陰影等,也就是一個(gè) DOM 元素所有的可視效果炬称。一般來說汁果,這個(gè)繪制過程是在多個(gè)層上完成的。
**5. **Composite:渲染層合并玲躯,由上一步可知据德,對頁面中 DOM 元素的繪制是在多個(gè)層上進(jìn)行的。在每個(gè)層上完成繪制過程之后跷车,瀏覽器會將所有層按照合理的順序合并成一個(gè)圖層棘利,然后顯示在屏幕上。對于有位置重疊的元素的頁面朽缴,這個(gè)過程尤其重要善玫,因?yàn)橐坏﹫D層的合并順序出錯(cuò),將會導(dǎo)致元素顯示異常密强。

這里又涉及了層(GraphicsLayer)的概念茅郎,GraphicsLayer 層是作為紋理(texture)上傳給 GPU 的蜗元,現(xiàn)在經(jīng)常能看到說 GPU 硬件加速,就和所謂的層的概念密切相關(guān)系冗。但是和本文的滾動優(yōu)化相關(guān)性不大奕扣,有興趣深入了解的可以自行 google 更多。

簡單來說掌敬,網(wǎng)頁生成的時(shí)候惯豆,至少會渲染(Layout+Paint)一次。用戶訪問的過程中奔害,還會不斷重新的重排(reflow)和重繪(repaint)楷兽。

其中,用戶 scroll 和 resize 行為(即是滑動頁面和改變窗口大幸ㄎ洹)會導(dǎo)致頁面不斷的重新渲染拄养。

當(dāng)你滾動頁面時(shí),瀏覽器可能會需要繪制這些層(有時(shí)也被稱為合成層)里的一些像素银舱。通過元素分組瘪匿,當(dāng)某個(gè)層的內(nèi)容改變時(shí),我們只需要更新該層的結(jié)構(gòu)寻馏,并僅僅重繪和柵格化渲染層結(jié)構(gòu)里變化的那一部分棋弥,而無需完全重繪。顯然诚欠,如果當(dāng)你滾動時(shí)顽染,像視差網(wǎng)站(戳我看看)這樣有東西在移動時(shí),有可能在多層導(dǎo)致大面積的內(nèi)容調(diào)整轰绵,這會導(dǎo)致大量的繪制工作粉寞。

防抖(Debouncing)和節(jié)流(Throttling)

scroll 事件本身會觸發(fā)頁面的重新渲染,同時(shí) scroll 事件的 handler 又會被高頻度的觸發(fā), 因此事件的 handler 內(nèi)部不應(yīng)該有復(fù)雜操作左腔,例如 DOM 操作就不應(yīng)該放在事件處理中唧垦。

針對此類高頻度觸發(fā)事件問題(例如頁面 scroll ,屏幕 resize液样,監(jiān)聽用戶輸入等)振亮,下面介紹兩種常用的解決方法,防抖和節(jié)流鞭莽。

防抖(Debouncing)

防抖技術(shù)即是可以把多個(gè)順序地調(diào)用合并成一次坊秸,也就是在一定時(shí)間內(nèi),規(guī)定事件被觸發(fā)的次數(shù)澎怒。

通俗一點(diǎn)來說褒搔,看看下面這個(gè)簡化的例子:

// 簡單的防抖動函數(shù)
function debounce(func, wait, immediate) {
    // 定時(shí)器變量
    var timeout;
    return function() {
        // 每次觸發(fā) scroll handler 時(shí)先清除定時(shí)器
        clearTimeout(timeout);
        // 指定 xx ms 后觸發(fā)真正想進(jìn)行的操作 handler
        timeout = setTimeout(func, wait);
    };
};
// 實(shí)際想綁定在 scroll 事件上的 handler
function realFunc(){
    console.log("Success");
}
// 采用了防抖動
window.addEventListener('scroll',debounce(realFunc,500));
// 沒采用防抖動
window.addEventListener('scroll',realFunc);

上面簡單的防抖的例子可以拿到瀏覽器下試一下,大概功能就是如果 500ms 內(nèi)沒有連續(xù)觸發(fā)兩次 scroll 事件,那么才會觸發(fā)我們真正想在 scroll 事件中觸發(fā)的函數(shù)站超。

// 防抖動函數(shù)
function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate & !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};
var myEfficientFn = debounce(function() {
    // 滾動中的真正的操作
}, 250);
// 綁定監(jiān)聽
window.addEventListener('resize', myEfficientFn);

節(jié)流(Throttling)
防抖函數(shù)確實(shí)不錯(cuò)荸恕,但是也存在問題,譬如圖片的懶加載死相,我希望在下滑過程中圖片不斷的被加載出來融求,而不是只有當(dāng)我停止下滑時(shí)候,圖片才被加載出來算撮。又或者下滑時(shí)候的數(shù)據(jù)的 ajax 請求加載也是同理生宛。

這個(gè)時(shí)候,我們希望即使頁面在不斷被滾動肮柜,但是滾動 handler 也可以以一定的頻率被觸發(fā)(譬如 250ms 觸發(fā)一次)陷舅,這類場景,就要用到另一種技巧审洞,稱為節(jié)流函數(shù)(throttling)莱睁。

節(jié)流函數(shù),只允許一個(gè)函數(shù)在 X 毫秒內(nèi)執(zhí)行一次芒澜。

與防抖相比仰剿,節(jié)流函數(shù)最主要的不同在于它保證在 X 毫秒內(nèi)至少執(zhí)行一次我們希望觸發(fā)的事件 handler。

與防抖相比痴晦,節(jié)流函數(shù)多了一個(gè) mustRun 屬性南吮,代表 mustRun 毫秒內(nèi),必然會觸發(fā)一次 handler 誊酌,同樣是利用定時(shí)器部凑,看看簡單的示例:

// 簡單的節(jié)流函數(shù)
function throttle(func, wait, mustRun) {
    var timeout,
        startTime = new Date();
    return function() {
        var context = this,
            args = arguments,
            curTime = new Date();
        clearTimeout(timeout);
        // 如果達(dá)到了規(guī)定的觸發(fā)時(shí)間間隔,觸發(fā) handler
        if(curTime - startTime >= mustRun){
            func.apply(context,args);
            startTime = curTime;
        // 沒達(dá)到觸發(fā)間隔碧浊,重新設(shè)定定時(shí)器
        }else{
            timeout = setTimeout(func, wait);
        }
    };
};
// 實(shí)際想綁定在 scroll 事件上的 handler
function realFunc(){
    console.log("Success");
}
// 采用了節(jié)流函數(shù)
window.addEventListener('scroll',throttle(realFunc,500,1000));

上面簡單的節(jié)流函數(shù)的例子可以拿到瀏覽器下試一下涂邀,大概功能就是如果在一段時(shí)間內(nèi) scroll 觸發(fā)的間隔一直短于 500ms ,那么能保證事件我們希望調(diào)用的 handler 至少在 1000ms 內(nèi)會觸發(fā)一次箱锐。

使用 rAF(requestAnimationFrame)觸發(fā)滾動事件

上面介紹的抖動與節(jié)流實(shí)現(xiàn)的方式都是借助了定時(shí)器 setTimeout 必孤,但是如果頁面只需要兼容高版本瀏覽器或應(yīng)用在移動端,又或者頁面需要追求高精度的效果瑞躺,那么可以使用瀏覽器的原生方法 rAF(requestAnimationFrame)。

requestAnimationFrame

window.requestAnimationFrame() 這個(gè)方法是用來在頁面重繪之前兴想,通知瀏覽器調(diào)用一個(gè)指定的函數(shù)幢哨。這個(gè)方法接受一個(gè)函數(shù)為參,該函數(shù)會在重繪前調(diào)用嫂便。

rAF 常用于 web 動畫的制作捞镰,用于準(zhǔn)確控制頁面的幀刷新渲染,讓動畫效果更加流暢,當(dāng)然它的作用不僅僅局限于動畫制作岸售,我們可以利用它的特性將它視為一個(gè)定時(shí)器践樱。(當(dāng)然它不是定時(shí)器)

通常來說,rAF 被調(diào)用的頻率是每秒 60 次凸丸,也就是 1000/60 拷邢,觸發(fā)頻率大概是 16.7ms 。(當(dāng)執(zhí)行復(fù)雜操作時(shí)屎慢,當(dāng)它發(fā)現(xiàn)無法維持 60fps 的頻率時(shí)瞭稼,它會把頻率降低到 30fps 來保持幀數(shù)的穩(wěn)定。)

簡單而言腻惠,使用 requestAnimationFrame 來觸發(fā)滾動事件环肘,相當(dāng)于上面的:

throttle(func, xx, 1000/60) //xx 代表 xx ms內(nèi)不會重復(fù)觸發(fā)事件 handler

簡單的示例如下:

var ticking = false; // rAF 觸發(fā)鎖
function onScroll(){
  if(!ticking) {
    requestAnimationFrame(realFunc);
    ticking = true;
  }
}
function realFunc(){
    // do something...
    console.log("Success");
    ticking = false;
}
// 滾動事件監(jiān)聽
window.addEventListener('scroll', onScroll, false);

上面簡單的使用 rAF 的例子可以拿到瀏覽器下試一下,大概功能就是在滾動的過程中集灌,保持以 16.7ms 的頻率觸發(fā)事件 handler悔雹。

使用 requestAnimationFrame 優(yōu)缺點(diǎn)并存,首先我們不得不考慮它的兼容問題欣喧,其次因?yàn)樗荒軐?shí)現(xiàn)以 16.7ms 的頻率來觸發(fā)腌零,代表它的可調(diào)節(jié)性十分差。但是相比 throttle(func, xx, 16.7) 续誉,用于更復(fù)雜的場景時(shí)莱没,rAF 可能效果更佳,性能更好酷鸦。

總結(jié)一下

**1. **防抖動:防抖技術(shù)即是可以把多個(gè)順序地調(diào)用合并成一次饰躲,也就是在一定時(shí)間內(nèi),規(guī)定事件被觸發(fā)的次數(shù)臼隔。
**2. **節(jié)流函數(shù):只允許一個(gè)函數(shù)在 X 毫秒內(nèi)執(zhí)行一次嘹裂,只有當(dāng)上一次函數(shù)執(zhí)行后過了你規(guī)定的時(shí)間間隔,才能進(jìn)行下一次該函數(shù)的調(diào)用摔握。
**3. **rAF:16.7ms 觸發(fā)一次 handler寄狼,降低了可控性,但是提升了性能和精確度氨淌。

簡化 scroll 內(nèi)的操作
上面介紹的方法都是如何去優(yōu)化 scroll 事件的觸發(fā)泊愧,避免 scroll 事件過度消耗資源的。

但是從本質(zhì)上而言盛正,我們應(yīng)該盡量去精簡 scroll 事件的 handler 删咱,將一些變量的初始化、不依賴于滾動位置變化的計(jì)算等都應(yīng)當(dāng)在 scroll 事件外提前就緒豪筝。

建議如下:

避免在scroll 事件中修改樣式屬性 / 將樣式操作從 scroll 事件中剝離


輸入事件處理函數(shù)痰滋,比如 scroll / touch 事件的處理摘能,都會在 requestAnimationFrame 之前被調(diào)用執(zhí)行。

因此敲街,如果你在 scroll 事件的處理函數(shù)中做了修改樣式屬性的操作团搞,那么這些操作會被瀏覽器暫存起來。然后在調(diào)用 requestAnimationFrame 的時(shí)候多艇,如果你在一開始做了讀取樣式屬性的操作逻恐,那么這將會導(dǎo)致觸發(fā)瀏覽器的強(qiáng)制同步布局。

滑動過程中嘗試使用 pointer-events: none 禁止鼠標(biāo)事件
大部分人可能都不認(rèn)識這個(gè)屬性墩蔓,嗯梢莽,那么它是干什么用的呢?

pointer-events 是一個(gè) CSS 屬性奸披,可以有多個(gè)不同的值昏名,屬性的一部分值僅僅與 SVG 有關(guān)聯(lián),這里我們只關(guān)注 pointer-events: none 的情況阵面,大概的意思就是禁止鼠標(biāo)行為轻局,應(yīng)用了該屬性后,譬如鼠標(biāo)點(diǎn)擊样刷,hover 等功能都將失效仑扑,即是元素不會成為鼠標(biāo)事件的 target。

可以就近 F12 打開開發(fā)者工具面板置鼻,給 標(biāo)簽添加上 pointer-events: none 樣式镇饮,然后在頁面上感受下效果,發(fā)現(xiàn)所有鼠標(biāo)事件都被禁止了箕母。

那么它有什么用呢储藐?

pointer-events: none 可用來提高滾動時(shí)的幀頻。的確嘶是,當(dāng)滾動時(shí)钙勃,鼠標(biāo)懸停在某些元素上,則觸發(fā)其上的 hover 效果聂喇,然而這些影響通常不被用戶注意辖源,并多半導(dǎo)致滾動出現(xiàn)問題。對 body 元素應(yīng)用 pointer-events: none 希太,禁用了包括 hover 在內(nèi)的鼠標(biāo)事件克饶,從而提高滾動性能。

.disable-hover {
    pointer-events: none;
}

大概的做法就是在頁面滾動的時(shí)候, 給 添加上 .disable-hover 樣式誊辉,那么在滾動停止之前, 所有鼠標(biāo)事件都將被禁止彤路。當(dāng)滾動結(jié)束之后,再移除該屬性芥映。

可以查看這個(gè) demo頁面洲尊。

上面說 pointer-events: none 可用來提高滾動時(shí)的幀頻 的這段話摘自 pointer-events-MDN ,還專門有文章講解過這個(gè)技術(shù):

使用pointer-events:none實(shí)現(xiàn)60fps滾動 奈偏。

這就完了嗎坞嘀?沒有,張鑫旭有一篇專門的文章惊来,用來探討 pointer-events: none 是否真的能夠加速滾動性能丽涩,并提出了自己的質(zhì)疑:

pointer-events:none提高頁面滾動時(shí)候的繪制性能?

結(jié)論見仁見智裁蚁,使用 pointer-events: none 的場合要依據(jù)業(yè)務(wù)本身來定奪矢渊,拒絕拿來主義,多去源頭看看枉证,動手實(shí)踐一番再做定奪矮男。

其他參考文獻(xiàn)(都是好文章,值得一讀):

**1. **實(shí)例解析防抖動(Debouncing)和節(jié)流閥(Throttling)
**2. **無線性能優(yōu)化:Composite
**3. **Javascript高性能動畫與頁面渲染
**4. **Google Developers–渲染性能
**5. **Web高性能動畫

本文轉(zhuǎn)自

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末室谚,一起剝皮案震驚了整個(gè)濱河市毡鉴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秒赤,老刑警劉巖猪瞬,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異入篮,居然都是意外死亡陈瘦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門潮售,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痊项,“玉大人,你說我怎么就攤上這事饲做∠呋椋” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵盆均,是天一觀的道長塞弊。 經(jīng)常有香客問我,道長泪姨,這世上最難降的妖魔是什么游沿? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮肮砾,結(jié)果婚禮上诀黍,老公的妹妹穿的比我還像新娘。我一直安慰自己仗处,他們只是感情好眯勾,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布枣宫。 她就那樣靜靜地躺著,像睡著了一般吃环。 火紅的嫁衣襯著肌膚如雪也颤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天郁轻,我揣著相機(jī)與錄音翅娶,去河邊找鬼。 笑死好唯,一個(gè)胖子當(dāng)著我的面吹牛竭沫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骑篙,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蜕提,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了替蛉?” 一聲冷哼從身側(cè)響起贯溅,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躲查,沒想到半個(gè)月后它浅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镣煮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年姐霍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片典唇。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镊折,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出介衔,到底是詐尸還是另有隱情恨胚,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布炎咖,位于F島的核電站赃泡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乘盼。R本人自食惡果不足惜升熊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绸栅。 院中可真熱鬧级野,春花似錦、人聲如沸粹胯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渊抽,卻和暖如春蟆豫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背懒闷。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留栈幸,地道東北人愤估。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像速址,于是被迫代替她去往敵國和親玩焰。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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