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

          MobX流程分析與最佳實踐

          共 18441字,需瀏覽 37分鐘

           ·

          2021-05-27 13:57

          大力輔導(dǎo)項目在首頁多個 tab,課程詳情、社區(qū)、語文字詞和個人信息頁等多個業(yè)務(wù)場景深度使用 Flutter 進行開發(fā),而在 Flutter 開發(fā)過程中狀態(tài)管理是繞不開的話題。

          在進行 Native 開發(fā)時,我們命令式地來表述 UI 構(gòu)建和更新邏輯,通過類似 setText、setImageUrl 的代碼對界面 UI 進行構(gòu)建與更新。和 Native 開發(fā)不同,在進行 Flutter 開發(fā)時,UI 的構(gòu)建是聲明式的,這種框架結(jié)構(gòu)直接影響了我們對更新邏輯的表達形式。

          Flutter 中觸發(fā)狀態(tài)更新的 API 即我們最熟悉的 setState 方法,但是項目中往往會碰到狀態(tài)需要跨層級或者在兄弟組件之間共享,僅僅使用 setState 一般不足以覆蓋復(fù)雜狀態(tài)管理的場景。因此我們需要狀態(tài)管理框架來幫助我們規(guī)范更新邏輯,同時也能更好地貼合 Flutter framework 的工作機制。

          框架選型

          我們在初期調(diào)研了多個開源的狀態(tài)管理框架包括 BLoC、Redux 以及 MobX 等。

          BLoC 使用流共享數(shù)據(jù),并且 Dart 語言本身對流的親和度很高,參考其它平臺的 ReactiveX 的解決方案,開發(fā)者可以快速地使用這種模式進行開發(fā)。BLoC 的最大問題是,其它的 ReactiveX 每個數(shù)據(jù)源都是獨立的 Stream,但是 BLoC 則是統(tǒng)一的單 Stream。單 Stream 表達整個頁面的所有業(yè)務(wù)邏輯不具有普適性,其抽象層級過高,部分場景需要配合其他的方案。

          就 Js 領(lǐng)域最流行的 Redux 框架而言,由于 Redux 本身的一些特點,Redux 的主打功能是應(yīng)用狀態(tài)可預(yù)知、可回溯,同時它也有使用上的成本,比如要求 reducer 是純函數(shù),store 之間的交流需要最佳實踐指導(dǎo),樣板代碼較多,可能需要開發(fā)同學(xué)有一定的 FP 開發(fā)背景。Redux 是諸多框架中編碼最為繁瑣,樣板代碼較多的一個。不過大量的模板代碼也規(guī)范了代碼風(fēng)格,大型項目中,Redux 更規(guī)范易操作擴展和維護。

          MobX 的數(shù)據(jù)響應(yīng)對開發(fā)者幾乎完全透明,開發(fā)者可以更加自由地組織自己的應(yīng)用程序的狀態(tài),擁有比較高的易用性和擴展性,也易于學(xué)習(xí),更加符合 OOP 的思想,也可以更快地支持業(yè)務(wù)迭代。使用 MobX,使得我們更加可以關(guān)注狀態(tài)本身和狀態(tài)引起的變化,不需要關(guān)心那么多復(fù)雜組件是如何組合連接起來的,所有的事情都被簡單優(yōu)雅的 API 抽象掉了。不過,MobX 自身過于自由的特性也帶來了一些麻煩,由于編碼沒有模板約束,過于自由,容易導(dǎo)致團隊代碼風(fēng)格不統(tǒng)一,不同的頁面不同的風(fēng)格,代碼難以管理維護,對此往往需要制定統(tǒng)一的團隊編碼規(guī)范。

          基于我們項目目前的規(guī)模,以及迭代速度,同一個頁面相鄰版本的兩次迭代很有可能發(fā)生了很大的變化,MobX 的簡單易用性最有利于我們的項目進行高強度快速迭代。最終我們在諸多框架中,選擇了使用 MobX 作為我們項目的狀態(tài)管理框架,本文著重分析 MobX 數(shù)據(jù)綁定和更新的主流程,以及最佳實踐。

          使用方法不再詳述,參見 MobX.dart 官網(wǎng),我們著重分析一下 MobX 驅(qū)動頁面更新的主流程,包含兩部分:數(shù)據(jù)綁定與數(shù)據(jù)更新。分析的代碼基于 MobX 的 1.1.0 版本。

          為了更直觀的分析,我們直接使用官網(wǎng)經(jīng)典的 MobX Counter 這個 demo 進行示例,通過 debug 的堆棧幫助我們?nèi)ヌ骄?MobX 中數(shù)據(jù)的綁定和更新的主流程。

          數(shù)據(jù)綁定流程

          Observer 和 @observable 對象一定通過某種方式建立了綁定關(guān)系,我們先來研究一下數(shù)據(jù)的綁定流程。

          從 reportRead() 入手

          Atom.reportRead()

          在代碼中獲取顯示的數(shù)字 counter.value 處打一個斷點,從 demo app 打開開始,第一次頁面 build 時,代碼會執(zhí)行到生成的. g.dart 中去。我們來看 value 相關(guān)的 get 方法和 Atom 對象:

          final _$valueAtom = Atom(name: '_Counter.value');
          @override
          int get value {
            _$valueAtom.reportRead();
            return super.value;
          }
          復(fù)制代碼

          生成的. g.dart 文件中有一個 Atom 對象,其中覆寫了 counter.value 的 get 方法,我們每次使用 @observable 標(biāo)記一個字段,在 .g.dart 中就會生成該字段的 getter 跟 setter 及對應(yīng)的 Atom 對象。Atom 對象是對原字段的一個封裝,當(dāng)我們讀取 couter.value 字段時,會在該 Atom 上調(diào)用 reportRead():

          extension AtomSpyReporter on Atom {
            void reportRead() {
              ...
              reportObserved();
            }
              ...
          }

          void reportObserved() {
            _context._reportObserved(this);
          }
          復(fù)制代碼

          ReactiveContext._reportObserved()

          這個_context,追溯一下可以看到是一個全局的 ReactiveContext 單例,注釋寫的比較明白了:

          它負責(zé)處理 Atom 跟 Reaction(下文會講到) 的依賴關(guān)系, 及進行數(shù)據(jù)方法綁定、分發(fā)、解綁等邏輯。

          最終走到了 context 中的_reportObserved 方法,這個 Atom 對象被添加到了一個 derivation 的_newObservables 字段中,該_newObservables 類型為 Set:

          void _reportObserved(Atom atom) {
            final derivation = _state.trackingDerivation;

            if (derivation != null) {
              derivation._newObservables.add(atom);
              if (!atom._isBeingObserved) {
                atom
                  .._isBeingObserved = true
                  .._notifyOnBecomeObserved();
              }
            }
          }
          復(fù)制代碼

          Atom 對象被類型為 Derivation 的變量 derivation 持有在一個_newObservables 的 Set 里面,我們回到之前打斷點的堆棧,來看一下這里的 derivation 到底是什么。

          回到起點

          堆棧信息

          下圖為整個頁面從 main.dart 的 runApp 開始到 MyHomePage 這個 Widget 的 build 的過程,從 debug 的堆棧信息入手:

          Observer 相關(guān)的 Widget 和 Element

          首先我們簡單看一下 Observer 以及相關(guān)的 Widget 和 Element 的概念。我們通常使用的 Observer 這個 Widget,它實際上是一個 StatelessObserverWidget(繼承自 StatelessWidget),其 build 方法中的 Widget 就是 builder 中返回的 widget,該 StatelessObserverWidget 還 mixin 了 ObserverWidgetMixin,StatelessObserverWidget 的 Element 為 StatelessObserverElement,該 Element 也 mixin 了 ObserverElementMixin,他們之間的關(guān)系如圖所示:

          ObserverElementMixin.mount()

          從該部分開始看起:

          @override
          void mount(Element parent, dynamic newSlot) {
            _reaction = _widget.createReaction(invalidate, onError: (e, _) {
                 ... ));
            }) as ReactionImpl;
            ...
          }
          復(fù)制代碼

          ObserverElementMixin 的 mount 方法給_reaction 賦了值,再追溯一下,ObserverWidgetMixin 的 createReaction 方法傳入了上文提到的核心的 ReactiveContext 單例,創(chuàng)建了 Reaction,而 Reaction 實現(xiàn)了 ReactionImpl 類,ReactionImpl 又實現(xiàn)自 Derivation,在 Derivation 類中我們看到了上一部分提到的_newobservables 這個 Set:

          此時可以有一個初步的猜想:由 Observer 這個 Widget 持有了 ReactionImpl,ReactionImpl 中持有了_newobservables 這個 Set,在 @observable 變量被讀取的時候通過對應(yīng) Atom 對象的 reportRead 方法將該 Atom 對象添加入了這個 Set,這樣就 Observer 這個 Widget 通過其中的 ReactionImpl 間接的持有了 @observable 對象。

          繼續(xù)往下看我們來驗證一下。

          ObserverElementMixin.build()

          按著堆棧信息走下去。來到 ObserverElementMixin 的 build() 方法,調(diào)用了 mount 中創(chuàng)建的 ReactionImpl 的 track() 方法:

          Widget build() {
          ...
            reaction.track(() {
              built = super.build();
            });
          ...
          }
          復(fù)制代碼

          ReactionImpl.track() -> ReactiveContext.trackDerivation()

          此處剔除掉了大部分和主流程無關(guān)的代碼,如下:

          void track(void Function() fn) {
          ...
            _context.trackDerivation(this, fn);
          ...
          }

          //主流程關(guān)注這兩句
          T trackDerivation<T>(Derivation d, T Function() fn) {
            final prevDerivation = _startTracking(d);
              ...
            result = fn();
              ...
            _endTracking(d, prevDerivation);
              ...
          }
          復(fù)制代碼

          ReactiveContext 的 trackDerivation() 方法接收 Derivation 參數(shù),這里傳入自身,來到下面,在_startTracking 和_endTracking 之間調(diào)了 fn,這里的 fn 就是 ObserverElementMixin 的 build 方法中傳入的 super.build():

          ReactiveContext._startTracking()

          _startTracking() 中做的是對狀態(tài)的更新。其中的_state 是個_ReactiveState,就是一個對 ReactiveContext 單例當(dāng)前狀態(tài)的封裝的類,這里我們關(guān)注 trackingDerivation,是當(dāng)前正在被記錄的一個 Derivation。

          _startTracking() 中最重要的一步是把_state 中記錄的 trackingDerivation 賦值為當(dāng)前的 Derivation(即上方傳入的 ReactionImpl),這一步很關(guān)鍵,直到_endTracking 執(zhí)行之前,這個 state.trackingDerivation 都是當(dāng)前設(shè)置的值,并返回一個 prevDerivation(上一個記錄的 trackingDerivation):

          Derivation _startTracking(Derivation derivation) {
            final prevDerivation = _state.trackingDerivation;
            _state.trackingDerivation = derivation;

            _resetDerivationState(derivation);
            derivation._newObservables = {};

            return prevDerivation;
          }
          復(fù)制代碼

          Observer.builder 調(diào)用的位置

          在_startTracking 和_endTracking 之間調(diào)用了 fn,即 ObserverElementMixin 中傳入的 super.build(),熟悉 dart mixin 語法規(guī)則的,也很快清楚這里調(diào)用鏈最終會走到 Observer 這個 Widget 的 build 方法,也即我們使用時傳入的 builder 方法里面去:

          class Observer extends StatelessObserverWidget implements Builder {
              ...
              @override
              Widget build(BuildContext context) => builder(context);
              ...
          }
          復(fù)制代碼

          這時候就回到了一開頭的部分,builder 中讀取了 counter.value,也即調(diào)用它的 get 方法,通過 reportRead,最終再通過 state.trackingDerivation 得到當(dāng)前正在記錄的 derivation 對象,并給他的_newObservables 的這個 Set 里面添加了 counter.value 對應(yīng)的封裝的 Atom 對象。

          解決一開始我們提出的問題——持有_newObservables 的 derivation 是什么?

          derivation 就是_startTracking() 方法中賦值給_state.trackingDerivation 的當(dāng)前 ObserverElementMixin 中持有的 ReactionImpl 對象。Observer 通過該對象間接持有了我們的 @observable 對象,也驗證了我們上文的猜想。

          回顧下,經(jīng)過上面_startTracking 中將當(dāng)前的 derivation 賦值給 context.state.trackingDerivation ,以及 Observer 的 builder 方法(fn)的調(diào)用,builder 方法中任何對 @observable 對象的 get 方法,都將經(jīng)過 reportRead,也就是 reportObserved,所以該 @observable 對象就會被添加到當(dāng)前的 derivation 的 _newObservables 集合上,表示該 derivation 和 @observable 對象的依賴關(guān)系,注意此時這樣的綁定關(guān)系是單向的,目的是為了收集依賴。真正的數(shù)據(jù)綁定過程在_endTracking() 中。

          ReactiveContext._endTracking()

          最后再看_endTracking,核心的建立綁定關(guān)系的方法是_bindDependencies:

          void _endTracking(Derivation currentDerivation, Derivation prevDerivation) {
            _state.trackingDerivation = prevDerivation;//這里又會把trackingDerivation恢復(fù)回去
            _bindDependencies(currentDerivation);
          }

          void _bindDependencies(Derivation derivation) {
              //derivation里面實際上有兩個set _observables和_newObservables,分別裝的是之前舊的atom和reportRead里面新加的atom
              //搞了兩次difference, 把新的和舊的@observable變量分開。舊的清空數(shù)據(jù),新的綁定觀察者
            final staleObservables =
                derivation._observables.difference(derivation._newObservables);
            final newObservables =
                derivation._newObservables.difference(derivation._observables);
            var lowestNewDerivationState = DerivationState.upToDate;

            // Add newly found observables
            for (final observable in newObservables) {
              observable._addObserver(derivation);//綁定觀察者
              // Computed = Observable + Derivation
              if (observable is Computed) {
                if (observable._dependenciesState.index >
                    lowestNewDerivationState.index) {
                  lowestNewDerivationState = observable._dependenciesState;
                }
              }
            }

            // Remove previous observables
            for (final ob in staleObservables) {
              ob._removeObserver(derivation);//解除綁定
            }

            if (lowestNewDerivationState != DerivationState.upToDate) {
              derivation
                .._dependenciesState = lowestNewDerivationState
                .._onBecomeStale();
            }

            derivation
              .._observables = derivation._newObservables
              .._newObservables = {}; // No need for newObservables beyond this point
          }

          //下面是atom的_addObserver和_removeObserver方法 
          //atom中有個observers變量 Set<Derivation>對象,記錄了觀察自己的Derivation。
          void _addObserver(Derivation d) {
            _observers.add(d);
            if (_lowestObserverState.index > d._dependenciesState.index) {
              _lowestObserverState = d._dependenciesState;
            }
          }
          void _removeObserver(Derivation d) {
            _observers.remove(d);
            if (_observers.isEmpty) {
              _context._enqueueForUnobservation(this);
            }
          }
          復(fù)制代碼

          這個方法的邏輯,根據(jù)前后兩次 build 時 Set 中收集 Atom 對象的依賴,分別執(zhí)行 _addObserver 和 _removeObserver,這樣,每個 @observable 對象上的 observers 集合都會是最新的了。

          結(jié)論

          Observer 對應(yīng)的 Element——StatelessObserverElement,持有一個 Derivation——即 ReactionImpl 對象 reacton**,** 而該對象持有一個 Set 類型的_observables,@observable 對象在被讀取調(diào)用 get 方法的時候,對應(yīng)的 Atom 被添加到了這個 Set 中去,該 Set 中的 @observable 對象對應(yīng)的 Atom 在 endTracking 中調(diào)用了_addObserver 方法,把觀察自己的 ReactionImpl 添加進 observers 這個 Set 中去。從而 @obsevable 對象對應(yīng)的 Atom 持有了 Observer 這個 Widget 中的 ReactionImpl,Observer 就這樣和 @observable 對象建立了綁定關(guān)系。

          數(shù)據(jù)更新流程

          知道了 Observer 和 @observable 對象是怎樣建立聯(lián)系之后,再來看一下當(dāng)我們修改 @observable 對象時候,更新界面邏輯是怎么觸發(fā)的。

          reportWrite()

          在. g.dart 文件中,覆寫了 @observable 變量的 get 方法,會在 get 時候調(diào)用對應(yīng) Atom 對象的 reportRead(),并且這里還覆寫了 @observable 變量的 set 方法,都會調(diào)用 Atom 對象的 reportWrite() 方法,這個方法做了兩件事情

          1. 更新數(shù)據(jù)

          2. 把與之綁定 的 derivation (即 reaction) 加到更新隊列。

            @override set value(int value) { _$valueAtom.reportWrite(value, super.value, () { super.value = value; }); }

          最終可以追溯到這里:

          void propagateChanged(Atom atom) {
            if (atom._lowestObserverState == DerivationState.stale) {
              return;
            }

            atom._lowestObserverState = DerivationState.stale;

           _observers就是上面數(shù)據(jù)綁定過程中涉及到的atom對象記錄觀察者的Set<Derivation>
            for (final observer in atom._observers) {
              if (observer._dependenciesState == DerivationState.upToDate) {
                observer._onBecomeStale();
              }
              observer._dependenciesState = DerivationState.stale;
            }
          }
          復(fù)制代碼

          ReactionImpl._onBecomStale()

          @override
          void _onBecomeStale() {
            schedule();
          }

          void schedule() {
             ...
            _context
              ..addPendingReaction(this)
              ..runReactions();
          }
          復(fù)制代碼

          ReactiveContext.addPendingReaction()

          reaction 添加到隊列,reaction也就是上面?zhèn)魅氲腞eactionImpl
          void addPendingReaction(Reaction reaction) {
            _state.pendingReactions.add(reaction);
          }
          復(fù)制代碼

          ReactiveContext.runReactions

          void runReactions() {
             ...
              for (final reaction in remainingReactions) {
                reaction._run();
              }
            
            _state
              ..pendingReactions = []
              ..isRunningReactions = false;
          }
          復(fù)制代碼

          ReactionImpl.run()

          @override
          void _run() {
              ...
              _onInvalidate();//這里實際上就是觸發(fā)更新的地方
              ...
          }
          復(fù)制代碼

          這邊的_onInvalidate() 就是在 ObserverElementMixin.mount() 里面 createReaction 時候傳進去的

          @override
          void mount(Element parent, dynamic newSlot) {
            _reaction = _widget.createReaction(invalidate, onError: (e, _) {
                 ... ));
            }) as ReactionImpl;
            ...
          }
          復(fù)制代碼

          看看 invalidate 是什么:

          void invalidate() => markNeedsBuild();
          復(fù)制代碼

          也就是 markNeedsBuild 標(biāo)臟操作,這樣 Flutter Framework 的 buildOwner 會在下一幀重新調(diào)用 build 方法,就完成了數(shù)據(jù)更新操作。

          結(jié)論

          至此數(shù)據(jù)更新的流程也搞明白了,在更改 @observable 變量的時候,調(diào)用到 Atom 對象的 reportWrite 方法,首先更新了數(shù)據(jù),然后把與之綁定的 ReactionImpl 對象 derivation 加到隊列 pendingReactions,最終隊列里面的 ReactionImpl 調(diào)用 run 方法,觸發(fā) markNeedsBuild,完成了界面更新。

          在使用 MobX 進行狀態(tài)管理的過程中,我們也踩了一些坑,總結(jié)了最佳實踐,對開發(fā)過程中時常遇到的更改數(shù)據(jù)頁面未被更新的情況做了總結(jié)。

          因為 MobX 的數(shù)據(jù)綁定是運行時的,所以需要注意綁定不要寫在控制流語句中,同時也要注意綁定的層級。在此看三個 bad case,同時引出最佳實踐。相信在了解了框架數(shù)據(jù)綁定和更新的原理之后,也很容易理解這些 bad case 出現(xiàn)的原因。

          Bad Case 1:

          Widget build(BuildContext context) {
            return Observer(builder:(context){
                Widget child;
              if (store.showImage) {
                  child = Image.network(
                    store.imageURL
                  );
                } else {
              // ...
            });

          }
          復(fù)制代碼

          這個例子里面 store.imageURL 是一個被 @observable 標(biāo)注的字段。如果在第一次 build 的過程中,即數(shù)據(jù)綁定的過程中,store.showImage 為 false,代碼走 else 分支,這樣 store.imageURL 就沒能和 Observer 建立綁定關(guān)系,后續(xù) store.imageURL 發(fā)生改變,就無法驅(qū)動界面更新。

          Bad Case 2:

          Widget build(BuildContext context) {
            return Observer(builder:(context){
                Widget child;
              if (store.a && store.b && store.c) {
                  child = Image.network(
                    store.imageURL
                  );
                } else {
              // ...
            });

          }
          復(fù)制代碼

          這個例子里面 store.a、store.b 還有 store.c 都是 @observable 標(biāo)注的 bool 變量,遵循大部分語言的邏輯表達式判斷規(guī)則,if 語句中多個并列的與的條件,如果排列靠前的條件為 false,那么后續(xù)的條件不會再被判斷,直接走入 else 分支。

          那么問題也顯而易見了,如果本意是希望 store.a、store.b 還有 store.c 都和 Observer 綁定關(guān)系,如果在第一次 build 時,store.a 為 false,那么 b 和 c 均沒有和 Observer 建立聯(lián)系,這樣 b 和 c 的變化就無法驅(qū)動該 Widget 更新。

          Bad Case 3:

          針對我們開發(fā)過程中一個常見的錯誤舉出這個 Case:

          class WidgetA extends StatelessWidget{
              Widget build(BuildContext context) {
                 ...
                Observer(builder:(context){
                     return TestWidget();
                });
                ...
              }
          }

          class WidgetB extends StatelessWidget {
            @override
            Widget build(BuildContext context) {
              return  GestureDetector(
                child: Text(
                  '${counter.value}',
                  style: Theme.of(context).textTheme.headline4,
                ),
                onTap: () {
                  counter.increment();
                },
              );
            }
          }
          復(fù)制代碼

          這個例子改編自 MobX 官網(wǎng)經(jīng)典的 counter Demo,counter.value 是 @observable 標(biāo)注的字段。編寫者的本意是用 Observer 包裹了 WidgetB,希望 GestureDetector 的點擊事件使得 counter.value 自增,驅(qū)動 Observer 的 Widget 的更新,不過我們點擊按鈕發(fā)現(xiàn)頁面并沒有更新。

          根據(jù)上述原理分析,數(shù)據(jù)綁定的過程是在_startTracking 和_endTracking 之間的 Observer.build 方法的調(diào)用過程中完成的。而這里 Observer.builder 中只是 return 了 TestWidget,也即調(diào)用了 WidgetB 的構(gòu)造方法,WidgetB 的 build 方法,也即讀取 counter.value 的方法是在下一層 widget 構(gòu)建的過程中,才會被調(diào)用,因此 counter.value 未能和它上一層的 Observer 建立綁定關(guān)系,自然也不能夠驅(qū)動頁面更新了。

          Good

          我們針對 Bad Case 2 提出最佳實踐:

          Widget build(BuildContext context) {
            return Observer(builder:(context){
                Widget child;
                bool a = store.a;
                bool b= store.b;
                bool c = store.c;
              if (a && b && c) {
                  child = Image.network(
                    store.imageURL
                  );
                } else {
              // ...
            });
          }
          復(fù)制代碼

          對于 @observable 對象的依賴依次羅列在最開始,而不是寫在 if 判斷括號中,就可以保證所有變量均和 Observer 建立了綁定關(guān)系。

          MobX 還有許多其他的細節(jié),比如,context 上的 startBatch 相關(guān),這是因為 action 中可以調(diào)用其他的 action,為了減少不必要的更新通知調(diào)用,通過 batch 機制合并 pendingReaction 的調(diào)用。同理,在 reaction 內(nèi)部也可以對 @observable 對象進行更新,因此也需要 batch 機制合并更改。

          MobX 也有一些優(yōu)化點,比如,上述數(shù)據(jù)更新的 reportWrite 方法,我們可以 diff 一下 oldValue 和 value,看二者是否相等,不相等的時候再進行后續(xù)流程。

          有興趣的讀者可以自行閱讀源碼探索更多的內(nèi)容,在此不作詳細分析了。


          1. 關(guān)注公眾號「前端Sharing」,持續(xù)為你推送精選好文。


          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  日韩黄色操少妇视频 | 亚洲伊人在线 | 久久大香蕉精品在线 | 色骚综合 | 男女撸视频 |