<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>

          你可能不知道的流式 React Hooks(關(guān)于組織代碼的最佳實(shí)踐)

          共 20346字,需瀏覽 41分鐘

           ·

          2021-06-18 13:06

          這次的分享結(jié)合我在項(xiàng)目中使用 full hooks-based React Components 的一些經(jīng)驗(yàn),給大家介紹一些我所認(rèn)為的 React Hooks 最佳實(shí)踐。

          文中的很多 term 是為了闡明一些概念所設(shè),并非專有名詞,不需要當(dāng)真。

          回顧一下 React Hooks

          首先還是簡(jiǎn)單回顧一下 React Hooks。

          先看傳統(tǒng)的 React Class-based Component。一個(gè)組件由四部分構(gòu)成:

          • 狀態(tài) state:一個(gè)統(tǒng)一的集中的 state
          • 生命周期回調(diào) lifecycle methods:一些需要誦記的生命周期相關(guān)的回調(diào)函數(shù)(WillMount / Mounted / WillReceiveProps / Updated / WillUnmount 等等)
          • 回調(diào)函數(shù) handlers:一些回調(diào)方法,在 view 層被調(diào)用,作用在 state 層上
          • 渲染函數(shù) render:即組件的 view 層,負(fù)責(zé)產(chǎn)出組件的 VirtualDOM 節(jié)點(diǎn),掛載回調(diào)函數(shù)等

          React Hooks 組件其實(shí)可以簡(jiǎn)單地理解成一個(gè) render 函數(shù)。這個(gè) render 函數(shù)本身即組件。他通過 useState 和 useEffect 兩個(gè)函數(shù)來實(shí)現(xiàn)函數(shù)的“狀態(tài)化”,即獲得對(duì) state 和生命周期的注冊(cè)和訪問能力。

          Hooks 是一套新的框架

          相比類組件,Hooks 組件有以下特點(diǎn)

          • 自上而下:相比類組件的方法之間相互調(diào)用,作為函數(shù)的 Hooks 組件的具有更單純的邏輯流,即自上而下
          • 弱化 handlers:相比類組件的方法注冊(cè),函數(shù)式組件雖然也可以實(shí)現(xiàn)在函數(shù)上下文中聲明回調(diào),但這對(duì)不如類方法來得自然。
          • 簡(jiǎn)化生命周期:Hooks 通過一個(gè)單純的 useEffect 來注冊(cè)基于依賴變更的生命周期函數(shù),把類組件中的生命周期都混合在一起。事實(shí)上,我們完全可以徹底拋棄對(duì)原先 React 組件生命周期的理解,直接來理解 useEffect,把他單純地當(dāng)成在 render 過程中注冊(cè)的函數(shù)副作用。
          • 分散的 state 和 effect 注冊(cè)和訪問:Hooks 不再像類組件一般要求在統(tǒng)一的地方注冊(cè)組件用到的所有狀態(tài)以及生命周期方法,這使得更「模型內(nèi)聚」的邏輯組織成為可能。
          • 依賴驅(qū)動(dòng):多個(gè)基礎(chǔ) Hooks 在設(shè)計(jì)上都有 deps 的概念,用以實(shí)現(xiàn)基于依賴項(xiàng)的變更來執(zhí)行所聲明的函數(shù)的能力

          基于上述迥異的語法和完全平行的 API,基于 Hooks 的組件書寫可以被當(dāng)作一門獨(dú)立于基于類組件的全新框架。我們應(yīng)盡量避免以模仿類組件的風(fēng)格去書寫 Hooks 組件的邏輯,而應(yīng)當(dāng)重新審視這種新的語法。

          由于上述的語法特點(diǎn),Hooks 適合通過「基于變更」的聲明風(fēng)格來書寫,而非「基于回調(diào)」的命令式方式來書寫。這會(huì)讓一個(gè)組件更易于拆分和復(fù)用邏輯并擁有更清晰的邏輯依賴關(guān)系。大家將逐步看到「基于變更」的風(fēng)格的優(yōu)勢(shì),下面小舉兩個(gè)例子來對(duì)比一下「基于變更」和「基于回調(diào)」的寫法:

          例一:通過 useEffect 聲明請(qǐng)求

          需求場(chǎng)景:更改一個(gè) keyword state 并發(fā)起查詢的請(qǐng)求

          基于回調(diào)的寫法(仿類寫法)

          const Demo: React.FC = () => {
              const [state, setState] = useState({
                  keyword'',
              });
              const query = useCallback((queryState: typeof state) => {
                  // ...
              }, []);
              const handleKeywordChange = useCallback((e: React.InputEvent) => {
                  const latestState = { ...state, keyword: e.target.value };
                  setState(latestState);
                  query(latestState);
              }, [state, query]);
              return // view
          }

          這種寫法有幾個(gè)問題:

          • handleKeywordChange 若在兩次渲染中被多次調(diào)用,會(huì)出現(xiàn) state 過舊的問題,從而得到的 latestState 將不是最新的,會(huì)產(chǎn)生bug。(這個(gè)問題類組件也會(huì)存在)
          • query 方法每次都需要在 handler 中被命令式地調(diào)用,如果需要調(diào)用它的 handler 變多,則依賴關(guān)系語法復(fù)雜,且容易疏忽忘記手動(dòng)調(diào)用。
          • query 使用的 queryState 就是最新的 state,卻每次需要由 handler 將 state 計(jì)算好交給 query 函數(shù),方法間職責(zé)分割得不明確。

          基于變更的寫法

          const Demo: React.FC = () => {
              const [state, setState] = useState({
                  keyword'',
              });
              const handleKeywordChange = useCallback((e: React.InputEvent) => 
                  {
                      const nextKeyword = e.target.value;
                      setState(prev => ({ ...prev, keyword: nextKeyword }))
                  }, []);
              useEffect(() => {
                  // query
              }, [state]);
              return // view
          }

          上面的寫法解決了「基于回調(diào)」寫法的所有問題。它把 state 作為了 query 的依賴,只要 state 發(fā)生變更,query 就會(huì)自動(dòng)執(zhí)行,且執(zhí)行時(shí)機(jī)一定是在 state 變更以后。我們沒有命令式地調(diào)用 query,而是聲明了在什么情況下它應(yīng)當(dāng)被調(diào)用。

          當(dāng)然這種寫法也不是沒有問題:

          • 萬一需求場(chǎng)景要求我們?cè)?state 的某些特定字段變更的時(shí)候不觸發(fā) query,上面的寫法就失效了

          事實(shí)上,這個(gè)問題恰恰要求我們?cè)趯?Hooks 時(shí)花更多的精力專注于「變」與「不變」的管理,而不是「調(diào)」與「不調(diào)」的管理上。

          例二:注冊(cè)對(duì) window size 的監(jiān)聽

          需求場(chǎng)景:在 window resize 時(shí)觸發(fā) callback 函數(shù)

          基于回調(diào)的寫法(仿類寫法)

          const Demo: FC = () => {
              const callback = // ...
              useEffect(() => {
                  window.addEventListener('resize', callback);
                  return () => window.removeEventListener('resize', callback);
              }, []);
              return // view
          }

          在「componentDidMount」的時(shí)候注冊(cè)這個(gè)監(jiān)聽,在「componentWillUnmount」的時(shí)候注銷它。很單純啊是不是?

          但是問題來了,在類組件中,callback 可以是一個(gè)類方法(method),它的引用在整個(gè)組件生命周期中都不會(huì)發(fā)生改變。但是函數(shù)式組件中的 callback 是在每次執(zhí)行的上下文中生成的,它極有可能每次都不一樣!這樣 window 對(duì)象上掛載的監(jiān)聽將會(huì)是組件第一次執(zhí)行產(chǎn)生的 callback,之后所有執(zhí)行輪次中產(chǎn)生的 callback 都將不會(huì)被掛載到 window 的訂閱者中,bug 就出現(xiàn)了。

          那改一下?

          基于回調(diào)的寫法2
          const Demo: FC = () => {
              const callback = // ...
              useEffect(() => {
                  window.addEventListener('resize', callback);
                  return () => window.removeEventListener('resize', callback);
              }, [callback]);
              return // view
          }

          這樣把 callback 放到注冊(cè)監(jiān)聽的 effect 的依賴中看起來似乎能 work,但是也太不優(yōu)雅了。在組件的執(zhí)行過程中,我們將瘋狂地在 window 對(duì)象上注冊(cè)注銷注冊(cè)注銷,聽起來就不太合理。下面看看基于變更的寫法:

          基于變更的寫法
          const Demo: FC = () => {
              const [windowSize, setWindowSize] = useState([
                  window.innerWidth,
                  window.innerHeight
              ] as const);
              useEffect(() => {
                  const handleResize = () => {
                      setWindowSize([window.innerWidth, window.innerHeight]);
                  }
                  window.addEventListener('resize', handleResize);
                  return () => window.removeEventListener('resize', handleResize);
              }, []);
              const callback = // ...
              useEffect(callback, [windowSize]);
              return // view
          };

          這里我們通過一個(gè) useState 和一個(gè) useEffect 首先把 window resize 從一個(gè)回調(diào)的注冊(cè)注銷過程轉(zhuǎn)換成了一個(gè)表示 window size 的 state。之后依賴這個(gè) state 的變更實(shí)現(xiàn)了對(duì) callback 的調(diào)用。這個(gè)調(diào)用同樣是聲明式的,而不是直接手動(dòng)命令式的調(diào)用的,而聲明式往往意味著更好的可測(cè)性。

          上面的代碼看似更復(fù)雜了,但事實(shí)上,只要我們把 2-10 行的代碼抽離出來,很快就得到了一個(gè)跨組件可復(fù)用的自定義 Hooks:useWindowSize。使得在別的組件中使用基于 window resize 的回調(diào)變得非常方便:

          const useWindowSize = () => {
              const [windowSize, setWindowSize] = useState([window.innerWidth, window.innerHeight] as const);
              useEffect(() => {
                  const handleResize = () => {
                      setWindowSize([window.innerWidth, window.innerHeight]);
                  }
                  window.addEventListener('resize', handleResize);
                  return () => window.removeEventListener('resize', handleResize);
              }, []);
              return windowSize
          }

          基于變更的寫法的關(guān)鍵在于把「 動(dòng)作」轉(zhuǎn)換成「 狀態(tài)

          Marble Diagrams

          通過上面的論述和例子我們可以看到在 Hooks-based 組件中合理地使用基于變更的代碼可以帶來一定的好處。為了更好地理解「基于變更」這件事。這里引入流式編程中常用于輔助理解的 Marble 圖。你將很快發(fā)現(xiàn),我們一直在說的「基于變更」于流式編程中的「流」沒有兩樣:

          RxMarble圖例[1]

          流式編程中,一個(gè)珠子(marble)就代表一個(gè)推送過來的數(shù)據(jù),一串橫向的珠子就代表一個(gè)數(shù)據(jù)流(ObservableSubject)在時(shí)間上的一系列推送數(shù)據(jù)。流式編程通過一系列操作符,對(duì)數(shù)據(jù)流實(shí)現(xiàn)加工整合映射等操作來實(shí)現(xiàn)編程邏輯。上圖的 merge 操作,是非常常用的合并兩個(gè)數(shù)據(jù)源的操作符。

          不可變數(shù)據(jù)流與「執(zhí)行幀」

          基于變更的 Hooks coding 其實(shí)是與 stream coding 相當(dāng)同構(gòu)的概念。兩者都弱化 callback,把 callback 包裝起來成為流或操作符。

          Hooks 組件中的一個(gè) state 就是流式編程中的流,即一串珠子

          而一個(gè) state 的每一次變更,便是一顆珠子

          不可變數(shù)據(jù)流 immutable dataflow

          為了完全地體現(xiàn)「變更」,所有的狀態(tài)更新都要做到 immutable 簡(jiǎn)而言之:讓引用的變化與值的變化完全一致

          為了實(shí)現(xiàn)這一點(diǎn),你可以:

          1. 每次 setState 的時(shí)候注意
          2. 自己實(shí)現(xiàn)一些 immutable utils
          3. 借助第三方的數(shù)據(jù)結(jié)構(gòu)庫(kù),如 facebook的 ImmutableJS[2]

          (個(gè)人推薦 1 或 2,可以盡可能減少引入不必要的概念)

          執(zhí)行幀

          在 Hooks-based 編程中,我們還要有所謂「執(zhí)行幀」的概念。這種概念在其他框架如 vue / Angular 中很被弱化,而對(duì) React 尤其是函數(shù)式組件中卻很有助于思考在組件上下文中的 state 或 props 一旦發(fā)生變更,就會(huì)觸發(fā)組件的執(zhí)行。每次執(zhí)行就相當(dāng)于一幀渲染的繪制。所有的 marble 就串在執(zhí)行幀與狀態(tài)構(gòu)成的網(wǎng)格中

          變更的源頭

          對(duì)一個(gè)組件來說,能觸發(fā)它重新渲染的變更稱為「源」source。一個(gè)組件的變更源一般有以下幾種:

          • props 變更:即父組件傳遞給組件的 props 發(fā)生變更
          • 事件 event:如點(diǎn)擊,如上文的 window resize 事件。對(duì)事件,需要將事件回調(diào)包裝成 state
          • 調(diào)度器:即 animationFrame / interval / timeout

          上述源頭,有些已經(jīng)被「marble化」了,如 props。有些還沒有,需要我們包裝的方式把他們「marble 化」

          例一:對(duì)事件的包裝

          const useClickEvent = () => {
              const [clickEvent, setClickEvent] = useState<{ x: number; y: number; }>(null);
              const dispatch = useCallback((e: React.MouseEvent) => {
                  setClickEvent({ x: e.clientX, y: e.clientY });
              }, []);
              return [clickEvent, dispatch] as const;
          }

          例二:對(duì)調(diào)度器的包裝(以 interval 為例)

          const useInterval = (interval: number) => {
              const [intervalCount, setIntervalCount] = useState();
              useEffect(() => {
                  const intervalId = setInterval(() => {
                      setIntervalCount(count => count + 1)
                  });
                  return () => clearInterval(intervalId);
              }, []);
              return intervalCount;
          };

          流式操作符

          從源變更到最終 view 層需要的數(shù)據(jù)狀態(tài),一個(gè)組件的數(shù)據(jù)組織可以抽象成下圖:中間的 operators 就是組件處理數(shù)據(jù)的核心邏輯。在流式編程中的 operator 幾乎都可以在 Hooks 中通過自定義 Hooks 寫出同構(gòu)的表示。

          這些「流式 Hook」是由基本 Hooks 復(fù)合而成的更高階的 Hooks,可以具有高度的復(fù)用性,使得代碼邏輯更簡(jiǎn)練。

          映射(map)

          通過 useMemo 就可以直接實(shí)現(xiàn)把一些變更整合到一起得到一個(gè)「computed」?fàn)顟B(tài)

          對(duì)應(yīng) ReactiveX 概念:map / combine / latestFrom

          const [state1, setState1] = useState(initalState1);
          const [state2, setState2] = useState(initialState2);
          const computedState = useMemo(() => {
              return Array(state2).fill(state1).join('');
          }, [state1, state2]);

          跳過前幾次(skip) / 只在前幾次響應(yīng)(take)

          有時(shí)候我們不想在第一次的時(shí)候執(zhí)行 effect 里的函數(shù),或進(jìn)行 computed 映射。可以實(shí)現(xiàn)自己實(shí)現(xiàn)的 useCountEffect / useCountMemo 來實(shí)現(xiàn)

          對(duì)應(yīng) ReactiveX 概念:take / skip

          const useCountMemo = <T>(callback: (count: number) => T, deps: any[]): T => {
              const countRef = useRef(0);
              return useMemo(() => {
                  const returnValue = callback(countRef.current);
                  countRef.current++;
                  return returnValue;
              }, deps);
          };
          export const useCountEffect = (cb: (index: number) => any, deps?: any[]) => {
              const countRef = useRef(0);
              useEffect(() => {
                  const returnValue = cb(countRef.current);
                  currentRef.current++;
                  return returnValue;        
              }, deps);
          };

          流程與調(diào)度(debounce / throttle / delay)

          在基于變更的 Hooks 組件中,debounce / throttle / delay 等操作變得非常簡(jiǎn)單。debounce / throttle / delay 的對(duì)象將不再是 callback 函數(shù)本身,而是變更的狀態(tài)

          對(duì)應(yīng) ReactiveX 的概念:debounce / delay / throttle

          const useDebounce = <T>(value: T, time = 250) => {
              const [debouncedState, setDebouncedState] = useState(null);
              useEffect(() => {
                  const timer = setTimeout(() => {
                      setDebouncedState(value);
                  }, time);
                  return () => clearTimeout(timer);
              }, [value]);
              return debouncedState;
          };
          const useThrottle = <T>(value: T, time = 250) => {
              const [throttledState, setThrottledState] = useState(null);
              const lastStamp = useRef(0);
              useEffect(() => {
                  const currentStamp = Date.now();
                  if (currentStamp - lastStamp > time) {
                      setThrottledState(value);
                      lastStamp.current = currentStamp;
                  }
              }, [value]);
              return throttledState
          }

          action / reducer 模式的異步流程

          Redux 的核心架構(gòu) action / reducer 模式在 Hooks 中的實(shí)現(xiàn)非常簡(jiǎn)單,React 甚至專門提供了一個(gè)經(jīng)過封裝的語法糖鉤子 useReducer 來實(shí)現(xiàn)這種模式。

          對(duì)于異步流程,我們同樣可以采用 action / reducer 的模式來實(shí)現(xiàn)一個(gè) useAsync 鉤子來幫助我們處理異步流程。

          這里示意的是一個(gè)最簡(jiǎn)單的基于 promise 的函數(shù)模式,類似 redux 中使用 redux-thunk 中間件。

          同時(shí),我們伴隨請(qǐng)求的數(shù)據(jù)狀態(tài)維護(hù)一組 loading / error / ready 字段,用來標(biāo)示當(dāng)前數(shù)據(jù)的狀態(tài)。

          useAsync 鉤子還可以內(nèi)置對(duì)多個(gè)異步流程的 競(jìng)爭(zhēng) / 保序 / 自動(dòng)取消 等機(jī)制的控制邏輯。

          下面示例了 useAsync 鉤子的用法,采用了 generator 來實(shí)現(xiàn)一個(gè)異步流程對(duì)狀態(tài)的多步修改。甚至可以實(shí)現(xiàn)類似 redux-saga 的復(fù)雜異步流程管理。

          const responseState = useAsync(responseInitialState, actionState, function * (action, prevState{
              switch (action?.type) {
                  case 'clear':
                      return null;
                  case 'request': {
                      const { data } = yield apiService.request(action.payload);
                      return data;
                  }
                  default:
                      return prevState;
              }
          })

          下面的代碼例舉了一個(gè)通過類「action/ reducer」模式的異步鉤子來維護(hù)一個(gè)字典類型的數(shù)據(jù)狀態(tài)的場(chǎng)景:

          // 來自 props 或 state 的 actions
          // fetch action: 獲取
          let fetchAction: {
            type'query',
            id: number;
          };

          let clearAction: {
            type'clear',
            ids: number[]; // 需要保留的 ids 
          }

          let updateAction: {
            type'update',
            id: number;
          }

          // 通過一個(gè)自定義的 merge 鉤子來保留上述三個(gè)狀態(tài)中最新變更的一個(gè)狀態(tài)
          const actions = useMerge(fetchAction, clearAction, updateAction);

          // reducer
          const dataState = useQuery(
              {} as Record<number, DataType>,
              actions,
              async (action, prev) => {
                  switch (action?.type) {
                      case 'update':
                      case 'query': {
                          const { id } = action;
                          // 已經(jīng)存在子列表的情況下,不對(duì)數(shù)據(jù)作變更,返回一個(gè) identity 函數(shù)
                          if (action.type === 'query' && prev[id]) return prevState => prevState;
                          // 拉取指定 id 下的列表數(shù)據(jù)
                          const { data } = await httpService.fetchListData({ id });
                          // 返回一個(gè)插入數(shù)據(jù)的狀態(tài)映射函數(shù)
                          return prev => ({
                              ...prev,
                              [id]: data,
                          });
                      }
                      case 'clear': {
                          // 返回一個(gè)保留特定 id 數(shù)據(jù)的狀態(tài)映射函數(shù)
                          return prev =>
                              pick( // pick 是一個(gè)從對(duì)象里獲取一部分 key value 對(duì)組成新對(duì)象的方法
                                  prev,
                                  action.ids,
                              );
                      }
                      default:
                          return prev;
                  }
              },
              { mode'multi'immediatefalse }
          );

          單例的 Hooks——全局狀態(tài)管理

          通過 Hooks 管理全局狀態(tài)可以與傳統(tǒng)方式一樣,例如借助 context 配合 redux 通過 Provider 來下發(fā)全局狀態(tài)。這里推薦更 Hooks 更方便的一種方式——單例 Hooks:Hox[3]

          通過第三方庫(kù) Hox 提供的 createModel 方法可以產(chǎn)生一個(gè)掛載在虛擬組件中的全局單例的 Hooks。這個(gè)虛擬組件的實(shí)例一經(jīng)創(chuàng)建將在 app 的整個(gè)生命周期中存活,等于是產(chǎn)生了一個(gè)全局的「marble 源」,從而任何的組件都可以使用這個(gè) Hooks 來獲取這個(gè)源來處理自己的邏輯。

          hox 的具體實(shí)現(xiàn)涉及自定義 React Reconciler,感興趣的同學(xué)可以去看一下它源碼的實(shí)現(xiàn)。

          流式 Hooks 局限性

          「基于變更」的 Hooks 組件書寫由于與流式編程非常相似,我也把他稱作「流式 Hooks」。

          上面介紹了很多流式 Hooks 的好處。通過合適的邏輯拆分和復(fù)用,流式 Hooks 可以實(shí)現(xiàn)非常細(xì)粒度且高內(nèi)聚的代碼邏輯。在長(zhǎng)期實(shí)踐中也證明了它是比較易于維護(hù)的。那么這種風(fēng)格 Hooks 存在什么局限性呢?

          「過頻繁」的變更

          在 React 中,存在三種不同「幀率」或「頻繁度」的東西:

          • 調(diào)和 reconcile:把 virtualDOM 的變更同步到真實(shí)的 DOM 上去
          • 執(zhí)行幀 rendering:即 React 組件的執(zhí)行頻率
          • 事件 event:即事件 dispatch 的頻率

          這三者的觸發(fā)頻率是從上至下越來越高的

          由于 React Hooks 的變更傳播的最小粒度是「執(zhí)行幀」粒度,故一旦事件的發(fā)生頻率高過它(一般來說只會(huì)是同步的多次事件的觸發(fā)),這種風(fēng)格的 Hooks 就需要一些較為 Hack 的邏輯來兜底處理。

          避免「為了流而流」

          流式編程如 RxJS 大量被用于消息通訊(如在 Angular 中),被用于處理復(fù)雜的事件流程。但其本身一直沒有成為主流的應(yīng)用架構(gòu)。導(dǎo)致這個(gè)狀況的一個(gè)瓶頸就在于它幾乎沒有辦法寫一星半點(diǎn)命令式的代碼,從而會(huì)出現(xiàn)把一些通過命令式/回調(diào)式很好實(shí)現(xiàn)的代碼寫得非常冗長(zhǎng)難懂的情況。

          React Hooks 雖然可以與 RxJS 的語法產(chǎn)生很大成都的同構(gòu),但其本質(zhì)仍然是命令式為底層的編程,故它可以是多范式的。在編碼中,我們?cè)诮^大部分場(chǎng)景下可以通過流式的風(fēng)格實(shí)現(xiàn),但也應(yīng)當(dāng)避免為了流而流。如 Redux 下的一個(gè)關(guān)于哪些狀態(tài)應(yīng)該放到全局哪些應(yīng)該放到組件內(nèi)的 Issue 下評(píng)論的:選擇看起來更不奇怪(less weird)的那個(gè)

          愿景

          目前我正在規(guī)劃和產(chǎn)出一套基礎(chǔ)的流式 Hooks,便于業(yè)務(wù)邏輯引用來書寫具有流式風(fēng)格的 Hooks 代碼Marble Hooks[4]

          參考資料

          [1]

          RxMarble圖例: https://rxmarbles.com/

          [2]

          ImmutableJS: https://immutable-js.github.io/immutable-js/

          [3]

          Hox: https://github.com/umijs/hox

          [4]

          Marble Hooks: https://github.com/pierrejacques/marble-hooks


          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  婷婷深爱激情网 | 亚洲成年影视网 | 淫妻激情在线观看 | 在线看v片 | 万影网五月天成人网 |