<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          「React18新特性」深入淺出用戶體驗—transition

          共 11237字,需瀏覽 23分鐘

           ·

          2021-12-25 10:01

          在 React 18 中,引進(jìn)了一個新的 API —— startTransition 還有二個新的 hooks —— useTransitionuseDeferredValue,本質(zhì)上它們離不開一個概念 transition

          什么叫做 transition 英文翻譯為 ‘過渡’,那么這里的過渡指的就是在一次更新中,數(shù)據(jù)展現(xiàn)從無到有的過渡效果。用 ReactWg 中的一句話描述 startTransition 。

          在大屏幕視圖更新的時,startTransition 能夠保持頁面有響應(yīng),這個 api 能夠把 React 更新標(biāo)記成一個特殊的更新類型 transitions ,在這種特殊的更新下,React 能夠保持視覺反饋和瀏覽器的正常響應(yīng)。

          單單從上述對 startTransition 的描述,我們很難理解這個新的 api 到底解決什么問題。不過不要緊,接下來讓我逐步分析這個 api 到底做了什么,以及它的應(yīng)用場景。

          二 transition 使命

          1 transition 的誕生

          為什么會出現(xiàn) Transition 呢?Transition 本質(zhì)上解決了渲染并發(fā)的問題,在 React 18 關(guān)于 startTransition 描述的時候,多次提到 ‘大屏幕’ 的情況,這里的大屏幕并不是單純指的是尺寸,而是一種數(shù)據(jù)量大,DOM 元素節(jié)點多的場景,比如數(shù)據(jù)可視化大屏情況,在這一場景下,一次更新帶來的變化可能是巨大的,所以頻繁的更新,執(zhí)行 js 事務(wù)頻繁調(diào)用,瀏覽器要執(zhí)行大量的渲染工作,所以給用戶感覺就是卡頓。

          Transition 本質(zhì)上是用于一些不是很急迫的更新上,在 React 18 之前,所有的更新任務(wù)都被視為急迫的任務(wù),在 React 18 誕生了 concurrent Mode 模式,在這個模式下,渲染是可以中斷,低優(yōu)先級任務(wù),可以讓高優(yōu)先級的任務(wù)先更新渲染。可以說 React 18 更青睞于良好的用戶體驗。從 ?concurrent Modesusponse 再到 startTransition 無疑都是圍繞著更優(yōu)質(zhì)的用戶體驗展開。

          startTransition 依賴于 concurrent Mode 渲染并發(fā)模式。也就是說在 React 18 中使用 startTransition ,那么要先開啟并發(fā)模式,也就是需要通過 createRoot 創(chuàng)建 Root 。我們先來看一下兩種模式下,創(chuàng)建 Root 區(qū)別。

          傳統(tǒng) legacy 模式

          import?ReactDOM?from?'react-dom'
          /*?通過?ReactDOM.render??*/
          ReactDOM.render(
          ????<App?/>,
          ????document.getElementById('app')
          )

          v18 concurrent Mode并發(fā)模式

          import?ReactDOM?from?'react-dom'
          /*?通過?createRoot?創(chuàng)建?root?*/
          const?root?=??ReactDOM.createRoot(document.getElementById('app'))
          /*?調(diào)用?root?的?render?方法?*/
          root.render(<App/>)

          上面說了 startTransition 使用條件,接下來探討一下 startTransition 到底應(yīng)用于什么場景。前面說了 React 18 確定了不同優(yōu)先級的更新任務(wù),為什么會有不同優(yōu)先級的任務(wù)。世界上本來沒有路,走的人多了就成了路,優(yōu)先級產(chǎn)生也是如此,React 世界里本來沒有優(yōu)先級,場景多了就出現(xiàn)了優(yōu)先級。

          如果一次更新中,都是同樣的任務(wù),那么也就無任務(wù)優(yōu)先級可言,統(tǒng)一按批次處理任務(wù)就可以了,可現(xiàn)實恰好不是這樣子。舉一個很常見的場景:就是有一個 input 表單。并且有一個大量數(shù)據(jù)的列表,通過表單輸入內(nèi)容,對列表數(shù)據(jù)進(jìn)行搜索,過濾。那么在這種情況下,就存在了多個并發(fā)的更新任務(wù)。分別為

          • 第一種:input 表單要實時獲取狀態(tài),所以是受控的,那么更新 input 的內(nèi)容,就要觸發(fā)更新任務(wù)。
          • 第二種:input 內(nèi)容改變,過濾列表,重新渲染列表也是一個任務(wù)。

          第一種類型的更新,在輸入的時候,希望是的視覺上馬上呈現(xiàn)變化,如果輸入的時候,輸入的內(nèi)容延時顯示,會給用戶一種極差的視覺體驗。第二種類型的更新就是根據(jù)數(shù)據(jù)的內(nèi)容,去過濾列表中的數(shù)據(jù),渲染列表,這個種類的更新,和上一種比起來優(yōu)先級就沒有那么高。那么如果 input 搜索過程中用戶更優(yōu)先希望的是輸入框的狀態(tài)改變,那么正常情況下,在 input 中綁定 onChange 事件用來觸發(fā)上述的兩種類的更新。

          const?handleChange=(e)=>{
          ???/*?改變搜索條件?*/?
          ???setInputValue(e.target.value)
          ???/*?改變搜索過濾后列表狀態(tài)?*/
          ???setSearchQuery(e.target.value)
          }

          上述這種寫法,那么 setInputValuesetSearchQuery 帶來的更新就是一個相同優(yōu)先級的更新。而前面說道,輸入框狀態(tài)改變更新優(yōu)先級要大于列表的更新的優(yōu)先級。 ,這個時候我們的主角就登場了。用 startTransition 把兩種更新區(qū)別開。

          const?handleChange=()=>{
          ????/*?高優(yōu)先級任務(wù)?——?改變搜索條件?*/
          ????setInputValue(e.target.value)
          ????/*?低優(yōu)先級任務(wù)?——?改變搜索過濾后列表狀態(tài)??*/
          ????startTransition(()=>{
          ????????setSearchQuery(e.target.value)
          ????})
          }
          • 如上通過 startTransition 把不是特別迫切的更新任務(wù) setSearchQuery ?隔離出來。這樣在真實的情景效果如何呢?我們來測試一下。

          2 模擬場景

          接下來我們模擬一下上述場景。流程大致是這樣的:

          • 有一個搜索框和一個 10000 條數(shù)據(jù)的列表,列表中每一項有相同的文案。
          • input 改變要實時改變 input 的內(nèi)容(第一種更新),然后高亮列表里面的相同的搜索值(第二種更新)。
          • 用一個按鈕控制 常規(guī)模式 | transition 模式。
          /*??模擬數(shù)據(jù)??*/
          const?mockDataArray?=?new?Array(10000).fill(1)
          /*?高量顯示內(nèi)容?*/
          function?ShowText({?query?}){
          ???const?text?=?'asdfghjk'
          ???let?children
          ???if(text.indexOf(query)?>?0?){
          ???????/*?找到匹配的關(guān)鍵詞?*/
          ???????const?arr?=?text.split(query)
          ???????children?=?<div>{arr[0]}<span?style={{?color:'pink'?}}?>{query}span>{arr[1]}?div>
          ???}else{
          ??????children?=?<div>{text}div>
          ???}
          ???return?<div>{children}div>
          }
          /*?列表數(shù)據(jù)?*/
          function?List?({?query?}){
          ????console.log('List渲染')
          ????return?<div>
          ????????{
          ???????????mockDataArray.map((item,index)=><div?key={index}?>
          ??????????????<ShowText?query={query}?/>
          ???????????div>
          )
          ????????}
          ????div>
          }
          /*?memo?做優(yōu)化處理??*/
          const?NewList?=?memo(List)
          • List 組件渲染一萬個 ShowText 組件。在 ShowText 組件中會通過傳入的 query 實現(xiàn)動態(tài)高亮展示。
          • 因為每一次改變 query 都會讓 10000 個重新渲染更新,并且還要展示 query 的高亮內(nèi)容,所以滿足并發(fā)渲染的場景。

          接下來就是 App 組件編寫。

          export?default?function?App(){
          ????const?[?value?,setInputValue?]?=?React.useState('')
          ????const?[?isTransition?,?setTransion?]?=?React.useState(false)
          ????const?[?query?,setSearchQuery??]?=?React.useState('')
          ????const?handleChange?=?(e)?=>?{
          ????????/*?高優(yōu)先級任務(wù)?——?改變搜索條件?*/
          ????????setInputValue(e.target.value)
          ????????if(isTransition){?/*?transition?模式?*/
          ????????????React.startTransition(()=>{
          ????????????????/*?低優(yōu)先級任務(wù)?——?改變搜索過濾后列表狀態(tài)??*/
          ????????????????setSearchQuery(e.target.value)
          ????????????})
          ????????}else{?/*?不加優(yōu)化,傳統(tǒng)模式?*/
          ????????????setSearchQuery(e.target.value)
          ????????}
          ????}
          ????return?<div>
          ????????<button?onClick={()=>setTransion(!isTransition)}?>{isTransition???'transition'?:?'normal'}?button>

          ????????<input?onChange={handleChange}
          ????????????placeholder="輸入搜索內(nèi)容"
          ????????????value={value}
          ????????/>

          ???????<NewList??query={query}?/>
          ????div>
          }

          我們看一下 App 做了哪些事情。

          • 首先通過 handleChange 事件來處理 onchange 事件。
          • button按鈕用來切換 transition (設(shè)置優(yōu)先級) 和 normal (正常模式)。接下來就是見證神奇的時刻。

          常規(guī)模式下效果:


          • 可以清楚的看到在常規(guī)模式下,輸入內(nèi)容,內(nèi)容呈現(xiàn)都變的異常卡頓,給人一種極差的用戶體驗。

          transtion 模式下效果:


          • 把大量并發(fā)任務(wù)通過 startTransition 處理之后,可以清楚看到,input 會正常的呈現(xiàn),更新列表任務(wù)變得滯后,不過用戶體驗大幅度提升,

          整體效果:


          • 來感受一些 startTransition 的魅力。

          總結(jié): 通過上面可以直觀的看到 startTransition 在處理過渡任務(wù),優(yōu)化用戶體驗上起到了舉足輕重的作用。

          3 為什么不是 setTimeout

          上述的問題能夠把 setSearchQuery 的更新包裝在 setTimeout 內(nèi)部呢,像如下這樣。

          const?handleChange=()=>{
          ????/*?高優(yōu)先級任務(wù)?——?改變搜索條件?*/
          ????setInputValue(e.target.value)
          ????/*?把?setSearchQuery?通過延時器包裹??*/
          ????setTimeout(()=>{
          ????????setSearchQuery(e.target.value)
          ????},0)
          }
          • 這里通過 setTimeout ,把更新放在 setTimeout 內(nèi)部,那么我們都知道 setTimeout 是屬于延時器任務(wù),它不會阻塞瀏覽器的正常繪制,瀏覽器會在下次空閑時間之行 setTimeout 。那么效果如何呢?我們來看一下:
          4.gif
          • 如上可以看到,通過 setTimeout 確實可以讓輸入狀態(tài)好一些,但是由于 setTimeout 本身也是一個宏任務(wù),而每一次觸發(fā) onchange 也是宏任務(wù),所以 setTimeout 還會影響頁面的交互體驗。

          綜上所述,startTransition 相比 setTimeout 的優(yōu)勢和異同是:

          • 一方面:startTransition 的處理邏輯和 setTimeout 有一個很重要的區(qū)別,setTimeout 是異步延時執(zhí)行,而 startTransition 的回調(diào)函數(shù)是同步執(zhí)行的。在 startTransition 之中任何更新,都會標(biāo)記上 transition,React 將在更新的時候,判斷這個標(biāo)記來決定是否完成此次更新。所以 Transition 可以理解成比 setTimeout 更早的更新。但是同時要保證 ui 的正常響應(yīng),在性能好的設(shè)備上,transition 兩次更新的延遲會很小,但是在慢的設(shè)備上,延時會很大,但是不會影響 UI 的響應(yīng)。

          • 另一方面,就是通過上面例子,可以看到,對于渲染并發(fā)的場景下,setTimeout 仍然會使頁面卡頓。因為超時后,還會執(zhí)行 setTimeout 的任務(wù),它們與用戶交互同樣屬于宏任務(wù),所以仍然會阻止頁面的交互。那么 transition 就不同了,在 conCurrent mode 下,startTransition 是可以中斷渲染的 ,所以它不會讓頁面卡頓,React 讓這些任務(wù),在瀏覽器空閑時間執(zhí)行,所以上述輸入 input 內(nèi)容時,startTransition 會優(yōu)先處理 input 值的更新,而之后才是列表的渲染。

          4 為什么不是節(jié)流防抖

          那么我們再想一個問題,為什么不是節(jié)流和防抖。首先節(jié)流和防抖能夠解決卡頓的問題嗎?答案是一定的,在沒有 transition 這樣的 api 之前,就只能通過防抖節(jié)流來處理這件事,接下來用防抖處理一下。

          const?SetSearchQueryDebounce?=?useMemo(()=>?debounce((value)=>?setSearchQuery(value),1000)??,[])
          const?handleChange?=?(e)?=>?{
          ????setInputValue(e.target.value)
          ????/*?通過防抖處理后的 setSearchQuery 函數(shù)。??*/
          ????SetSearchQueryDebounce(e.target.value)
          }
          • 如上將 setSearchQuery 防抖處理。然后我們看一下效果。


          通過上面可以直觀感受到通過防抖處理后,基本上已經(jīng)不影響 input 輸入了。但是面臨一個問題就是 list 視圖改變的延時時間變長了。那么 transition 和節(jié)流防抖 本質(zhì)上的區(qū)別是:

          • 一方面,節(jié)流防抖 本質(zhì)上也是 setTimeout ,只不過控制了執(zhí)行的頻率,那么通過打印的內(nèi)容就能發(fā)現(xiàn),原理就是讓 render 次數(shù)減少了。而 transitions 和它相比,并沒有減少渲染的次數(shù)。

          • 另一方面,節(jié)流和防抖需要有效掌握 Delay Time 延時時間,如果時間過長,那么給人一種渲染滯后的感覺,如果時間過短,那么就類似于 setTimeout(fn,0) 還會造成前面的問題。而 startTransition 就不需要考慮這么多。

          5 受到計算機(jī)性能影響

          transition 在處理慢的計算機(jī)上效果更加明顯,我們來看一下 Real world example

          注意看滑塊速度

          • 處理性能高,更快速的設(shè)備上。不使用 startTransition 。

          • 處理性能高,更快速的設(shè)備上。使用 startTransition。


          • 處理性能差,慢速的設(shè)備上,不使用 startTransition。


          • 處理性能差,慢速的設(shè)備上,使用 startTransition。


          三 transition 特性

          既然已經(jīng)講了 transition 的產(chǎn)生初衷,接下來看 transition 的功能介紹 。

          1 什么是過度任務(wù)。

          一般會把狀態(tài)更新分為兩類:

          • 第一類緊急更新任務(wù)。比如一些用戶交互行為,按鍵,點擊,輸入等。
          • 第二類就是過渡更新任務(wù)。比如 UI 從一個視圖過渡到另外一個視圖。

          2 什么是 startTransition

          上邊已經(jīng)用了 startTransition 開啟過度任務(wù),對于 startTransition 的用法,相信很多同學(xué)已經(jīng)清楚了。

          startTransition(scope)
          • scope 是一個回調(diào)函數(shù),里面的更新任務(wù)都會被標(biāo)記成過渡更新任務(wù),過渡更新任務(wù)在渲染并發(fā)場景下,會被降級更新優(yōu)先級,中斷更新。

          使用

          startTransition(()=>{
          ???/*?更新任務(wù)?*/
          ???setSearchQuery(value)
          })

          3 什么是 useTranstion

          上面介紹了 startTransition ,又講到了過渡任務(wù),本質(zhì)上過渡任務(wù)有一個過渡期,在這個期間當(dāng)前任務(wù)本質(zhì)上是被中斷的,那么在過渡期間,應(yīng)該如何處理呢,或者說告訴用戶什么時候過渡任務(wù)處于 pending 狀態(tài),什么時候 pending 狀態(tài)完畢。

          為了解決這個問題,React 提供了一個帶有 isPending 狀態(tài)的 hooks —— useTransition 。useTransition 執(zhí)行返回一個數(shù)組。數(shù)組有兩個狀態(tài)值:

          • 第一個是,當(dāng)處于過渡狀態(tài)的標(biāo)志——isPending。
          • 第二個是一個方法,可以理解為上述的 startTransition。可以把里面的更新任務(wù)變成過渡任務(wù)。
          import?{?useTransition?}?from?'react'?

          /*?使用?*/
          const??[?isPending?,?startTransition?]?=?useTransition?()

          那么當(dāng)任務(wù)處于懸停狀態(tài)的時候,isPendingtrue,可以作為用戶等待的 UI 呈現(xiàn)。比如:

          {?isPending??&&???}

          useTranstion 實踐

          接下來我們做一個 useTranstion 的實踐,還是復(fù)用上述 demo 。對上述 demo 改造。

          export?default?function?App(){
          ????const?[?value?,setInputValue?]?=?React.useState('')
          ????const?[?query?,setSearchQuery??]?=?React.useState('')
          ????const?[?isPending?,?startTransition?]?=?React.useTransition()
          ????const?handleChange?=?(e)?=>?{
          ????????setInputValue(e.target.value)
          ????????startTransition(()=>{
          ????????????setSearchQuery(e.target.value)
          ????????})
          ????}
          ????return??<div>
          ????{isPending?&&?<span>isTransitonspan>
          }
          ????<input?onChange={handleChange}
          ????????placeholder="輸入搜索內(nèi)容"
          ????????value={value}
          ????/>

          ???<NewList??query={query}?/>
          div>
          }
          • 如上用 useTransitionisPending 代表過渡狀態(tài),當(dāng)處于過渡狀態(tài)時候,顯示 isTransiton 提示。

          接下來看一下效果:





          可以看到能夠準(zhǔn)確捕獲到過渡期間的狀態(tài)。

          4 什么是 useDeferredValue

          如上場景我們發(fā)現(xiàn),本質(zhì)上 query 也是 value ,不過 query 的更新要滯后于 value 的更新。那么 React 18 提供了 useDeferredValue 可以讓狀態(tài)滯后派生。useDeferredValue 的實現(xiàn)效果也類似于 transtion,當(dāng)迫切的任務(wù)執(zhí)行后,再得到新的狀態(tài),而這個新的狀態(tài)就稱之為 DeferredValue 。

          useDeferredValue 和上述 useTransition 本質(zhì)上有什么異同呢?

          相同點:

          • useDeferredValue 本質(zhì)上和內(nèi)部實現(xiàn)與 useTransition ?一樣都是標(biāo)記成了過渡更新任務(wù)。

          不同點:

          • useTransition 是把 startTransition 內(nèi)部的更新任務(wù)變成了過渡任務(wù)transtion,而 useDeferredValue 是把原值通過過渡任務(wù)得到新的值,這個值作為延時狀態(tài)。 一個是處理一段邏輯,另一個是生產(chǎn)一個新的狀態(tài)。
          • useDeferredValue 還有一個不同點就是這個任務(wù),本質(zhì)上在 useEffect 內(nèi)部執(zhí)行,而 useEffect 內(nèi)部邏輯是異步執(zhí)行的 ,所以它一定程度上更滯后于 useTransitionuseDeferredValue = useEffect + transtion

          那么回到 demo 上來,似乎 query 變成 DeferredValue 更適合現(xiàn)實情況,那么對 demo 進(jìn)行修改。

          export?default?function?App(){
          ????const?[?value?,setInputValue?]?=?React.useState('')
          ????const?query?=?React.useDeferredValue(value)
          ????const?handleChange?=?(e)?=>?{
          ????????setInputValue(e.target.value)
          ????}
          ????return??<div>
          ?????<button>useDeferredValuebutton>

          ????<input?onChange={handleChange}
          ????????placeholder="輸入搜索內(nèi)容"
          ????????value={value}
          ????/>

          ???<NewList??query={query}?/>
          ???div>
          }
          • 如上可以看到 query 是 value 通過 useDeferredValue 產(chǎn)生的。

          效果:

          7.gif

          四 原理

          接下來又到了原理環(huán)節(jié),從 startTransition 到 useTranstion 再到 useDeferredValue 原理本質(zhì)上很簡單,

          1 startTransition

          首先看一下最基礎(chǔ)的 startTransition 是如何實現(xiàn)的。

          react/src/ReactStartTransition.js -> startTransition

          export?function?startTransition(scope)?{
          ??const?prevTransition?=?ReactCurrentBatchConfig.transition;
          ??/*?通過設(shè)置狀態(tài)?*/
          ??ReactCurrentBatchConfig.transition?=?1;
          ??try?{??
          ??????/*?執(zhí)行更新?*/
          ????scope();
          ??}?finally?{
          ????/*?恢復(fù)狀態(tài)?*/??
          ????ReactCurrentBatchConfig.transition?=?prevTransition;
          ??}
          }
          • startTransition 原理特別簡單,有點像 React v17 中 batchUpdate 的批量處理邏輯。就是通過設(shè)置開關(guān)的方式,而開關(guān)就是 transition = 1 ,然后執(zhí)行更新,里面的更新任務(wù)都會獲得 transtion 標(biāo)志。

          • 接下來在 concurrent mode 模式下會單獨處理 transtion 類型的更新。

          其原理圖如下所示。

          9.jpg

          2 useTranstion

          接下來看一下 useTranstion 的內(nèi)部實現(xiàn)。

          react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

          function?mountTransition(){
          ????const?[isPending,?setPending]?=?mountState(false);
          ????const?start?=?(callback)=>{
          ????????setPending(true);
          ????????const?prevTransition?=?ReactCurrentBatchConfig.transition;
          ????????ReactCurrentBatchConfig.transition?=?1;
          ????????try?{
          ????????????setPending(false);
          ????????????callback();
          ????????}?finally?{
          ????????????ReactCurrentBatchConfig.transition?=?prevTransition;
          ????????}
          ????}
          ?????return?[isPending,?start];
          }

          這段代碼不是源碼,我把源碼里面的內(nèi)容進(jìn)行組合,壓縮。

          • 從上面可以看到,useTranstion 本質(zhì)上就是 useState + ?startTransition
          • 通過 useState 來改變 pending 狀態(tài)。在 mountTransition 執(zhí)行過程中,會觸發(fā)兩次 setPending ,一次在 transition = 1 之前,一次在之后。一次會正常更新 setPending(true) ,一次會作為 transition 過渡任務(wù)更新 setPending(false); ,所以能夠精準(zhǔn)捕獲到過渡時間。

          其原理圖如下所示。

          10.jpg

          3 useDeferredValue

          最后,讓我們看一下 useDeferredValue 的內(nèi)部實現(xiàn)原理。

          react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

          function?updateDeferredValue(value){
          ??const?[prevValue,?setValue]?=?updateState(value);
          ??updateEffect(()?=>?{
          ????const?prevTransition?=?ReactCurrentBatchConfig.transition;
          ????ReactCurrentBatchConfig.transition?=?1;
          ????try?{
          ??????setValue(value);
          ????}?finally?{
          ??????ReactCurrentBatchConfig.transition?=?prevTransition;
          ????}
          ??},?[value]);
          ??return?prevValue;
          }

          useDeferredValue 處理流程是這樣的。

          • 從上面可以看到 useDeferredValue 本質(zhì)上是 useDeferredValue = useState + useEffect + transition
          • 通過傳入 useDeferredValue 的 value 值,useDeferredValue 通過 useState 保存狀態(tài)。
          • 然后在 useEffect 中通過 transition 模式來更新 value 。這樣保證了 DeferredValue 滯后于 state 的更新,并且滿足 transition ?過渡更新原則。

          其原理圖如下所示。

          11.jpg

          四 總結(jié)

          本章節(jié)講到的知識點如下:

          • Transition 產(chǎn)生初衷,解決了什么問題。
          • startTransition 的用法和原理。
          • useTranstion 的用法和原理。
          • useDeferredValue 的用法和原理。

          感興趣的同學(xué)可以是一下

          參考文檔

          • New feature: startTransition

          • Real world example: adding startTransition for slow renders

          瀏覽 105
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲激情小说区 | aⅴ视频 | 色秘 乱码一区二区三区在线男奴 | 亚洲无视频| 蜜桃Av久久精品人人槡 |