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

          Vue3 的響應(yīng)式和以前有什么區(qū)別,Proxy 無敵?(源碼級(jí)詳解)

          共 9974字,需瀏覽 20分鐘

           ·

          2020-04-14 23:31

          前言

          大家都知道,Vue2 里的響應(yīng)式其實(shí)有點(diǎn)像是一個(gè)半完全體,對于對象上新增的屬性無能為力,對于數(shù)組則需要攔截它的原型方法來實(shí)現(xiàn)響應(yīng)式。

          舉個(gè)例子:

          let?vm?=?new?Vue({
          ??data()?{
          ????a:?1
          ??}
          })

          //??? oops,沒反應(yīng)!
          vm.b?=?2?
          let?vm?=?new?Vue({
          ??data()?{
          ????a:?1
          ??},
          ??watch:?{
          ????b()?{
          ??????console.log('change?!!')
          ????}
          ??}
          })

          //??? oops,沒反應(yīng)!
          vm.b?=?2

          這種時(shí)候,Vue 提供了一個(gè) api:this.$set,來使得新增的屬性也擁有響應(yīng)式的效果。

          但是對于很多新手來說,很多時(shí)候需要小心翼翼的去判斷到底什么情況下需要用 $set,什么時(shí)候可以直接觸發(fā)響應(yīng)式。

          總之,在 Vue3 中,這些都將成為過去。本篇文章會(huì)帶你仔細(xì)講解,proxy 到底會(huì)給 Vue3 帶來怎么樣的便利。并且會(huì)從源碼級(jí)別,告訴你這些都是如何實(shí)現(xiàn)的。

          響應(yīng)式倉庫

          Vue3 不同于 Vue2 也體現(xiàn)在源碼結(jié)構(gòu)上,Vue3 把耦合性比較低的包分散在 packages 目錄下單獨(dú)發(fā)布成 npm 包。這也是目前很流行的一種大型項(xiàng)目管理方式 Monorepo。

          其中負(fù)責(zé)響應(yīng)式部分的倉庫就是 @vue/rectivity[1],它不涉及 Vue 的其他的任何部分,是非常非常 「正交」 的一種實(shí)現(xiàn)方式。

          甚至可以輕松的集成進(jìn) React[2]。

          這也使得本篇的分析可以更加聚焦的分析這一個(gè)倉庫,排除其他無關(guān)部分。

          區(qū)別

          Proxy 和 Object.defineProperty 的使用方法看似很相似,其實(shí) Proxy 是在 「更高維度」 上去攔截屬性的修改的,怎么理解呢?

          Vue2 中,對于給定的 data,如 { count: 1 },是需要根據(jù)具體的 key 也就是 count,去對「修改 data.count 」 和 「讀取 data.count」進(jìn)行攔截,也就是

          Object.defineProperty(data,?'count',?{
          ??get()?{},
          ??set()?{},
          })

          必須預(yù)先知道要攔截的 key 是什么,這也就是為什么 Vue2 里對于對象上的新增屬性無能為力。

          而 Vue3 所使用的 Proxy,則是這樣攔截的:

          new?Proxy(data,?{
          ??get(key)?{?},
          ??set(key,?value)?{?},
          })

          可以看到,根本不需要關(guān)心具體的 key,它去攔截的是 「修改 data 上的任意 key」 和 「讀取 data 上的任意 key」。

          所以,不管是已有的 key ?還是新增的 key,都逃不過它的魔爪。

          但是 Proxy 更加強(qiáng)大的地方還在于 Proxy 除了 get 和 set,還可以攔截更多的操作符。

          簡單的例子?

          先寫一個(gè) Vue3 響應(yīng)式的最小案例,本文的相關(guān)案例都只會(huì)用 reactiveeffect 這兩個(gè) api。如果你了解過 React 中的 useEffect,相信你會(huì)對這個(gè)概念秒懂,Vue3 的 effect 不過就是去掉了手動(dòng)聲明依賴的「進(jìn)化版」的 useEffect。

          React 中手動(dòng)聲明 [data.count] 這個(gè)依賴的步驟被 Vue3 內(nèi)部直接做掉了,在 effect 函數(shù)內(nèi)部讀取到 data.count 的時(shí)候,它就已經(jīng)被收集作為依賴了。

          Vue3:

          //?響應(yīng)式數(shù)據(jù)
          const?data?=?reactive({?
          ??count:?1
          })

          //?觀測變化
          effect(()?=>?console.log('count?changed',?data.count))

          //?觸發(fā)?console.log('count?changed',?data.count)?重新執(zhí)行
          data.count?=?2

          React:

          //?數(shù)據(jù)
          const?[data,?setData]?=?useState({
          ??count:?1
          })

          //?觀測變化?需要手動(dòng)聲明依賴
          useEffect(()?=>?{
          ??console.log('count?changed',?data.count)
          },?[data.count])

          //?觸發(fā)?console.log('count?changed',?data.count)?重新執(zhí)行
          setData(({
          ??count:?2
          }))

          其實(shí)看到這個(gè)案例,聰明的你也可以把 effect 中的回調(diào)函數(shù)聯(lián)想到視圖的重新渲染、 watch 的回調(diào)函數(shù)等等…… 它們是同樣基于這套響應(yīng)式機(jī)制的。

          而本文的核心目的,就是探究這個(gè)基于 Proxy 的 reactive api,到底能強(qiáng)大到什么程度,能監(jiān)聽到用戶對于什么程度的修改。

          先講講原理

          先最小化的講解一下響應(yīng)式的原理,其實(shí)就是在 Proxy 第二個(gè)參數(shù) handler 也就是陷阱操作符[3]中,攔截各種取值、賦值操作,依托 tracktrigger 兩個(gè)函數(shù)進(jìn)行依賴收集和派發(fā)更新。

          track 用來在讀取時(shí)收集依賴。

          trigger 用來在更新時(shí)觸發(fā)依賴。

          track

          function?track(target:?object,?type:?TrackOpTypes,?key:?unknown)?{
          ??const?depsMap?=?targetMap.get(target);
          ??//?收集依賴時(shí)?通過?key?建立一個(gè)?set
          ??let?dep?=?new?Set()
          ??targetMap.set(ITERATE_KEY,?dep)
          ??//?這個(gè)?effect?可以先理解為更新函數(shù)?存放在?dep?里
          ??dep.add(effect)????
          }

          target 是原對象。

          type 是本次收集的類型,也就是收集依賴的時(shí)候用來標(biāo)識(shí)是什么類型的操作,比如上文依賴中的類型就是 get,這個(gè)后續(xù)會(huì)詳細(xì)講解。

          key 是指本次訪問的是數(shù)據(jù)中的哪個(gè) key,比如上文例子中收集依賴的 key 就是 count

          首先全局會(huì)存在一個(gè) targetMap,它用來建立 數(shù)據(jù) -> 依賴 的映射,它是一個(gè) WeakMap 數(shù)據(jù)結(jié)構(gòu)。

          targetMap 通過數(shù)據(jù) target,可以獲取到 depsMap,它用來存放這個(gè)數(shù)據(jù)對應(yīng)的所有響應(yīng)式依賴。

          depsMap 的每一項(xiàng)則是一個(gè) Set 數(shù)據(jù)結(jié)構(gòu),而這個(gè) Set 就存放著對應(yīng) key 的更新函數(shù)。

          是不是有點(diǎn)繞?我們用一個(gè)具體的例子來舉例吧。

          const?target?=?{?count:?1}
          const?data?=?reactive(target)

          const?effection?=?effect(()?=>?{
          ??console.log(data.count)
          })

          對于這個(gè)例子的依賴關(guān)系,

          1. 全局的 targetMap 是:
          targetMap:?{
          ??{?count:?1?}:?dep????
          }
          1. dep 則是
          dep:?{
          ??count:?Set?{?effection?}
          }

          這樣一層層的下去,就可以通過 target 找到 count 對應(yīng)的更新函數(shù) effection 了。

          trigger

          這里是最小化的實(shí)現(xiàn),僅僅為了便于理解原理,實(shí)際上要復(fù)雜很多,

          其實(shí) type 的作用很關(guān)鍵,先記住,后面會(huì)詳細(xì)講。

          export?function?trigger(
          ??target:?object,
          ??type:?TriggerOpTypes,
          ??key?:?unknown,
          )?
          {
          ??//?簡化來說?就是通過?key?找到所有更新函數(shù)?依次執(zhí)行
          ??const?dep?=?targetMap.get(target)
          ??dep.get(key).forEach(effect?=>?effect())
          }

          新增屬性

          這個(gè)上文已經(jīng)講了,由于 Proxy 完全不關(guān)心具體的 key,所以沒問題。

          //?響應(yīng)式數(shù)據(jù)
          const?data?=?reactive({?
          ??count:?1
          })

          //?觀測變化
          effect(()?=>?console.log('newCount?changed',?data.newCount))

          //???觸發(fā)響應(yīng)
          data.newCount?=?2

          數(shù)組新增索引:

          //?響應(yīng)式數(shù)據(jù)
          const?data?=?reactive([])

          //?觀測變化
          effect(()?=>?console.log('data[1]?changed',?data[1]))

          //???觸發(fā)響應(yīng)
          data[1]?=?5

          數(shù)組調(diào)用原生方法:

          const?data?=?reactive([])
          effect(()?=>?console.log('c',?data[1]))

          //?沒反應(yīng)
          data.push(1)

          //???觸發(fā)響應(yīng)?因?yàn)樾薷牧讼聵?biāo)為?1?的值
          data.push(2)

          其實(shí)這一個(gè)案例就比較有意思了,我們僅僅是在調(diào)用 push,但是等到數(shù)組的第二項(xiàng)被 push的時(shí)候,我們之前關(guān)注 data[1] 為依賴的回調(diào)函數(shù)也執(zhí)行了,這是什么原理呢?寫個(gè)簡單的 Proxy 就知道了。

          const?raw?=?[]
          const?arr?=?new?Proxy(raw,?{
          ??get(target,?key)?{
          ????console.log('get',?key)
          ????return?Reflect.get(target,?key)
          ??},
          ??set(target,?key,?value)?{
          ????console.log('set',?key)
          ????return?Reflect.set(target,?key,?value)
          ??}
          })

          arr.push(1)

          在這個(gè)案例中,我們只是打印出了對于 raw 這個(gè)數(shù)組上的所有 get、set 操作,并且調(diào)用 Reflect[4] 這個(gè) api 原樣處理取值和賦值操作后返回??纯?arr.push(1) 后控制臺(tái)打印出了什么?

          get push
          get length
          set 0
          set length

          原來一個(gè)小小的 push,會(huì)觸發(fā)兩對 get 和 set,我們來想象一下流程:

          1. 讀取 push 方法
          2. 讀取 arr 原有的 length 屬性
          3. 對于數(shù)組第 0 項(xiàng)賦值
          4. 對于 length 屬性賦值

          這里的重點(diǎn)是第三步,對于第 index 項(xiàng)的賦值,那么下次再 push,可以想象也就是對于第 1 項(xiàng)觸發(fā) set 操作。

          而我們在例子中讀取 data[1],是一定會(huì)把對于 1 這個(gè)下標(biāo)的依賴收集起來的,這也就清楚的解釋了為什么 push 的時(shí)候也能精準(zhǔn)的觸發(fā)響應(yīng)式依賴的執(zhí)行。

          對了,記住這個(gè)對于 length 的 set 操作,后面也會(huì)用到,很重要。

          遍歷后新增

          //?響應(yīng)式數(shù)據(jù)
          const?data?=?reactive([])

          //?觀測變化
          effect(()?=>?console.log('data?map?+1',?data.map(item?=>?item?+?1))

          //???觸發(fā)響應(yīng)?打印出?[2]
          data.push(1)

          這個(gè)攔截很神奇,但是也很合理,轉(zhuǎn)化成現(xiàn)實(shí)里的一個(gè)例子來看,

          假設(shè)我們要根據(jù)學(xué)生 id 的集合 ids, 去請求學(xué)生詳細(xì)信息,那么僅僅是需要這樣寫即可:

          const?state?=?reactive({})
          const?ids?=?reactive([1])

          effect(async?()?=>?{
          ??state.students?=?await?axios.get('students/batch',?ids.map(id?=>?({?id?})))
          })

          //???觸發(fā)響應(yīng)?
          ids.push(2)

          這樣,每次調(diào)用各種 api 改變 ids 數(shù)組,都會(huì)重新發(fā)送請求獲取最新的學(xué)生列表。

          如果我在監(jiān)聽函數(shù)中調(diào)用了 map、forEach 等 api,

          說明我關(guān)心這個(gè)數(shù)組的長度變化,那么 push 的時(shí)候觸發(fā)響應(yīng)是完全正確的。

          但是它是如何實(shí)現(xiàn)的呢?感覺似乎很復(fù)雜啊。

          因?yàn)?effect 第一次執(zhí)行的時(shí)候, data 還是個(gè)空數(shù)組,怎么會(huì) push 的時(shí)候能觸發(fā)更新呢?

          還是用剛剛的小測試,看看 map 的時(shí)候會(huì)發(fā)生什么事情。

          const?raw?=?[1,?2]
          const?arr?=?new?Proxy(raw,?{
          ??get(target,?key)?{
          ????console.log('get',?key)
          ????return?Reflect.get(target,?key)
          ??},
          ??set(target,?key,?value)?{
          ????console.log('set',?key)
          ????return?Reflect.set(target,?key,?value)
          ??}
          })

          arr.map(v?=>?v?+?1)
          get?map
          get?length
          get?constructor
          get?0
          get?1

          和 push 的部分有什么相同的?找一下線索,我們發(fā)現(xiàn) map 的時(shí)候會(huì)觸發(fā) get length,而在觸發(fā)更新的時(shí)候, Vue3 內(nèi)部會(huì)對 「新增 key」 的操作進(jìn)行特殊處理,這里是新增了 0 這個(gè)下標(biāo)的值,會(huì)走到 trigger ?中這樣的一段邏輯里去:

          源碼地址[5]

          //?簡化版
          if?(isAddOrDelete)?{
          ??add(depsMap.get('length'))
          }

          把之前讀取 length 時(shí)收集到的依賴拿到,然后觸發(fā)函數(shù)。

          這就一目了然了,我們在 effect 里 map 操作讀取了 length,收集了 length 的依賴。

          在新增 key 的時(shí)候, 觸發(fā) length 收集到的依賴,觸發(fā)回調(diào)函數(shù)即可。

          對了,對于 for of 操作,也一樣可行:

          //?響應(yīng)式數(shù)據(jù)
          const?data?=?reactive([])

          //?觀測變化
          effect(()?=>?{
          ??for?(const?val?of?data)?{
          ????console.log('val',?val)
          ??}
          })

          //???觸發(fā)響應(yīng)?打印出?val?1
          data.push(1)

          可以按我們剛剛的小試驗(yàn)自己跑一下攔截, for of 也會(huì)觸發(fā) length 的讀取。

          length 真是個(gè)好同志…… 幫了大忙了。

          遍歷后刪除或者清空

          注意上面的源碼里的判斷條件是 isAddOrDelete,所以刪除的時(shí)候也是同理,借助了 length 上收集到的依賴。

          //?簡化版
          if?(isAddOrDelete)?{
          ??add(depsMap.get('length'))
          }
          const?arr?=?reactive([1])
          ??
          effect(()?=>?{
          ??console.log('arr',?arr.map(v?=>?v))
          })

          //???觸發(fā)響應(yīng)?
          arr.length?=?0

          //???觸發(fā)響應(yīng)?
          arr.splice(0,?1)

          真的是什么操作都能響應(yīng),愛了愛了。

          獲取 keys

          const?obj?=?reactive({?a:?1?})
          ??
          effect(()?=>?{
          ??console.log('keys',?Reflect.ownKeys(obj))
          })

          effect(()?=>?{
          ??console.log('keys',?Object.keys(obj))
          })

          effect(()?=>?{
          ??for?(let?key?in?obj)?{
          ????console.log(key)
          ??}
          })

          //???觸發(fā)所有響應(yīng)?
          obj.b?=?2

          這幾種獲取 key 的方式都能成功的攔截,其實(shí)這是因?yàn)?Vue 內(nèi)部攔截了 ownKeys 操作符。

          const?ITERATE_KEY?=?Symbol(?'iterate'?);

          function?ownKeys(target)?{
          ????track(target,?"iterate",?ITERATE_KEY);
          ????return?Reflect.ownKeys(target);
          }

          ITERATE_KEY 就作為一個(gè)特殊的標(biāo)識(shí)符,表示這是讀取 key 的時(shí)候收集到的依賴。它會(huì)被作為依賴收集的 key。

          那么在觸發(fā)更新時(shí),其實(shí)就對應(yīng)這段源碼:

          if?(isAddOrDelete)?{
          ????add(depsMap.get(isArray(target)???'length'?:?ITERATE_KEY));
          }

          其實(shí)就是我們聊數(shù)組的時(shí)候,代碼簡化掉的那部分。判斷非數(shù)組,則觸發(fā) ITERATE_KEY 對應(yīng)的依賴。

          小彩蛋:

          Reflect.ownKeys、 Object.keysfor in 其實(shí)行為是不同的,

          Reflect.ownKeys 可以收集到 Symbol 類型的 key,不可枚舉的 key。

          舉例來說:

          var?a?=?{
          ??[Symbol(2)]:?2,
          }

          Object.defineProperty(a,?'b',?{
          ??enumerable:?false,
          })

          Reflect.ownKeys(a)?//?[Symbol(2),?'b']
          Object.keys(a)?//?[]

          ownKeys 攔截內(nèi)部直接之間返回了 Reflect.ownKeys(target),按理來說這個(gè)時(shí)候 Object.keys 的操作經(jīng)過了這個(gè)攔截,也會(huì)按照 Reflect.ownKeys 的行為去返回值。

          然而最后返回的結(jié)果卻還是 Object.keys 的結(jié)果,這是比較神奇的一點(diǎn)。

          刪除對象屬性

          有了上面 ownKeys 的基礎(chǔ),我們再來看看這個(gè)例子

          const?obj?=?reactive({?a:?1,?b:?2})
          ??
          effect(()?=>?{
          ??console.log(Object.keys(obj))
          })

          //???觸發(fā)響應(yīng)?
          delete?obj['b']

          這也是個(gè)神奇的操作,原理在于對于 deleteProperty 操作符的攔截:

          function?deleteProperty(target:?object,?key:?string?|?symbol):?boolean?{
          ??const?result?=?Reflect.deleteProperty(target,?key)
          ??trigger(target,?TriggerOpTypes.DELETE,?key)
          ??return?result
          }

          這里又用到了 TriggerOpTypes.DELETE 的類型,根據(jù)上面的經(jīng)驗(yàn),一定對它有一些特殊的處理。

          其實(shí)還是 trigger 中的那段邏輯:

          const?isAddOrDelete?=?type?===?TriggerOpTypes.ADD?||?type?===?TriggerOpTypes.DELETE
          if?(isAddOrDelete)?{
          ??add(depsMap.get(isArray(target)???'length'?:?ITERATE_KEY))
          }

          這里的 target 不是數(shù)組,所以還是會(huì)去觸發(fā) ITERATE_KEY 收集的依賴,也就是上面例子中剛提到的對于 key 的讀取收集到的依賴。

          判斷屬性是否存在

          const?obj?=?reactive({})

          effect(()?=>?{
          ??console.log('has',?Reflect.has(obj,?'a'))
          })

          effect(()?=>?{
          ??console.log('has',?'a'?in?obj)
          })

          effect(()?=>?{
          ??console.log('has',?Object.hasOwnProperty(obj,?'a'))
          })

          //???觸發(fā)兩次響應(yīng)?
          obj.a?=?1

          這個(gè)就很簡單了,就是利用了 has 操作符的攔截。

          function?has(target,?key)?{
          ??const?result?=?Reflect.has(target,?key);
          ??track(target,?"has",?key);
          ??return?result;
          }

          Map 和 Set

          其實(shí) Vue3 對于這兩種數(shù)據(jù)類型也是完全支持響應(yīng)式的,對于它們的原型方法也都做了完善的攔截,限于篇幅原因本文不再贅述。

          說實(shí)話 Vue3 的響應(yīng)式部分代碼邏輯分支還是有點(diǎn)過多,對于代碼理解不是很友好,因?yàn)樗€會(huì)涉及到 readonly 等只讀化的操作,如果看完這篇文章你對于 Vue3 的響應(yīng)式原理非常感興趣的話,建議從簡化版的庫入手去讀源碼。

          這里我推薦 observer-util[6],我解讀過這個(gè)庫的源碼,和 Vue3 的實(shí)現(xiàn)原理基本上是一模一樣!但是簡單了很多。麻雀雖小,五臟俱全。

          對于這個(gè)庫的解讀,可以看我之前的兩篇文章:

          帶你徹底搞懂Vue3的Proxy響應(yīng)式原理!TypeScript從零實(shí)現(xiàn)基于Proxy的響應(yīng)式庫。[7]

          帶你徹底搞懂Vue3的Proxy響應(yīng)式原理!基于函數(shù)劫持實(shí)現(xiàn)Map和Set的響應(yīng)式[8]

          在第二篇文章里,你也可以對于 Map 和 Set 可以做什么攔截操作,獲得源碼級(jí)別的理解。

          總結(jié)

          Vue3 的 Proxy 真的很強(qiáng)大,把 Vue2 里我認(rèn)為心智負(fù)擔(dān)很大的一部分給解決掉了。(在我剛上手 Vue 的時(shí)候,我是真的不知道什么情況下該用 $set),它的 composition-api 又可以完美對標(biāo) React Hook,并且得益于響應(yīng)式系統(tǒng)的強(qiáng)大,在某些方面是優(yōu)勝于它的。精讀《Vue3.0 Function API》[9]

          希望這篇文章能在 Vue3 正式到來之前,提前帶你熟悉 Vue3 的一些新特性。

          擴(kuò)展閱讀

          Proxy 的攔截器里有個(gè) receiver 參數(shù),在本文中為了簡化沒有體現(xiàn)出來,它是用來做什么的?

          new?Proxy(raw,?{
          ??get(target,?key,?receiver)?{
          ????return?Reflect.get(target,?key,?receiver)
          ??}
          })

          what-is-a-receiver-in-javascript[10]

          參考資料

          [1]

          @vue/rectivity: https://github.com/vuejs/vue-next/tree/master/packages/reactivity

          [2]

          輕松的集成進(jìn) React: https://juejin.im/post/5e70970af265da576429aada

          [3]

          陷阱操作符: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler

          [4]

          Reflect: https://es6.ruanyifeng.com/?search=reflect&x=0&y=0#docs/reflect

          [5]

          源碼地址: https://github.com/vuejs/vue-next/blob/0764c33d3da8c06d472893a4e451e33394726a42/packages/reactivity/src/effect.ts#L214-L219

          [6]

          observer-util: https://github.com/nx-js/observer-util

          [7]

          帶你徹底搞懂Vue3的Proxy響應(yīng)式原理!TypeScript從零實(shí)現(xiàn)基于Proxy的響應(yīng)式庫。: https://juejin.im/post/5e21196fe51d454d523be084

          [8]

          帶你徹底搞懂Vue3的Proxy響應(yīng)式原理!基于函數(shù)劫持實(shí)現(xiàn)Map和Set的響應(yīng)式: https://juejin.im/post/5e23b20f51882510073eb571

          [9]

          精讀《Vue3.0 Function API》: https://juejin.im/post/5d1955e3e51d4556d86c7b09

          [10]

          what-is-a-receiver-in-javascript: https://stackoverflow.com/questions/37563495/what-is-a-receiver-in-javascript/37565299#37565299




          推薦閱讀




          我的公眾號(hào)能帶來什么價(jià)值?(文末有送書規(guī)則,一定要看)

          每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

          瀏覽 33
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  影音先锋av黄色 影音先锋成人网址 | 欧美啪啪自拍 | 私人玩物七七 | 青青草成人免费在线视频 | 国产激情av在线观看 |