老樹發(fā)新芽—使用 mobx 加速你的 AngularJS 應(yīng)用,mobxangularjs

    1月底的時候,Angular 官方博客發(fā)布了一則消息:

    AngularJS is planning one more significant release, version 1.7, and on July 1, 2024 it will enter a 3 year Long Term Support period.

    即在 7月1日 AngularJS 發(fā)布 1.7.0 版本之后,AngularJS 將進入一個為期 3 年的 LTS 時期。愛掏網(wǎng) - it200.com也就是說 2024年7月1日 起至 2024年6月30日,AngularJS 不再合并任何會導致 breaking changes 的 features 或 bugfix,只做必要的問題修復。愛掏網(wǎng) - it200.com詳細信息見這里:Stable AngularJS and Long Term Support

    看到這則消息時我還是感觸頗多的,作為我的前端啟蒙框架,我從 AngularJS 上汲取到了非常多的養(yǎng)分。愛掏網(wǎng) - it200.com雖然 AngularJS 作為一款優(yōu)秀的前端 MVW 框架已經(jīng)出色的完成了自己的歷史使命,但考慮到即便到了 2024 年,許多公司基于 AngularJS 的項目依然處于服役階段,結(jié)合我過去一年多在 mobx 上的探索和實踐,我決定給 AngularJS 強行再續(xù)一波命。愛掏網(wǎng) - it200.com(搭車求治拖延癥良方,二月初起草的文章五月份才寫完,新聞都要過期了)

    準備工作

    在開始之前,我們需要給 AngularJS 搭配上一些現(xiàn)代化 webapp 開發(fā)套件,以便后面能更方便地裝載上 mobx 引擎。愛掏網(wǎng) - it200.com

    AngularJS 配合 ES6/next

    現(xiàn)在是2024年,使用 ES6 開發(fā)應(yīng)用已經(jīng)成為事實標準(有可能的推薦直接上 TS )。愛掏網(wǎng) - it200.com如何將 AngularJS 搭載上 ES6 這里不再贅述,可以看我之前的這篇文章:Angular1.x + ES6 開發(fā)風格指南

    基于組件的應(yīng)用架構(gòu)

    AngularJS 在 1.5.0 版本后新增了一系列激動人心的特性,如 onw-way bindings、component lifecycle hooks、component definition 等,基于這些特性,我們可以方便的將 AngularJS 系統(tǒng)打造成一個純組件化的應(yīng)用(如果你對這些特性很熟悉可直接跳過至 [AngularJS 搭配 mobx](#AngularJS 搭配 mobx))。愛掏網(wǎng) - it200.com我們一個個來看:

    • onw-way bindings 單向綁定 AngularJS 中使用 來定義組件的單向數(shù)據(jù)綁定,例如我們這樣定義一個組件:

      angular
       .module('app.components', [])
       .directive('component', () => ({
       restrict: 'E',
       template: '

      count: {{$ctrl.count}}

      '
      scope: { count: ' }, bindToController: true, controllerAs: '$ctrl', })

      使用時:

      {{app.count}}
      component count="app.count">component> 

      當我們點擊組件的 increase 按鈕時,可以看到組件內(nèi)的 count 加 1 了,但是 app.count并不受影響。愛掏網(wǎng) - it200.com

      區(qū)別于 AngularJS 賴以成名的雙向綁定特性 scope: { count: '='},單向數(shù)據(jù)綁定能更有效的隔離操作影響域,從而更方便的對數(shù)據(jù)變化溯源,降低 debug 難度。愛掏網(wǎng) - it200.com 雙向綁定與單向綁定有各自的優(yōu)勢與劣勢,這里不再討論,有興趣的可以看我這篇回答:單向數(shù)據(jù)綁定和雙向數(shù)據(jù)綁定的優(yōu)缺點,適合什么場景?

    • component lifecycle hooks 組件生命周期鉤子

      1.5.3 開始新增了幾個組件的生命周期鉤子(目的是為更方便的向 Angular2+ 遷移),分別是 $onInit$onChanges $onDestroy $postLink $doCheck(1.5.8增加),寫起來大概長這樣:

      class Controller {
      
       $onInit() {
       // initialization
       }
      
       $onChanges(changesObj) {
       const { user } = changesObj;
       if(user && !user.isFirstChange()) {
       // changing
       }
       }
      
       $onDestroy() {}
      
       $postLink() {}
      
       $doCheck() {} 
      }
      
      angular
       .module('app.components', [])
       .directive('component', () => ({
       	controller: Controller,
       	...
      	}))
      

      事實上在 1.5.3 之前,我們也能借助一些機制來模擬組件的生命周期(如 $scope.$watch$scope.$on('$destroy')等),但基本上都需要借助$scope這座‘‘橋梁’’。愛掏網(wǎng) - it200.com但現(xiàn)在我們有了框架原生 lifecycle 的加持,這對于我們構(gòu)建更純粹的、框架無關(guān)的 ViewModel 來講有很大幫助。愛掏網(wǎng) - it200.com更多關(guān)于 lifecycle 的信息可以看官方文檔:AngularJS lifecycle hooks

    • component definition

      AngularJS 1.5.0 后增加了 component 語法用于更方便清晰的定義一個組件,如上述例子中的組件我們可以用component語法改寫成:

      angular
       .module('app.components', [])
       .component('component', {
       template: '

      count: {{$ctrl.count}}

      '
      bindings: { count: ', onUpdate: '&' }, })

      本質(zhì)上component就是directive的語法糖,bindings 是 bindToController + controllerAs + scope 的語法糖,只不過component語法更簡單語義更明了,定義組件變得更方便,與社區(qū)流行的風格也更一致(熟悉 vue 的同學應(yīng)該已經(jīng)發(fā)現(xiàn)了)。愛掏網(wǎng) - it200.com更多關(guān)于 AngularJS 組件化開發(fā)的 best practice,可以看官方的開發(fā)者文檔:Understanding Components

    • AngularJS 搭配 mobx

    準備工作做了一堆,我們也該開始進入本文的正題,即如何給 AngularJS 搭載上 mobx 引擎(本文假設(shè)你對 mobx 中的基礎(chǔ)概念已經(jīng)有一定程度的了解,如果不了解可以先移步 mobx repo mobx official doc):

    1. mobx-angularjs

    引入 mobx-angularjs 庫連接 mobx 和 angularjs 。愛掏網(wǎng) - it200.com

    npm i mobx-angularjs -S
    
    

    2. 定義 ViewModel

    在標準的 MVVM 架構(gòu)里,ViewModel/Controller 除了構(gòu)建視圖本身的狀態(tài)數(shù)據(jù)(即局部狀態(tài))外,作為視圖跟業(yè)務(wù)模型之間溝通的橋梁,其主要職責是將業(yè)務(wù)模型適配(轉(zhuǎn)換/組裝)成對視圖更友好的數(shù)據(jù)模型。愛掏網(wǎng) - it200.com因此,在 mobx 視角下,ViewModel 主要由以下幾部分組成:

    • 視圖(局部)狀態(tài)對應(yīng)的 observable data

      class ViewModel {
       @observable
       isLoading = true;
      
      	@observable
      	isModelOpened = false;
      }
      

      可觀察數(shù)據(jù)(對應(yīng)的 observer 為 view),即視圖需要對其變化自動做出響應(yīng)的數(shù)據(jù)。愛掏網(wǎng) - it200.com在 mobx-angularjs 庫的協(xié)助下,通常 observable data 的變化會使關(guān)聯(lián)的視圖自動觸發(fā) rerender(或觸發(fā)網(wǎng)絡(luò)請求之類的副作用)。愛掏網(wǎng) - it200.comViewModel 中的 observable data 通常是視圖狀態(tài)(UI-State),如 isLoading、isOpened 等。愛掏網(wǎng) - it200.com

    • 由 應(yīng)用/視圖 狀態(tài)衍生的 computed data

      Computed values are values that can be derived from the existing state or other computed values.

      class ViewModel {
       @computed
       get userName() {
       return `${this.user.firstName} ${this.user.lastName}`;
       }
      }
      

      計算數(shù)據(jù)指的是由其他 observable/computed data 轉(zhuǎn)換而來,更方便視圖直接使用的衍生數(shù)據(jù)(derived data)。愛掏網(wǎng) - it200.com 在重業(yè)務(wù)輕交互的 web 類應(yīng)用中(通常是各種企業(yè)服務(wù)軟件), computed data 在 ViewModel 中應(yīng)該占主要部分,且基本是由業(yè)務(wù) store 中的數(shù)據(jù)(即應(yīng)用狀態(tài))轉(zhuǎn)換而來。愛掏網(wǎng) - it200.com computed 這種數(shù)據(jù)推導關(guān)系描述能確保我們的應(yīng)用遵循 single source of truth 原則,不會出現(xiàn)數(shù)據(jù)不一致的情況,這也是 RP 編程中的基本原則之一。愛掏網(wǎng) - it200.com

    • action ViewModel 中的 action 除了一小部分改變視圖狀態(tài)的行為外,大部分應(yīng)該是直接調(diào)用 Model/Store 中的 action 來完成業(yè)務(wù)狀態(tài)的流轉(zhuǎn)。愛掏網(wǎng) - it200.com建議把所有對 observable data 的操作都放到被 aciton 裝飾的方法下進行。愛掏網(wǎng) - it200.com

    mobx 配合下,一個相對完整的 ViewModel 大概長這樣:

    import UserStore from './UserStore';
     
    class ViewModel {
     
     @inject(UserStore)
     store;
    	
     @observable
     isDropdownOpened = false;
    
    	@computed
    	get userName() {
     	return `${this.store.firstName} ${this.store.lastName}`;
    	}
     
    	@action
    	toggel() {
     	this.isDropdownOpened = !isDropdownOpened;
    	}
     
     updateFirstName(firstName) {
     this.store.updateFirstName(firstName);
     }
    }
    

    3. 連接 AngularJS 和 mobx

    section mobx-autorun> counter value="$ctrl.count">counter> button type="button" ng-click="$ctrl.increse()">incresebutton> section> 
    import template from './index.tpl.html';
    class ViewModel {
     @observable count = 0;
    	
    	@action increse() {
     	this.count++;
    	}
    }
    
    export default angular
     .module('app', [])
     .component('container', {
     	template,
     	controller: Controller,
    	})
     .component('counter', {
     	template: '
    {{$ctrl.count}}
    '
    bindings: { value: ' } }) .name;

    可以看到,除了常規(guī)的基于 mobx 的 ViewModel 定義外,我們只需要在模板的根節(jié)點加上 mobx-autorun 指令,我們的 angularjs 組件就能很好的運作的 mobx 的響應(yīng)式引擎下,從而自動的對 observable state 的變化執(zhí)行 rerender。愛掏網(wǎng) - it200.com

    mobx-angularjs 加速應(yīng)用的魔法

    從上文的示例代碼中我們可以看到,將 mobx 跟 angularjs 銜接運轉(zhuǎn)起來的是 mobx-autorun指令,我們翻下 mobx-angularjs 代碼:

    const link: angular.IDirectiveLinkFn = ($scope) => {
    
     const { $$watchers = [] } = $scope as any
     const debouncedDigest = debounce($scope.$digest.bind($scope), 0);
    
     const dispose = reaction(
     () => [...$$watchers].map(watcher => watcher.get($scope)),
     () => !$scope.$root.$$phase && debouncedDigest()
     )
    
     $scope.$on('$destroy', dispose)
    }
    

    可以看到 核心代碼 其實就三行:

    reaction(
     () => [...$$watchers].map(watcher => watcher.get($scope)),
     () => !$scope.$root.$$phase && debouncedDigest()
    

    思路非常簡單,即在指令 link 之后,遍歷一遍當前 scope 上掛載的 watchers 并取值,由于這個動作是在 mobx reaction 執(zhí)行上下文中進行的,因此 watcher 里依賴的所有 observable 都會被收集起來,這樣當下次其中任何一個 observable 發(fā)生變更時,都會觸發(fā) reaction 的副作用對 scope 進行 digest,從而達到自動更新視圖的目的。愛掏網(wǎng) - it200.com

    我們知道,angularjs 的性能被廣為詬病并不是因為 ‘臟檢查’ 本身慢,而是因為 angularjs 在每次異步事件發(fā)生時都是無腦的從根節(jié)點開始向下 digest,從而會導致一些不必要的 loop 造成的。愛掏網(wǎng) - it200.com而當我們在搭載上 mobx 的 push-based 的 change propagation 機制時,只有當被視圖真正使用的數(shù)據(jù)發(fā)生變化時,相關(guān)聯(lián)的視圖才會觸發(fā)局部 digest (可以理解為只有 observable data 存在 subscriber/observer 時,狀態(tài)變化才會觸發(fā)關(guān)聯(lián)依賴的重算,從而避免不必要資源消耗,即所謂的 lazy),區(qū)別于異步事件觸發(fā)即無腦地 $rootScope.$apply, 這種方式顯然更高效。愛掏網(wǎng) - it200.com

    進一步壓榨性能

    我們知道 angularjs 是通過劫持各種異步事件然后從根節(jié)點做 apply 的,這就導致只要我們用到了會被 angularjs 劫持的特性就會觸發(fā) apply,其他的諸如 $http $timeout 都好說,我們有很多替代方案,但是 ng-click 這類事件監(jiān)聽指令我們無法避免,就像上文例子中一樣,假如我們能杜絕潛藏的根節(jié)點 apply,想必應(yīng)用的性能提升能更進一步。愛掏網(wǎng) - it200.com

    思路很簡單,我們只要把 ng-click 之流替換成不觸發(fā) apply 的版本即可。愛掏網(wǎng) - it200.com比如把原來的 ng event 實現(xiàn)這樣改一下:

    forEach(
     'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
     function(eventName) {
     var directiveName = directiveNormalize('native-' + eventName);
     ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
     return {
     restrict: 'A',
     compile: function($element, attr) {
     var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
     return function ngEventHandler(scope, element) {
     element.on(eventName, function(event) {
     fn(scope, {$event:event})
     });
     };
     }
     };
     }];
     }
    );
    

    時間監(jiān)聽的回調(diào)中只是簡單觸發(fā)一下綁定的函數(shù)即可,不再 apply,bingo!

    注意事項/ best practise

    在 mobx 配合 angularjs 開發(fā)過程中,有一些點我們可能會 碰到/需要考慮:

    • 避免 TTL 單向數(shù)據(jù)流優(yōu)點很多,大部分場景下我們會優(yōu)先使用 one-way binding 方式定義組件。愛掏網(wǎng) - it200.com通常你會寫出這樣的代碼:

      class ViewModel {
       @computed
       get unCompeletedTodos() {
       return this.store.todos.filter(todo => !todo.compeleted)
       }
      }
      
      section mobx-autorun> todo-panel todos="$ctrl.unCompeletedTodos">todo-panel> section> 

      todo-panel 組件使用單向數(shù)據(jù)綁定定義:

      angular
       .module('xxx', [])
       .component('todoPanel', {
       	template: '
      • {{todo.content}}
      '
      bindings: { todos: ' } })

      看上去沒有任何問題,但是當你把代碼扔到瀏覽器里時就會收獲一段 angularjs 饋贈的 TTL 錯誤:Error: $rootScope:infdigInfinite $digest Loop愛掏網(wǎng) - it200.com實際上這并不是 mobx-angularjs 惹的禍,而是 angularjs 目前未實現(xiàn) one-way binding 的 deep comparison 導致的,由于每次 get unCompeletedTodos 都會返回一個新的數(shù)組引用,而又是基于引用作對比,從而每次 prev === current 都是 false,最后自然報 TTL 錯誤了(具體可以看這里 One-way bindings + shallow watching )。愛掏網(wǎng) - it200.com

      不過好在 mobx 優(yōu)化手段中恰好有一個方法能間接的解決這個問題。愛掏網(wǎng) - it200.com我們只需要給 computed 加一個表示要做深度值對比的 modifier 即可:

      @computed.struct
      get unCompeletedTodos() {
       return this.store.todos.filter(todo => !todo.compeleted)
      }
      

      本質(zhì)上還是對 unCompeletedTodos 的 memorization,只不過對比基準從默認的值對比(===)變成了結(jié)構(gòu)/深度 對比,因而在第一次 get unCompeletedTodos 之后,只要計算出來的結(jié)果跟前次的結(jié)構(gòu)一致(只有當 computed data 依賴的 observable 發(fā)生變化的時候才會觸發(fā)重算),后續(xù)的 getter 都會直接返回前面緩存的結(jié)果,從而不會觸發(fā)額外的 diff,進而避免了 TTL 錯誤的出現(xiàn)。愛掏網(wǎng) - it200.com

    • $onInit$onChanges 觸發(fā)順序的問題 通常情況下我們希望在 ViewModel 中借助組件的 lifecycle 鉤子做一些事情,比如在 $onInit 中觸發(fā)副作用(網(wǎng)絡(luò)請求,事件綁定等),在 $onChanges 里監(jiān)聽傳入數(shù)據(jù)變化做視圖更新。愛掏網(wǎng) - it200.com

      class ViewModel {
      
       $onInit() {
       	this.store.fetchUsers(this.id); 
       }
      
       $onChanges(changesObj) {
       const { id } = changesObj;
       if(id && !id.isFirstChange()) {
       this.store.fetchUsers(id.currentValue)
       }
       }
      }
      

      可以發(fā)現(xiàn)其實我們在 $onInit$onChanges 中做了重復的事情,而且這種寫法也與我們要做視圖框架無關(guān)的數(shù)據(jù)層的初衷不符,借助 mobx 的 observe 方法,我們可以將上面的代碼改造成這種:

      import { ViewModel, postConstruct } from 'mmlpx';
      @ViewModel
      class ViewModel {
      
       @observable
       id = null;
      
       @postConstruct
       onInit() {
       observe(this, 'id', changedValue => this.store.fetchUsers(changedValue))
       }
      }
      

      熟悉 angularjs 的同學應(yīng)該能發(fā)現(xiàn),事實上 observe 做的事情跟 $scope.$watch 是一樣的,但是為了保證數(shù)據(jù)層的 UI 框架無關(guān)性,我們這里用 mobx 自己的觀察機制來替代了 angularjs 的 watch。愛掏網(wǎng) - it200.com

    • 忘記你是在寫 AngularJS,把它當成一個簡單的動態(tài)模板引擎

      不論是我們嘗試將 AngularJS 應(yīng)用 ES6/TS 化還是引入 mobx 狀態(tài)管理庫,實際上我們的初衷都是將我們的 Model 甚至 ViewModel 層做成視圖框架無關(guān),在借助 mobx 管理數(shù)據(jù)的之間的依賴關(guān)系的同時,通過 connector 將 mobx observable data 與視圖連接起來,從而實現(xiàn)視圖依賴的狀態(tài)發(fā)生變化自動觸發(fā)視圖的更新。愛掏網(wǎng) - it200.com在這個過程中,angularjs 不再扮演一個框架的角色影響整個系統(tǒng)的架構(gòu),而僅僅是作為一個動態(tài)模板引擎提供 render 能力而已,后續(xù)我們完全可以通過配套的 connector,將 mobx 管理的數(shù)據(jù)層連接到不同的 view library 上。愛掏網(wǎng) - it200.com目前 mobx 官方針對 React/Angular/AngularJS 均有相應(yīng)的 connector,社區(qū)也有針對 vue 的解決方案,并不需要我們從零開始。愛掏網(wǎng) - it200.com

      在借助 mobx 構(gòu)建數(shù)據(jù)層之后,我們就能真正做到標準 MVVM 中描述的那樣,在 Model 甚至 VIewModel 不改一行代碼的前提下輕松適配其他視圖。愛掏網(wǎng) - it200.comview library 的語法、機制差異不再成為視圖層 升級/替換 的鴻溝,我們能通過改很少量的代碼來填平它,畢竟只是替換一個動態(tài)模板引擎而已。愛掏網(wǎng) - it200.com

    Why MobX

    React and MobX together are a powerful combination. React renders the application state by providing mechanisms to translate it into a tree of renderable components. MobX provides the mechanism to store and update the application state that React then uses.

    Both React and MobX provide optimal and unique solutions to common problems in application development. React provides mechanisms to optimally render UI by using a virtual DOM that reduces the number of costly DOM mutations. MobX provides mechanisms to optimally synchronize application state with your React components by using a reactive virtual dependency state graph that is only updated when strictly needed and is never stale.

    MobX 官方的介紹,把上面一段介紹中的 React 換成任意其他( Vue/Angular/AngularJS ) 視圖框架/庫(VDOM 部分適當調(diào)整一下) 也都適用。愛掏網(wǎng) - it200.com得益于 MobX 的概念簡單及獨立性,它非常適合作為視圖中立的狀態(tài)管理方案。愛掏網(wǎng) - it200.com簡言之是視圖層只做拿數(shù)據(jù)渲染的工作,狀態(tài)流轉(zhuǎn)由 MobX 幫你管理。愛掏網(wǎng) - it200.com

    Why Not Redux

    Redux 很好,而且社區(qū)也有很多跟除 React 之外的視圖層集成的實踐。愛掏網(wǎng) - it200.com單純的比較 Redux 跟 MobX 大概需要再寫一篇文章來闡述,這里只簡單說幾點與視圖層集成時的差異:

    1. 雖然 Redux 本質(zhì)也是一個觀察者模型,但是在 Redux 的實現(xiàn)下,狀態(tài)的變化并不是通過數(shù)據(jù) diff 得出而是 dispatch(action) 來手動通知的,而真正的 diff 則交給了視圖層,這不僅導致可能的渲染浪費(并不是所有 library 都有 vdom),在處理各種需要在變化時觸發(fā)副作用的場景也會顯得過于繁瑣。愛掏網(wǎng) - it200.com
    2. 由于第一條 Redux 不做數(shù)據(jù) diff,因此我們無法在視圖層接手數(shù)據(jù)前得知哪個局部被更新,進而無法更高效的選擇性更新視圖。愛掏網(wǎng) - it200.com
    3. Redux 在 store 的設(shè)計上是 opinionated 的,它奉行 單一 store 原則。愛掏網(wǎng) - it200.com應(yīng)用可以完全由狀態(tài)數(shù)據(jù)來描述、且狀態(tài)可管理可回溯 這一點上我沒有意見,但并不是只有單一 store這一條出路,多 store 依然能達成這一目標。愛掏網(wǎng) - it200.com顯然 mobx 在這一點上是 unopinionated 且靈活性更強。愛掏網(wǎng) - it200.com
    4. Redux 概念太多而自身做的又太少。愛掏網(wǎng) - it200.com可以對比一下 ngRedux 跟 mobx-angularjs 看看實現(xiàn)復雜度上的差異。愛掏網(wǎng) - it200.com

    最后

    除了給 AngularJS 搭載上更高效、精確的高速引擎之外,我們最主要的目的還是為了將 業(yè)務(wù)模型層甚至 視圖模型層(統(tǒng)稱為應(yīng)用數(shù)據(jù)層) 做成 UI 框架無關(guān),這樣在面對不同的視圖層框架的遷移時,才可能做到游刃有余。愛掏網(wǎng) - it200.com而 mobx 在這個事情上是一個很好的選擇。愛掏網(wǎng) - it200.com

    最后想說的是,如果條件允許的話,還是建議將 angularjs 系統(tǒng)升級成 React/Vue/Angular 之一,畢竟大部分時候基于新的視圖技術(shù)開發(fā)應(yīng)用是能帶來確實的收益的,如 性能提升、開發(fā)效率提升 等。愛掏網(wǎng) - it200.com即便你短期內(nèi)無法替換掉 angularjs(多種因素,比如已經(jīng)基于 angularjs 開發(fā)/使用 了一套完整的組件庫,代碼體量太大改造成本過高),你依然可以在局部使用 mobx/mobx-angularjs 改造應(yīng)用或開發(fā)新功能,在 mobx-angularjs 幫助你提升應(yīng)用性能的同時,也給你后續(xù)的升級計劃創(chuàng)造了可能性。愛掏網(wǎng) - it200.com


    原文發(fā)布時間:05/09

    原文作者:kuitos

    本文來源開源中國如需轉(zhuǎn)載請緊急聯(lián)系作者


    聲明:所有內(nèi)容來自互聯(lián)網(wǎng)搜索結(jié)果,不保證100%準確性,僅供參考。如若本站內(nèi)容侵犯了原著者的合法權(quán)益,可聯(lián)系我們進行處理。
    發(fā)表評論
    更多 網(wǎng)友評論0 條評論)
    暫無評論

    返回頂部

    主站蜘蛛池模板: 中文字幕一区二区免费| 国产一区二区影院| 久久一区二区精品| 日韩精品国产一区| 中文字幕乱码人妻一区二区三区| 国产成人综合精品一区| 在线一区二区观看| 麻豆一区二区99久久久久| 亚洲综合av永久无码精品一区二区| 韩国精品福利一区二区三区| 人妻夜夜爽天天爽爽一区| 中文字幕无码免费久久9一区9| 麻豆精品一区二区综合av| 成人精品视频一区二区| 久久精品无码一区二区三区不卡| 精品无码AV一区二区三区不卡| 久久se精品一区二区| 日本不卡一区二区三区视频| 中文字幕一区二区三区在线播放| 天码av无码一区二区三区四区 | 亚洲成人一区二区| 欧洲精品码一区二区三区| 国产精品无码亚洲一区二区三区 | 免费一区二区三区在线视频| 成人无号精品一区二区三区 | 亚洲无线码一区二区三区| 国产精品亚洲产品一区二区三区| 一区二区三区电影网| 精品无码成人片一区二区| 丝袜美腿高跟呻吟高潮一区| 国产精品视频无圣光一区| 日本一区二区三区四区视频| 国产一区韩国女主播| 视频一区二区在线播放| 精品国产AⅤ一区二区三区4区 | 国产亚洲日韩一区二区三区| 久久se精品一区精品二区| 精品一区二区三区在线观看视频 | 一区二区三区在线| 日韩人妻无码一区二区三区久久99| 亚洲AV无码一区二区乱子伦|