成人怡红院-成人怡红院视频在线观看-成人影视大全-成人影院203nnxyz-美女毛片在线看-美女免费黄

站長資訊網
最全最豐富的資訊網站

深入理解vue2中的VNode和diff算法

深入理解vue2中的VNode和diff算法

node.js極速入門課程:進入學習

虛擬domdiff算法是vue學習過程中的一個難點,也是面試中必須掌握的一個知識點。這兩者相輔相成,是vue框架的核心。今天我們再來總結下vue2中的虛擬domdiff算法。(學習視頻分享:vue視頻教程)

什么是 VNode

我們知道,render function 會被轉化成 VNodeVNode 其實就是一棵以 JavaScript 對象作為基礎的樹,用對象屬性來描述節點,實際上它只是一層對真實 DOM 的抽象。最終可以通過一系列操作使這棵樹映射到真實環境上。

比如有如下template

<template>   <span class="demo" v-show="isShow"> This is a span. </span>  </template>
登錄后復制

它換成 VNode 以后大概就是下面這個樣子

{   tag: "span",   data: {     /* 指令集合數組 */     directives: [       {         /* v-show指令 */         rawName: "v-show",         expression: "isShow",         name: "show",         value: true,       },     ],     /* 靜態class */     staticClass: "demo",   },   text: undefined,   children: [     /* 子節點是一個文本VNode節點 */     {       tag: undefined,       data: undefined,       text: "This is a span.",       children: undefined,     },   ], };
登錄后復制

總的來說,VNode 就是一個 JavaScript 對象。這個JavaScript 對象能完整地表示出真實DOM

為什么vue要使用 VNode

筆者認為有兩點原因

  • 由于 Virtual DOM 是以 JavaScript 對象為基礎而不依賴真實平臺環境,所以使它具有了跨平臺的能力,比如說瀏覽器平臺、Weex、Node 等。

  • 減少操作DOM,任何頁面的變化,都只使用VNode進行操作對比,只需要在最后一次進行掛載更新DOM,避免了頻繁操作DOM,減少了瀏覽器的回流和重繪從而提高頁面性能

diff算法

下面我們來看看組件更新所涉及到的diff算法

前面我們講依賴收集的時候有說到,渲染watcher傳遞給Watcherget方法其實是updateComponent方法。

updateComponent = () => {   vm._update(vm._render(), hydrating) }  new Watcher(vm, updateComponent, noop, {   before () {     if (vm._isMounted) {       callHook(vm, 'beforeUpdate')     }   } }, true /* isRenderWatcher */)
登錄后復制

所以組件在響應式數據發生變化的時候會再次觸發該方法,接下來我們來詳細分析一下updateComponent里面的_update方法。

_update

_update方法中做了初始渲染和更新的區分,雖然都是調用__patch__方法,但是傳遞的參數不一樣。

// src/core/instance/lifecycle.js  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {   const vm: Component = this   const prevEl = vm.$el   const prevVnode = vm._vnode   vm._vnode = vnode   // 初次渲染沒有 prevVnode,組件更新才會有   if (!prevVnode) {     // 初次渲染     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)   } else {     // 更新     vm.$el = vm.__patch__(prevVnode, vnode)   }      // ... }
登錄后復制

下面我們再來看看__patch__方法

__patch__

patch方法接收四個參數,由于初始渲染的時候oldVnodevm.$elnull,所以初始渲染是沒有oldVnode

// src/core/vdom/patch.js  return function patch (oldVnode, vnode, hydrating, removeOnly) {   // 新節點不存在,只有oldVnode就直接銷毀,然后返回   if (isUndef(vnode)) {     if (isDef(oldVnode)) invokeDestroyHook(oldVnode)     return   }    let isInitialPatch = false   const insertedVnodeQueue = []   // 沒有老節點,直接創建,也就是初始渲染   if (isUndef(oldVnode)) {     isInitialPatch = true     createElm(vnode, insertedVnodeQueue)   } else {     const isRealElement = isDef(oldVnode.nodeType)     // 不是真實dom,并且是相同節點走patch     if (!isRealElement && sameVnode(oldVnode, vnode)) {       // 這里才會涉及到diff算法       patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)     } else {       if (isRealElement) {         // ...       }        // replacing existing element       const oldElm = oldVnode.elm       const parentElm = nodeOps.parentNode(oldElm)        // 1.創建一個新節點       createElm(         vnode,         insertedVnodeQueue,         // extremely rare edge case: do not insert if old element is in a         // leaving transition. Only happens when combining transition +         // keep-alive + HOCs. (#4590)         oldElm._leaveCb ? null : parentElm,         nodeOps.nextSibling(oldElm)       )        // 2.更新父節點占位符       if (isDef(vnode.parent)) {         let ancestor = vnode.parent         const patchable = isPatchable(vnode)         while (ancestor) {           for (let i = 0; i < cbs.destroy.length; ++i) {             cbs.destroy[i](ancestor)           }           ancestor.elm = vnode.elm           if (patchable) {             for (let i = 0; i < cbs.create.length; ++i) {               cbs.create[i](emptyNode, ancestor)             }                          const insert = ancestor.data.hook.insert             if (insert.merged) {               // start at index 1 to avoid re-invoking component mounted hook               for (let i = 1; i < insert.fns.length; i++) {                 insert.fns[i]()               }             }           } else {             registerRef(ancestor)           }           ancestor = ancestor.parent         }       }        // 3.刪除老節點       if (isDef(parentElm)) {         removeVnodes([oldVnode], 0, 0)       } else if (isDef(oldVnode.tag)) {         invokeDestroyHook(oldVnode)       }     }   }        //觸發插入鉤子   invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)   return vnode.elm }
登錄后復制

patch方法大概流程如下:

  • 沒有新節點只有老節點直接刪除老節點。

  • 只有新節點沒有老節點直接添加新節點。

  • 既有新節點又有老節點則判斷是不是相同節點,相同則進入pathVnodepatchVnode我們后面會重點分析。

  • 既有新節點又有老節點則判斷是不是相同節點,不相同則直接刪除老節點添加新節點。

我們再來看看它是怎么判斷是同一個節點的。

// src/core/vdom/patch.js  function sameVnode (a, b) {   return (     a.key === b.key &&     a.asyncFactory === b.asyncFactory && (       (         a.tag === b.tag &&         a.isComment === b.isComment &&         isDef(a.data) === isDef(b.data) &&         sameInputType(a, b)       ) || (         isTrue(a.isAsyncPlaceholder) &&         isUndef(b.asyncFactory.error)       )     )   ) }  function sameInputType (a, b) {   if (a.tag !== 'input') return true   let i   const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type   const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type   return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
登錄后復制

判斷兩個VNode節點是否是同一個節點,需要同時滿足以下條件

  • key相同

  • 都有異步組件工廠函數

  • tag(當前節點的標簽名)相同

  • isComment是否同為注釋節點

  • 是否data(當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型)

  • 當標簽是<input>的時候,type必須相同

當兩個VNodetag、key、isComment都相同,并且同時定義或未定義data的時候,且如果標簽為input則type必須相同。這時候這兩個VNode則算sameVnode,可以直接進行patchVnode操作。

patchVnode

下面我們再來詳細分析下patchVnode方法。

// src/core/vdom/patch.js  function patchVnode (   oldVnode,   vnode,   insertedVnodeQueue,   ownerArray,   index,   removeOnly ) {   // 兩個vnode相同則直接返回   if (oldVnode === vnode) {     return   }    if (isDef(vnode.elm) && isDef(ownerArray)) {     // clone reused vnode     vnode = ownerArray[index] = cloneVNode(vnode)   }    const elm = vnode.elm = oldVnode.elm    if (isTrue(oldVnode.isAsyncPlaceholder)) {     if (isDef(vnode.asyncFactory.resolved)) {       hydrate(oldVnode.elm, vnode, insertedVnodeQueue)     } else {       vnode.isAsyncPlaceholder = true     }     return   }    /*     如果新舊VNode都是靜態的,同時它們的key相同(代表同一節點),     并且新的VNode是clone或者是標記了once(標記v-once屬性,只渲染一次),     那么只需要替換componentInstance即可。   */   if (isTrue(vnode.isStatic) &&     isTrue(oldVnode.isStatic) &&     vnode.key === oldVnode.key &&     (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))   ) {     vnode.componentInstance = oldVnode.componentInstance     return   }    let i   const data = vnode.data   /*調用prepatch鉤子*/   if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {     i(oldVnode, vnode)   }    // 獲取新老虛擬節點的子節點   const oldCh = oldVnode.children   const ch = vnode.children   if (isDef(data) && isPatchable(vnode)) {     for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)     if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)   }      // 新節點不是文本節點   if (isUndef(vnode.text)) {     /*新老節點均有children子節點,則對子節點進行diff操作,調用updateChildren*/     if (isDef(oldCh) && isDef(ch)) {       if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)     /*如果只有新節點有子節點,先清空elm文本內容,然后為當前DOM節點加入子節點。*/     } else if (isDef(ch)) {       if (process.env.NODE_ENV !== 'production') {         checkDuplicateKeys(ch)       }       if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')       addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)     /*如果只有老節點有子節點,則移除elm所有子節點*/     } else if (isDef(oldCh)) {       removeVnodes(oldCh, 0, oldCh.length - 1)     /*當新老節點都無子節點的時候,因為這個邏輯中新節點text不存在,所以直接去除ele的文本*/     } else if (isDef(oldVnode.text)) {       nodeOps.setTextContent(elm, '')     }   // 新節點是文本節點,如果文本不一樣就設置新的文本     } else if (oldVnode.text !== vnode.text) {     nodeOps.setTextContent(elm, vnode.text)   }   /*調用postpatch鉤子*/   if (isDef(data)) {     if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)   } }
登錄后復制

patchVnode方法大概流程如下:

1.新老節點相同就直接返回。

2.如果新舊VNode都是靜態的,同時它們的key相同(代表同一節點),并且新的VNode是clone或者是標記了once(標記v-once屬性,只渲染一次),那么只需要替換componentInstance即可。

3.新節點不是文本節點,新老節點均有children子節點,則對子節點進行diff操作,調用updateChildren,這個updateChildrendiff算法的核心,后面我們會重點說。

4.新節點不是文本節點,如果老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文本內容,然后為當前DOM節點加入子節點。

5.新節點不是文本節點,當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的所有子節點。

6.新節點不是文本節點,并且新老節點都無子節點的時候,只需要將老節點文本清空。

7.新節點是文本節點,并且新老節點文本不一樣,則進行文本的替換。

updateChildren(diff算法核心)

updateChildrendiff算法的核心,下面我們來重點分析。

深入理解vue2中的VNode和diff算法

這兩張圖代表舊的VNode與新VNode進行patch的過程,他們只是在同層級的VNode之間進行比較得到變化(相同顏色的方塊代表互相進行比較的VNode節點),然后修改變化的視圖,所以十分高效。所以Diff算法是:深度優先算法。 時間復雜度:O(n)

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {   let oldStartIdx = 0   let newStartIdx = 0   let oldEndIdx = oldCh.length - 1   let oldStartVnode = oldCh[0]   let oldEndVnode = oldCh[oldEndIdx]   let newEndIdx = newCh.length - 1   let newStartVnode = newCh[0]   let newEndVnode = newCh[newEndIdx]   let oldKeyToIdx, idxInOld, vnodeToMove, refElm    const canMove = !removeOnly    if (process.env.NODE_ENV !== 'production') {     checkDuplicateKeys(newCh)   }    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {     if (isUndef(oldStartVnode)) {       oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left     } else if (isUndef(oldEndVnode)) {       oldEndVnode = oldCh[--oldEndIdx]     // 老 VNode 節點的頭部與新 VNode 節點的頭部是相同的 VNode 節點,直接進行 patchVnode,同時 oldStartIdx 與 newStartIdx 向后移動一位。     } else if (sameVnode(oldStartVnode, newStartVnode)) {       patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)       oldStartVnode = oldCh[++oldStartIdx]       newStartVnode = newCh[++newStartIdx]     // 兩個 VNode 的結尾是相同的 VNode,同樣進行 patchVnode 操作。并將 oldEndVnode 與 newEndVnode 向前移動一位。     } else if (sameVnode(oldEndVnode, newEndVnode)) {       patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)       oldEndVnode = oldCh[--oldEndIdx]       newEndVnode = newCh[--newEndIdx]     // oldStartVnode 與 newEndVnode 符合 sameVnode 的時候,     // 將 oldStartVnode.elm 這個節點直接移動到 oldEndVnode.elm 這個節點的后面即可。     // 然后 oldStartIdx 向后移動一位,newEndIdx 向前移動一位。     } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right       patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)       canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))       oldStartVnode = oldCh[++oldStartIdx]       newEndVnode = newCh[--newEndIdx]     // oldEndVnode 與 newStartVnode 符合 sameVnode 時,     // 將 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。     // oldEndIdx 向前移動一位,newStartIdx 向后移動一位。     } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left       patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)       canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)       oldEndVnode = oldCh[--oldEndIdx]       newStartVnode = newCh[++newStartIdx]     } else {       // createKeyToOldIdx 的作用是產生 key 與 index 索引對應的一個 map 表       if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)       idxInOld = isDef(newStartVnode.key)         ? oldKeyToIdx[newStartVnode.key]         : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)       // 如果沒有找到相同的節點,則通過 createElm 創建一個新節點,并將 newStartIdx 向后移動一位。       if (isUndef(idxInOld)) { // New element         createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)       } else {         vnodeToMove = oldCh[idxInOld]         // 如果找到了節點,同時它符合 sameVnode,則將這兩個節點進行 patchVnode,將該位置的老節點賦值 undefined         // 同時將 newStartVnode.elm 插入到 oldStartVnode.elm 的前面         if (sameVnode(vnodeToMove, newStartVnode)) {           patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)           oldCh[idxInOld] = undefined           canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)         } else {           // 如果不符合 sameVnode,只能創建一個新節點插入到 parentElm 的子節點中,newStartIdx 往后移動一位。           createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)         }       }       newStartVnode = newCh[++newStartIdx]     }   }   // 當 while 循環結束以后,如果 oldStartIdx > oldEndIdx,說明老節點比對完了,但是新節點還有多的,   // 需要將新節點插入到真實 DOM 中去,調用 addVnodes 將這些節點插入即可。   if (oldStartIdx > oldEndIdx) {     refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm     addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)   // 如果滿足 newStartIdx > newEndIdx 條件,說明新節點比對完了,老節點還有多,   // 將這些無用的老節點通過 removeVnodes 批量刪除即可。   } else if (newStartIdx > newEndIdx) {     removeVnodes(oldCh, oldStartIdx, oldEndIdx)   } }
登錄后復制

vue2diff算法采用的是雙端比較,所謂雙端比較就是新列表舊列表兩個列表的頭與尾互相對比,在對比的過程中指針會逐漸向內靠攏,直到某一個列表的節點全部遍歷過,對比停止。

首尾對比的四種情況

我們首先來看看首尾對比的四種情況。

  • 使用舊列表的頭一個節點oldStartNode新列表的頭一個節點newStartNode對比

  • 使用舊列表的最后一個節點oldEndNode新列表的最后一個節點newEndNode對比

  • 使用舊列表的頭一個節點oldStartNode新列表的最后一個節點newEndNode對比

  • 使用舊列表的最后一個節點oldEndNode新列表的頭一個節點newStartNode對比

首先是 oldStartVnodenewStartVnode 符合 sameVnode 時,說明老 VNode 節點的頭部與新 VNode 節點的頭部是相同的 VNode 節點,直接進行 patchVnode,同時 oldStartIdxnewStartIdx 向后移動一位。

深入理解vue2中的VNode和diff算法

其次是 oldEndVnodenewEndVnode 符合 sameVnode,也就是兩個 VNode 的結尾是相同的 VNode,同樣進行 patchVnode 操作并將 oldEndVnodenewEndVnode 向前移動一位。

深入理解vue2中的VNode和diff算法

接下來是兩種交叉的情況。

先是 oldStartVnodenewEndVnode 符合 sameVnode 的時候,也就是老 VNode 節點的頭部與新 VNode 節點的尾部是同一節點的時候,將 oldStartVnode.elm 這個節點直接移動到 oldEndVnode.elm 這個節點的后面即可。然后 oldStartIdx 向后移動一位,newEndIdx 向前移動一位。

深入理解vue2中的VNode和diff算法

同理,oldEndVnodenewStartVnode 符合 sameVnode 時,也就是老 VNode 節點的尾部與新 VNode 節點的頭部是同一節點的時候,將 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。同樣的,oldEndIdx 向前移動一位,newStartIdx 向后移動一位。

深入理解vue2中的VNode和diff算法

最后是當以上情況都不符合的時候,這種情況怎么處理呢?

查找對比

那就是查找對比。

首先通過createKeyToOldIdx方法生成oldVnodekeyindex 索引對應的一個 map 表。

然后我們根據newStartVnode.key,可以快速地從 oldKeyToIdxcreateKeyToOldIdx 的返回值)中獲取相同 key 的節點的索引 idxInOld,然后找到相同的節點。

這里又分三種情況

  • 如果沒有找到相同的節點,則通過 createElm 創建一個新節點,并將 newStartIdx 向后移動一位。

  • 如果找到了節點,同時它符合 sameVnode,則將這兩個節點進行 patchVnode,將該位置的老節點賦值 undefined(之后如果還有新節點與該節點key相同可以檢測出來提示已有重復的 key ),同時將 newStartVnode.elm 插入到 oldStartVnode.elm 的前面。同理,newStartIdx 往后移動一位。

深入理解vue2中的VNode和diff算法

  • 如果不符合 sameVnode,只能創建一個新節點插入到 parentElm 的子節點中,newStartIdx 往后移動一位。

深入理解vue2中的VNode和diff算法

添加、刪除節點

最后一步就很容易啦,當 while 循環結束以后,如果 oldStartIdx > oldEndIdx,說明老節點比對完了,但是新節點還有多的,需要將新節點插入到真實 DOM 中去,調用 addVnodes 將這些節點插入即可。

深入理解vue2中的VNode和diff算法

同理,如果滿足 newStartIdx > newEndIdx 條件,說明新節點比對完了,老節點還有多,將這些無用的老節點通過 removeVnodes 批量刪除即可。

深入理解vue2中的VNode和diff算法

總結

Diff算法是一種對比算法。對比兩者是舊虛擬DOM和新虛擬DOM,對比出是哪個虛擬節點更改了,找出這個虛擬節點,并只更新這個虛擬節點所對應的真實節點,而不用更新其他數據沒發生改變的節點,實現精準地更新真實DOM,進而提高效率和性能

精準主要體現在,diff 算法首先就是找到可復用的節點,然后移動到正確的位置。當元素沒有找到的話再來創建新節點。

擴展

vue中為什么需要使用key,它的作用是什么?

keyVuevnode 的唯一標記,通過這個 keydiff 操作可以更準確、更快速。

  1. 更準確:因為帶 key 就不是就地復用了,在 sameNode 函數 a.key === b.key 對比中可以避免就地復用的情況。所以會更加準確。
  2. 更快速:利用 key 的唯一性生成 map 對象來獲取對應節點,比遍歷方式更快。

為什么不推薦使用index作為key

當我們的列表只涉及到 展示,不涉及到排序、刪除、添加的時候使用index作為key是沒什么問題的。因為此時的index在每個元素上是唯一的。

但是如果涉及到排序、刪除、添加的時候就不能再使用index作為key了,因為每個元素key不再唯一了。不唯一的key,對diff算法沒有任何幫助,寫和沒寫是一樣的。

(學習視頻分享:web前端開發、編程基礎視頻)

贊(0)
分享到: 更多 (0)
?
網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
丰满老熟妇好大BBBBB| 国产 国语对白 露脸| 被粗大噗嗤噗嗤进出灌满浓浆| 布丁漫画土豪漫画入口页面| 隔壁邻居是巨爆乳寡妇| 国产欧美日韩精品丝袜高跟鞋| 韩国无码AV片在线观看网站| 久久国产精品无码HDAV | 性XXXXX欧美极品少妇| 亚洲成A人片在线观看无码下载| 亚洲中文久久久久久精品国产| 中文字幕一区二区人妻| 超碰CAO已满18进入离开| 国产精品沙发系列| 久久99精品久久久久婷婷| 免费又黄又爽1000禁片| 日本欧美大码A在线观看| 无码人妻丰满熟妇一区二区三区| 亚洲国产成人久久一区久久| 中文字幕乱码亚洲∧V日本 | 国产在线码观看超清无码视频| 久久精品熟女亚洲AV麻豆网站| 欧美VA久久久噜噜噜久久| 少妇爆乳无码AV无码波霸| 亚洲AV无码之国产精品| 真实国产乱子伦沙发睡午觉 | 国产在线精品一区二区三区直播 | 乱人伦人妻中文字幕不卡| 人禽杂交18禁网站免费| 亚洲 中文字幕 日韩 无码| 永久免费AⅤ无码网站国产| 超碰97人人模人人爽人人喊| 国产婷婷内射精品1区| 老熟女露脸内射正在播放| 色天使综合婷婷国产日韩AV| 亚洲精品一区二区丝袜图片| www高潮无码免费看| 国产一区二区精品丝袜| 男生把感叹号放进女生的括号| 熟妇的奶头又大又粗视频| 亚洲熟妇无码V在线观看| 波多野结衣AV一区二区无码| 好姐妹高清在线韩国电影观看| 欧美黑人巨大精品VIDEOS | 国产麻豆精选AV| 美美女高清毛片视频免费观看| 四十路の五十路熟女豊満AV| 一区适合晚上一个人看B站| 丰满人妻无码使劲张开双腿AV| 久久精品国内一区二区三区| 日韩一区二区视频在线| 亚洲日韩一区二区三区黑人| 成人A级毛片免费观看AV不卡| 极品少妇自慰喷白浆av| 日本AⅤ精品一区二区三区日| 亚洲国产精品成人精品无码区| 把腿张开老子cao烂你n视频| 很黄很黄的曰批视频| 日本无人区码一码二码三码四码| 亚洲精品无码不卡久久久久| 成熟丰满熟妇AV无码区| 久久久久久精品免费无码 | 亚洲午夜性春猛交77777 | 精品久久久无码中文字幕| 日产精品久久久久久久性色| 一面亲上边一面膜下边56| 国产精品毛片Av无码一区二区| 嫩草院一区二区乱码| 亚洲AV无码成人精品网站| M豆传媒有限公司观看| 精品少妇AY一区二区三区| 少妇做爰免费视频网站| 坐着轮流提双腿能起到什么效果| 国产亚洲情侣一区二区无| 人人妻人人澡人人| 永久免费的啪啪免费网址| 国产午夜成人AV在线播放| 日本熟妇人妻ⅩXXXX| 中文字字幕在线中文乱码| 火柴人战争遗产破解版| 熟妇女领导呻吟疯狂| 91人妻人人做人碰人人爽九色| 精品国产黑色丝袜高跟鞋| 天天摸天天做天天爽水多| FREEMOVIES性中国| 老司机带带我免费看| 亚洲不卡AV不卡一区二区| 国产成人啪精品视频免费软件| 欧美日韩精品视频一区二区三区 | 亚洲不卡无码AV中文字幕| 跪趴式啪啪GIF动态图27报| 欧美一性一乱一交一视频C| 一区二区操逼视频| 狠狠色噜噜狠狠狠狠7777米奇 | 亚洲色大成网站久久久| 国产乱人伦AⅤ在线麻豆A| 日韩人妻无码精品专区90618| 97久久久久人妻精品区一| 久久久久无码精品国产H动漫| 亚洲AV无码精品黑人黑人| 国产成人亚洲精品无码青| 日产乱码一二三区别免费一 | 欧美人妻兽交V1DE0S| 曰韩无码AV片免费播放不卡| 精品国产亚洲一区二区三区| 午夜理论电影在线观看亚洲| 国产 在线 | 日韩| 日韩AV片无码一区二区三区不卡| A级无遮挡超级高清-在线观看| 林静公交车被做到高C| 亚洲人成网77777色在线播放| 国产一二三四2021精字窝| 无码中文字幕AV免费放软件| 国产成人无码AⅤ片在线观看导航| 日韩一卡2卡3卡4卡| 超薄肉色丝袜一区二区| 人妻波多野结衣爽到喷水| 99尹人香蕉国产免费天天| 男人吃奶摸下挵进去好爽| 久久综合给久久狠狠97色| 亚洲国产婷婷香蕉久久久久久| 国产小视频A在线观看| 无码无遮挡在线观看免费 | 久久久久亚洲AV无码专| 亚洲欧洲中文日韩久久AV乱码| 精品国产免费第一区二区三区| 亚洲AV秘 无码一区二黑人| 国产无人区卡一卡二卡乱码 | 护士猛少妇色ⅩⅩXXX猛叫| 亚洲AV羞羞无码高潮喷水好爽| 国产最新AV在线播放不卡| 亚洲AV午夜福利精品一区人妖| 国内精自线一二三四2021| 亚洲VA韩国VA欧美VA| 娇妻被交换粗又大又硬视频| 亚洲欧美日本中文字不卡| 久99久无码精品视频免费播放| 亚洲欧洲日产国码AⅤ | JEALOUSVUE成熟| 人善交XUANWEN200| 高潮喷奶水在线播放视频| 无码AV中文一区二区三区桃花岛| 国产精品久久久久9999小说| 亚瑟国产精品久久| 精品国产肉丝袜久久| 亚洲线精品一区二区三区| 久久午夜羞羞影院免费观看| 亚洲综合色在线观看一区二区| 久久综合九色综合欧洲98| 4HUWWW四虎永久免费| 人妻无码ΑV中文字幕琪琪布| 成熟丰满熟妇高潮XXXXX视频| 四虎AV永久在线精品免费观看| 国产日韩一区二区三区在线观看| 亚洲ΑV无码一区二区三区四区| 久久66热人妻偷产精品9| 岳女四人共侍一夫婷婷| 欧美一级137片内射亚洲| 成人亚洲欧美在线观看| 无码精品日韩专区| 精品国产一区二区三区无码蜜桃| 又黑又肥的60岁岳| 强壮公把我一次次弄上高潮| 国产99视频精品免费视频36| 色一情一乱一伦一区二区三区日本| 国产成人无码精品一区二区三区| 亚洲AV日韩综合一区尤物| 久久久久亚洲AV综合仓井空| AV无码久久久久不卡网站下载| 色欲AV综合久久一区二区三区| 国产欧美一区二区三区在线看| 亚洲熟妇成人精品二区蜜臀| 欧美精品亚洲精品日韩专区VA| 国产99久久久国产精品~~牛| 亚洲AV无码专区精品无码 | 中文字幕天天躁日日躁狠狠躁| 人妻少妇久久中文字幕一区二区| 国产精品乱码久久久久久软件| 亚洲熟妇一区二区三区| 欧美日韩在线视频一区| 国产精品沙发午睡系列| 诱女偷伦初尝云雨H| 色鬼7777久久| 精品无码国产自产野外拍在线| AV不卡秒播在线观看| 无码精品A∨在线观看中文| 久久久精品久久久久久96| 少妇搡BBBB搡BBB搡| 欧美丰满熟妇XX猛交| 国产成人精品无码专区| 亚洲熟妇AⅤ无码一区二区| 欧美人与禽2O2O性论交| 国产精品无码电影在线观看| 一出一进一爽一粗一大视频| 日本中文字幕一区二区有码在线 | 免费网站看V片在线18禁无码| 丰满爆乳无码一区二区三区| 亚洲日韩国产精品第一页一区| 人妻免费一区二区三区最新| 狠狠色噜噜狠狠狠狠AV不卡|