国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

Vue3源碼分析-完整update流程和diif算法

共 41778字,需瀏覽 84分鐘

 ·

2021-05-18 07:23

前言

在上一篇文章vue的首次渲染過程中提到在組件的首次渲染階段會有一個副作用渲染函數(shù)setupRenderEffect,在這個函數(shù)內(nèi)會使用響應式API Effect創(chuàng)建副作用函數(shù)componentEffect,這里只需要簡單的理解為,當組件內(nèi)的數(shù)據(jù)改動時這個由Effect包裹的componentEffect就會重新調(diào)用,在這個函數(shù)內(nèi)部會判斷當前組件是處于首次渲染還是更新,當組件內(nèi)數(shù)據(jù)發(fā)生變化時會進入到update的分支,本文要看的diff流程也就是從這里開始。

PS: 當前的Vue版本是3.0.5。本文會忽略TELEPORT、SUSPENSE等特殊vnode類型,對于一些細微的優(yōu)化也會忽略(比如patch函數(shù)的optimized參數(shù))。

回顧setupRenderEffect

在組件的數(shù)據(jù)發(fā)生改變時會自動觸發(fā)副作用渲染函數(shù)setupRenderEffect

// runtime-core/src/renderer.ts
import { effect } from '@vue/reactivity'

const setupRenderEffect = (instance, initialVNode, container, ...) => {
  // 在當前的組件實例上添加 update 方法,通過響應式方法 effect 創(chuàng)建
  instance.update = effect(function componentEffect({
    if (!instance.isMounted) {

      //...
      
    } else {
      // 非首次渲染,進入 update 階段
      let { next, vnode } = instance
      // 緩存一份更新后的 vnode
      let originNext = next
      if (next) {
        // 這里需要把更新后的組件 vnode 賦值 el,因為下面第一次 渲染新的組件 vnode 時并沒有設(shè)置 el
        next.el = vnode.el
        updateComponentPreRender(instance, next)
      } else {
        // 如果沒有 next 直接指向當前 vnode
        next = vnode
      }
      // 渲染新的組件 vnode,因為數(shù)據(jù)發(fā)生了變化
      const nextTree = renderComponentRoot(instance)
      // 緩存舊的組件 vnode
      const preTree = instance.subTree
      // 更新實例上的 subTree 為新的組件 vnode
      instance.subTree = nextTree
      
      patch(prevTree, nextTree, ..., instance)
      // 更新 DOM 元素
      next.el = nextTree.el
    }
  }, prodEffectOptions)

這里主要看update的部分(即else代碼塊)。首先會先從組件實例instance里取出next(在處理組件一節(jié)會詳細說明next存在與不存在的情況)。

組件內(nèi)的數(shù)據(jù)發(fā)生了改變,所以要生成新的組件模板的vnode節(jié)點,在渲染階段命名為subTree,然后還要保存一份舊的subTree,這樣有了新舊subTree后就可以用patch函數(shù)更新DOM。

patch函數(shù)

在進入具體的diff流程之前,我們不妨先想一下,當數(shù)據(jù)發(fā)生改變時,會有哪些變化類型?

實際上按照類型可以分為更新普通DOM元素和更新vue組件這兩種情況。下面先關(guān)注一下patch函數(shù)的邏輯。

// runtime-core/src/renderer.ts
const patch = (n1, n2, container, ...) => {
  // n1 是舊節(jié)點,n2 是新節(jié)點
  // 如果 n1、n2 新舊節(jié)點的類型不同,直接銷毀舊節(jié)點
  if(n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  
  const { type, ref, shapeFlag } = n2
  
  switch(type) {
    // 處理文本節(jié)點
    case Text:
      processText(n1, n2, container, anchor)
      break
    // 處理注釋
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    
    // ...
    
    default
      // 處理 DOM 元素
      if(shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, ...)
      // 處理 vue 組件
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container, ...)
      }
      
      // ...
  }
  
}

最開始先判斷新舊節(jié)點的類型是否一樣,如果不一樣,可以設(shè)想某個節(jié)點從div標簽變成span標簽,最合理的方式是直接舍棄舊節(jié)點,重新掛載新的節(jié)點。

再往下會根據(jù)當前節(jié)點類型type進行特定的處理。比如文本節(jié)點執(zhí)行processText的特定處理、注釋節(jié)點執(zhí)行processCommentNode的特定處理。這樣的前置處理實際上是一個優(yōu)化,在編譯階段,vue會將模版語法編譯成渲染函數(shù),這個時候會把第一個參數(shù)節(jié)點類型type填上,如果這個參數(shù)命中了這樣的特殊節(jié)點,會直接執(zhí)行相應的process方法。

default塊兒里才是分析的重點,即處理普通DOM元素和vue組件。

處理DOM元素

先舉一個栗子??,假設(shè)有以下一段代碼:

<template>
  <div>
    <button @click="add">add</button>
    <p>{{ num }}</p>
  </div>
</template>
<script>
import { ref } from 'vue'
export default {
  setup () {
    const num = ref(1)
    function add() {
      num.value += 1
    }
    return {
      num,
      add
    }
  }
}
</script>

這段代碼非常簡單,點擊add按鈕,p標簽里的num會加一。

因為p標簽是一個普通的DOM節(jié)點,所以在具體執(zhí)行patch方法時,會走處理DOM的邏輯,執(zhí)行processElement方法。

// runtime-core/src/renderer.ts
const processElement= (n1, n2, container, ...) => {
  // 無舊節(jié)點,首次渲染邏輯
  if (n1 === null) {
    // ...
  } else {
    patchElement(n1, n2, ...)
  }
}

我們只關(guān)心新舊節(jié)點都存在的update邏輯,所以接著看patchElement函數(shù)。

前置屬性

在具體看patchElement之前,我們需要先了解兩個前置屬性:

1. PatchFlags

vnode中有patchFlag這樣一個字段,用來表示當前節(jié)點發(fā)生改變的類型。PatchFlags的所有枚舉類型如下所示:

// shared/src/patchFlags.ts
export const enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  DEV_ROOT_FRAGMENT = 1 << 11,
  HOISTED = -1,
  BAIL = -2
}

patchFlag所有的枚舉類型都由二進制來表示,這樣做的好處是很容易對多種類型進行判斷,比如當前變化包括TEXTCLASS。

在判斷時,只需要對想要判斷的類型進行&操作,如果大于0,說明包含此類型。

TEXT & CLASS

2. dynamicChildren

vue3中對靜態(tài)節(jié)點做了標記,在patch階段,不會比較靜態(tài)節(jié)點,只會比較動態(tài)節(jié)點,即dynamicChildren內(nèi)的節(jié)點。

patchElement

了解完上面兩個屬性后我們回歸主線,看一下patchElement函數(shù):

// runtime-core/src/renderer.ts
const patchElement = (n1, n2, ...) => {
  // 緩存舊的DOM節(jié)點,因為這是DOM的更新,所以舊DOM節(jié)點即 n1.el 一定存在
  const el = (n2.el = n1.el!)
  // 取出新節(jié)點的 patchFlag dynamicChildren 后面會進行判斷
  let { patchFlag, dynamicChildren } = n2
  // 保存舊節(jié)點 props
  const oldProps = n1.props || {}
  // 保存新節(jié)點 props
  const newProps = n2.props || {}

  if (patchFlag > 0) {
    // 對所 props 都進行比較更新
    if (patchFlag & PatchFlags.FULL_PROPS) {
      patchProps(el, n2, oldProps, newProps, ...)
    } else {
      // 存在動態(tài) class 屬性時
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class'null, newProps.class, ...)
        }
      }
      // 存在動態(tài) style 屬性時
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, ...)
      }
      
      // 針對除了 style、class 的 props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          if (next !== prev) {
            hostPatchProp(el, key, prev, next, ...)
          }
        }
      }
      // 存在動態(tài) 文本
      if (patchFlag & PatchFlags.TEXT) {
        if (n1.children !== n2.children) {
          hostSetElementText(el, n2.children as string)
        }
      }
    } else if (dynamicChildren == null) {
      patchProps(el, n2, oldProps, newProps, ...)
    }
  }
  
  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren!, dynamicChildren, el, ...)
  } else {
    patchChildren(n1, n2, el, null, ...)
  }
}

對于DOM節(jié)點的更新主要是props子節(jié)點的更新,其中利用patchFlagdynamicChildren做了很多優(yōu)化,不會每次都對props子節(jié)點進行全量的對比更新。

下面這兩張圖對代碼里的一些if else分支做了總結(jié),vue3會充分利用patchFlagdynamicChildren做優(yōu)化,如果確定只是某個局部的變動,比如STYLE改變,那么只會調(diào)用hostPatchProp并傳入對應的參數(shù)STYLE做特定的更新。

更新props

更新子節(jié)點

下面一一看下上圖提到的幾個函數(shù)具體做了什么:

  1. hostPatchProphostPatchProp函數(shù)會根據(jù)參數(shù)的key執(zhí)行相應的方法,比較簡單。
// runtime-dom/src/patchProp.ts
export const patchProp = (el, key, prevValue, nextValue, ...) => {
  switch (key) {
    case 'class':
      // 更新 class
      patchClass(el, nextValue, isSVG)
      break
    case 'style':
      // 更新 style
      patchStyle(el, prevValue, nextValue)
      break
    default:
      // ...
      patchAttr(el, key, nextValue, isSVG)
  }
}

這幾個方法都比較類似,最終都是調(diào)用原生的DOM API更新,其中看下patchClass方法,這個方法比較有意思的一點是,源碼中注釋寫直接對className賦值比使用setAttribute方法要快,不得不說真的細hhhh。

// runtime-dom/src/modules/class.ts
export function patchClass(el, value{
  if (value == null) {
    value = ''
  }
  ...
  // directly setting className should be faster than setAttribute in theory
  el.className = value
}
  1. patchPropspatchProps就如其名了,遍歷所有屬性,全部進行更新。
// runtime-core/src/renderer.ts
const patchProps = (el, vnode, oldProps, newProps, ...) => {
  if (oldProps !== newProps) {
    // 遍歷 newProps 更新
    for (const key in newProps) {
      const next = newProps[key]
      const prev = oldProps[key]
      if (next !== prev) {
        hostPatchProp(el, key, prev, next, ...)
      }
    }
    
    if (oldProps !== EMPTY_OBJ) {
      // 遍歷 oldProps
      for (const key in oldProps) {
        // 如果 存在某個屬性 不在 newProps 調(diào)用 hostPatchProp 移除該屬性
        if (!(key in newProps)) {
          hostPatchProp(el, key, oldProps[key], null,...)
        }
      }
    }
  }
}
  1. patchBlockChildren

DOM是一顆樹,不難想到會有子節(jié)點嵌套的情況,所以這里的子節(jié)點更新是一個深度優(yōu)先遍歷的過程,即更新完當前節(jié)點后,會去更新當前節(jié)點的子節(jié)點,不過vue3在這個過程中有所優(yōu)化。具體會通過對patch的最后一個參數(shù)optimized傳參來防止不必要的渲染。這里簡單知道有這個優(yōu)化即可,全部羅列出來比較繁瑣,感興趣的同學可以詳細看看。

這個方法也比較簡單,因為在編譯的時候已經(jīng)確定了哪些是動態(tài)節(jié)點,所以直接遍歷所有的動態(tài)節(jié)點然后進行patch即可。

// runtime-core/src/renderer.ts
const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, ...) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i]
    const newVNode = newChildren[i]
    patch(oldVNode, newVNode, container, ...)
  }
}
  1. patchChildren

對于子節(jié)點來說,只會有三種可能,分別是:文本節(jié)點、數(shù)組和空。所以這個方法里所有的if else分支就是在考慮新舊節(jié)點可能的全部情況,并進行相應的處理。

// runtime-core/src/renderer.ts
const patchChildren = (n1, n2, container, ...) => {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children
  
  const { patchFlag, shapeFlag } = n2
  
  // ...
  
  // children has 3 possibilities: text, array or no children.
  if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // text children fast path
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 如果舊子節(jié)點是數(shù)組 先卸載
      unmountChildren(c1, parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
      // 更新文本節(jié)點
      hostSetElementText(container, c2)
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 舊子節(jié)點是數(shù)組
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // two arrays, cannot assume anything, do full diff
        patchKeyedChildren(c1, c2, container, ...)
      } else {
        // 沒有新子節(jié)點,直接卸載舊子節(jié)點
        unmountChildren(c1, parentComponent, parentSuspense, true)
      }
    } else {
      // prev children was text OR null
      // new children is array OR null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      // mount new if array
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(c2, container, ...)
      }
    }
  }
}
新舊節(jié)點情況

其中新舊節(jié)點都是數(shù)組的情況涉及到我們平常所說的diff算法,會放到后面專門去解析。

處理vue組件

看完處理DOM元素的情況,接下來看處理vue組件。再舉一個例子??:

<template>
  <div>
    <button @click="add">add</button>
    <foo :num="num" />
  </div>
</template>
<script>
import { ref } from 'vue'
export default {
  setup () {
    const num = ref(1)
    function add() {
      num.value += 1
    }
    return {
      num,
      add
    }
  }
}
</script>
// foo 組件
<template>
  <div>
    <p>num is, {{ num }}</p>
  </div>
</template>
<script>
  export default {
    props: {
      num: Number
    }
  }
</script>

這個例子就是將之前的普通元素標簽換成了foo組件,foo組件接收num props,點擊add按鈕就會加一。

讓我們回到patch方法,當點擊add按鈕時會觸發(fā)重新渲染,其中更新foo時會進入processComponent方法。

// runtime-core/src/renderer.ts
const processComponent = (n1, n2, container, ...) => {
  if (n1 === null) {
    // 首次渲染
    // ...
  } else {
    // 更新
    updateComponent(n1, n2, ...)
  }
}

在更新的時候我們只關(guān)心updateComponent。

// runtime-core/src/renderer.ts
const updateComponent = (n1, n2, ...) => {
  // 緩存最新的 組件 vnode
  const instance = (n2.component = n1.component)!
  // 對比新舊 vnode 節(jié)點,
  if (shouldUpdateComponent(n1, n2)) {
      // ...
      // 把最新組件vnode 賦值給 instance.next
      instance.next = n2
      // in case the child component is also queued, remove it to avoid
      // double updating the same child component in the same flush.
      invalidateJob(instance.update)
      // instance.update is the reactive effect runner.
      instance.update()
  } else {
    // no update needed. just copy over properties
    n2.component = n1.component
    n2.el = n1.el
    instance.vnode = n2
  }
}

updateComponent方法內(nèi)部,先緩存最新的組件實例,接下來有一個優(yōu)化點,會通過shouldUpdateComponent方法來比較新舊組件是否需要更新,這里主要是對比組件vnodeprops、children等屬性。這樣的提前判斷會避免不必要的渲染,如果需要渲染,會把最新的組件vnode賦值給instance.next,這在下面調(diào)用組件首次渲染時注冊的instance.update副作用渲染函數(shù)時會使用到。

至于invalidateJob這個方法,它是從scheduler.ts文件中引出的,所以大概可以知道是處理調(diào)度相關(guān)。再結(jié)合注釋和傳入的參數(shù),就比較明白了。DOM結(jié)構(gòu)是一棵樹,從上面的流程中可以知道,在更新一個節(jié)點時不光會更新節(jié)點本身,還會更新節(jié)點的子節(jié)點,所以,vue會在這里進行檢查,看是否當前組件的副作用函數(shù)已經(jīng)在隊列中了,如果在,直接移除掉,反正再往下也會主動觸發(fā)更新。這樣就避免了二次重復渲染。

如果對比了新舊節(jié)點發(fā)現(xiàn)不需要更新,那很好辦,就不會主動調(diào)用instance.update觸發(fā)更新,僅僅是更新相關(guān)的屬性。

接著執(zhí)行instance.update,這個函數(shù)就是在setupRenderEffect內(nèi)創(chuàng)建的。最終子組件的更新還會走一遍自己副作用渲染函數(shù),然后patch子組件的子模板DOM,接上上面的流程。

update流程小結(jié)

其實到這里可能還是不太清楚整個流程是怎樣的,還是以上面的例子為代表,我們從頭捋一遍點擊add后到底經(jīng)歷了哪些流程。首先我們有一個根組件app,app模板的根DOM元素是div,div里面有button元素和foo組件。appfoo之間通過props: num通信,點擊buttonnum會加一。

當點擊add后,app組件內(nèi)的num更新,由于初次渲染時在組件實例上添加了響應式的update方法。app組件會觸發(fā)自身的update。

// runtime-core/src/renderer.ts
// setupRenderEffect 函數(shù)內(nèi)
instance.update = effect(function componentEffect({
  if (!instance.isMounted) {
    ...
  } else {
    let { next } = instance
    if (next) {
      updateComponentPreRender(instance, next)
    } else {
      next = vnode
    }
    const nextTree = renderComponentRoot(instance)
    const prevTree = instance.subTree
    instance.subTree = nextTree
    patch(prevTree, nextTree, ..., instance)
  }
}, prodEffectOptions)

注意現(xiàn)在是app組件自身的數(shù)據(jù)變化,所以此時是沒有next的,接下來渲染新的子組件vnode,得到真實的模版vnode nextTree,用新舊subTree進行patch。

因為對比的是app內(nèi)模板的普通元素vnode,此時patch的元素類型是div,進入更新普通元素的流程,先更新props,再更新子節(jié)點,當前div下的子節(jié)點有buttonfoo組件,先更新button元素,再更新foo組件。

在更新foo組件時,會先將foo組件instance.next賦值為最新的foo子組件vnode,之后再主動調(diào)用foo.update進入上面的副作用渲染函數(shù),這次的實例是foo組件且next存在值。之后就是同樣的邏輯,進入foo組件的patch,后面就省略掉不細說了。

可以發(fā)現(xiàn),一個組件的更新存在兩種情況。在副作用渲染函數(shù)的內(nèi)部,如果next不存在,是組件本身數(shù)據(jù)發(fā)生變化引發(fā)的update,next存在,是父組件更新子組件的時候引發(fā)的update。

update邏輯

diff算法

在前面分析更新普通元素子節(jié)點時,有一種情況是新舊子節(jié)點都是數(shù)組,這個時候就需要某種成本較低的策略進行diff更新。

先假設(shè)所有元素都擁有一個獨一無二的key值。

我們可以先想一下,新舊子節(jié)點都是數(shù)組會有哪幾種變化情況?

四種原子操作

無論是哪種變化最后都是由更新、添加、刪除、移動這四種操作的一種或者幾種的組合。

源碼里面劃分的比較清晰,主要分為了三種情況:在同一位置添加一個或多個節(jié)點、在同一位置刪除一個或多個節(jié)點和處理未知序列。

新舊數(shù)組序列情況

從上面這三種情況可以看出一個共性,從頭開始的一部分和從尾部倒序的一部分(所有淡黃色的)可能是不需要改變的,不論哪種情況整體都可以分為從頭部正序不需要改動的部分、中間發(fā)生變動的部分、從尾部倒序不需要改動的部分。所以在最開始,可以先進行頭部和尾部的預處理。

源碼里在diff算法的最開始,會先從頭部正序掃描從尾部倒序掃描,以便排除類型一樣的干擾項,進一步的提高效率。

此處的類型一樣指vnode節(jié)點的type、key都一樣。

// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // 從頭部開始掃描的索引
  let i = 0
  // 新節(jié)點長度
  const l2 = c2.length
  // 舊數(shù)組序列尾部索引
  let e1 = c1.length - 1
  // 新數(shù)組序列尾部索引
  let e2 = l2 - 1
  
  // 1. 正序掃描頭部,找到不相同的為止
  while (i <= e1 && i <= e2) {
    // 當前掃描到的舊數(shù)組序列中的節(jié)點
    const n1 = c1[i]
    // 當前掃描到的新數(shù)組序列中的節(jié)點
    const n2 = c2[i]
    // 如果當前的新舊節(jié)點 type、key 相同,才為 true
    if (isSameVNodeType(n1, n2)) {
      // 更新
      patch(n1, n2, ...)
    } else {
      break
    }
    i++
  }
  
  // 2. 倒序掃描尾部,找到不相同的為止
  while (i <= e1 && i<= e2) {
    // 當前掃描到的舊數(shù)組序列中的節(jié)點
    const n1 = c1[e1]
    // 當前掃描到的新數(shù)組序列中的節(jié)點
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, ...)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // ...
}

這段代碼就像上面說到的那樣,先從頭部正序掃描,再從尾部倒序掃描,終止的條件是當前索引不能越界或者遇到新舊數(shù)組序列中的節(jié)點,類型不一樣或者key值不一樣,掃描到相同的節(jié)點會進行patch更新,這里不用操心當前的節(jié)點到底是否需要更新,patch函數(shù)內(nèi)部會做相關(guān)的判斷。

同一位置的添加、刪除

這種情況相對而言比較簡單,因為只涉及到添加或者刪除這兩種單一的原子操作之一,且位置還都是固定。

只需要掃描頭部尾部,找出是在哪個位置進行的添加或刪除,之后再進行相應的操作即可。

添加節(jié)點使用這個例子,從頭部和從尾部掃描完畢之后,各個變量情況如圖所示。

同一位置添加節(jié)點
// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // ...
  
  // 1. 正序掃描頭部,找到不相同的為止
  // 2. 倒序掃描尾部,找到不相同的為止
  // 3. if (同一位置添加節(jié)點)
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? (c2[nextPos].el : parentAnchor
      while (i <= e2) {
        // 第一個參數(shù) 舊節(jié)點 為 null,新的掛載節(jié)點
        patch(null, c2[i], ...)
        i++
      }
    }
  }
  
  // ...
}

如果i > e1 && i <= e2,第一個條件語句表示當前索引已經(jīng)到了舊數(shù)組序列除去尾部相同節(jié)點的末尾,但是還沒到新數(shù)組序列除去尾部相同節(jié)點的末尾,意味著新的數(shù)組序列在舊的數(shù)組序列上新添加了一個或多個的連續(xù)節(jié)點,所以自然而然會命中新添加節(jié)點的情況。只需要對[i, e2]這個新數(shù)組序列內(nèi)的節(jié)點依次進行掛載即可。

當然,如果掃描完畢后情況相反,即當前索引到了新數(shù)組序列除去尾部相同節(jié)點的末尾,但是還沒到舊數(shù)組序列除去尾部相同節(jié)點的末尾,即發(fā)生變化的索引區(qū)間新數(shù)組序列小于舊數(shù)組序列的,這意味著新數(shù)組序列在舊數(shù)組序列的基礎(chǔ)上刪除了一個或多個節(jié)點。只需要對[i, e2]這個舊數(shù)組序列內(nèi)的節(jié)點依次進行卸載即可。

同一位置刪除節(jié)點
// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // ...
  
  // 1. 正序掃描頭部,找到不相同的為止
  // 2. 倒序掃描尾部,找到不相同的為止
  // 3. if (同一位置添加節(jié)點) 沒有命中
  // 4. else if (同一位置刪除節(jié)點)
  else if (i > e2) {
    if (i <= e1) {
      while (i <= e1) {
        unmount(c1[i], ...)
        i++
      }
    }
  }
  // ...
}

未知數(shù)組序列

如果沒有命中上面的兩種情況,那么就需要處理未知數(shù)組序列了??匆幌孪旅孢@個例子。

中間變化區(qū)域是未知序列

先人工分析一下,中間發(fā)生變動的部分經(jīng)過了哪些改變:

  1. 有節(jié)點的移動,d節(jié)點移動到c節(jié)點前,e節(jié)點移動到d節(jié)點前。
  2. 有節(jié)點的添加,添加了h節(jié)點。

按照之前的流程,仍舊是先進行頭部與尾部的預處理掃描,通過i、e1、e2圈出發(fā)生改動的區(qū)間。這個例子不符合添加和刪除的分支,所以進入最后一個elsec處理未知序列的代碼塊     。

// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // ...
  
  // 1. 正序掃描頭部,找到不相同的為止
  // 2. 倒序掃描尾部,找到不相同的為止
  // 3. if (同一位置添加節(jié)點)       沒有命中
  // 4. else if (同一位置刪除節(jié)點)  沒有命中
  // 5. else 處理未知數(shù)組序列
  else {
    // 舊數(shù)組未知序列開始索引
    const s1 = i
    // 新數(shù)組未知序列開始索引
    const s2 = i
    
    // 5.1 創(chuàng)建新數(shù)組未知序列的 key -> 索引 map
    const keyToNewIndexMap = new Map()
    // 因為針對新數(shù)組未知序列創(chuàng)建 map,所以臨界是 e2
    for (i = s2; i <= e2; i++) {
      // 遍歷到的新數(shù)組的節(jié)點
      const nextChild = c2[i]
      // 前面已經(jīng)假設(shè)一定存在 key 值
      if (nextChild.key !== null) {
        // 存儲的 <key, value> 是 節(jié)點 key 值、索引
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }
    // ...
  }
  // ...
}

注釋5開始現(xiàn)在正式進入到處理未知序列的流程中,會根據(jù)新數(shù)組的未知序列建立一個keyToNewIndexMap<key, index>map結(jié)構(gòu)。只需要遍歷新數(shù)組的未知序列即可,得到{ e: 2, d: 3, c: 4, h: 5 }

建立keyToNewIndexMap

遍歷舊數(shù)組序列進行選擇性的更新和移除

下面我們遍歷舊數(shù)組的未知序列,根據(jù)key值且對照著剛剛建立的keyToNewIndexMap,查找舊數(shù)組序列中哪些節(jié)點仍然存在可以patch、哪些節(jié)點不存在需要移除、哪些節(jié)點需要移動。

// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // ...
  
  // 1. 正序掃描頭部,找到不相同的為止
  // 2. 倒序掃描尾部,找到不相同的為止
  // 3. if (同一位置添加節(jié)點)       沒有命中
  // 4. else if (同一位置刪除節(jié)點)  沒有命中
  // 5. else 處理未知數(shù)組序列
  else {
    // 舊數(shù)組未知序列開始索引
    const s1 = i
    // 新數(shù)組未知序列開始索引
    const s2 = i
    
    // 5.1 創(chuàng)建新數(shù)組未知序列的 key -> 索引 map
    // 5.2 遍歷舊數(shù)組未知序列,使用 key 值,根據(jù)keyToNewIndexMap 找出哪些節(jié)點需要patch、移除、移動
    // 新舊數(shù)組中已經(jīng) patch 過的節(jié)點數(shù)
    let patched = 0
    // 所有待處理的節(jié)點,是新數(shù)組未知序列的長度
    const toBePatched = e2 - s2 + 1
    // 是否有節(jié)點需要移動
    let moved = false
    // used to track whether any node has moved 跟蹤是否有節(jié)點移動
    let maxNewIndexSoFar = 0
    // 這個數(shù)組本身的 index 代表新數(shù)組元素的索引,數(shù)組的值代表舊數(shù)組元素的索引
    const newIndexToOldIndexMap = new Array(toBePatched)
    // 初始化數(shù)組值為 0
    for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
    // 遍歷舊數(shù)組未知序列
    for (i = s1; i <= e1; i++) {
      // 當前的 舊節(jié)點
      const prevChild = c1[i]
      // 如果已經(jīng) patch 過的舊節(jié)點數(shù)大于等于 所有新數(shù)組中待處理的節(jié)點
      // 說明所有新數(shù)組中的節(jié)點都已經(jīng) patch 完畢,其余的要移除
      if (patched >= toBePatched) {
        unmount(prevChild, ...)
        continue
      }
      let newIndex
      // 假設(shè) key 一定存在
      if (prevChild.key != null) {
        // 從 keyToNewIndexMap 中獲取當前節(jié)點在新數(shù)組中的索引
        newIndex = keyToNewIndexMap.get(prevChild.key)
      }
      // 如果當前節(jié)點在新數(shù)組中找不到,說明新數(shù)組中沒有,移除該節(jié)點
      if (newIndex === undefined) {
        unmount(prevChild, ...)
      } else {
        // 更新數(shù)組,因為默認值是 0,i 有可能是 0,+ 1 避免和默認值沖突
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        // maxNewIndexSoFar 初始值是0
        // 每次maxNewIndexSoFar賦值的是當前節(jié)點在 新數(shù)組中的索引
        // 如果新數(shù)組的順序和舊數(shù)組一樣,那么就是遞增的
        // false 說明順序發(fā)生改變
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex
        } else {
          moved = true
        }
        // patch 新舊節(jié)點中匹配的節(jié)點
        patch(prevChild. c2[newIndex], ...)
        // 當前 patch 過的節(jié)點數(shù) +1
        pached++
      }
    }
  }
  // ...
}

因為現(xiàn)在的DOM還是由舊數(shù)組生成的,我們需要知道當前的這些舊DOM節(jié)點是需要更新還是刪除還是移動。所以我們要去遍歷舊數(shù)組的未知序列,并結(jié)合剛生成的keyToNewIndexMap與新數(shù)組的未知序列進行對比。

先定義了幾個變量。patched表示當前已經(jīng)更新的節(jié)點數(shù),toBePatched表示當前待更新的全部節(jié)點,通過這個描述我們也就知道了,如果patched大于等于toBePatched,那么剩下的舊節(jié)點就是要全部舍棄的。

moved用來表示當前是否有移動的節(jié)點。

maxNewIndexSoFar用來判斷是否有節(jié)點移動。

newIndexToOldIndexMap這個數(shù)組本身的index代表當前節(jié)點在新數(shù)組序列中的索引,實際的值代表當前節(jié)點舊子序列索引。默認值全部是0。

接下來開始正式遍歷舊數(shù)組,先取出舊數(shù)組序列里的節(jié)點c1[i],然后判斷patched是否大于等于toBePatched,如果是,卸載當前的舊節(jié)點,跳出本次循環(huán)。

如果當前更新的節(jié)點數(shù)沒有大于等于所有待更新的節(jié)點數(shù),那么開始更新keyToNewIndexMap這個數(shù)組,只需要通過key值從newIndexToOldIndexMap內(nèi)取出相應的newIndex索引即可。

如果找不到索引,說明在新的數(shù)組序列中這個節(jié)點不存在,直接刪除此節(jié)點。

如果找到了這個索引,那么更新newIndexToOldIndexMap[newIndex - s2] = i + 1,這里為什么要i+1呢?因為這個數(shù)組的默認值設(shè)置為了0,如果當前的i就是0,會引起沖突。

再往下執(zhí)行,因為maxNewIndexSoFar 初始值是0,每次maxNewIndexSoFar賦值的是當前節(jié)點在新數(shù)組中的索引,如果新數(shù)組的順序和舊數(shù)組一樣,那么每次的maxNewIndexSoFar不可能大于newIndex。如果大于了,說明有節(jié)點發(fā)生了移動,需要將moved設(shè)置為true。

最后,沒有經(jīng)過前面的刪除,證明當前的這個節(jié)點在新舊節(jié)點中都是存在的,那么直接patch(prevChild, c2[newIndex])即可。最后別忘記把記錄已更新節(jié)點數(shù)變量patched加一。

遍歷舊數(shù)組后結(jié)果

移動和掛載新節(jié)點

通過moved變量,我們已經(jīng)知道了當前有節(jié)點移動,下面需要處理的就是移動和掛載新節(jié)點。

vue3移動節(jié)點采取的策略是先得到最長遞增子序列的索引newIndexToOldIndexMap。舉個列子:

  • [2, 3, 1, 0]的最長遞增子序列是[2, 3],最終需要的索引是[0, 1]

關(guān)于如何求解最長遞增子序列,推薦看leetcode300最長遞增子序列題解,在這里就不贅述具體算法實現(xiàn)了,主要目標仍然是在整體流程。

// runtime-core/src/renderer.ts
const patchKeyedChildren = (c1, c2, ...) => {
  // ...
  
  // 1. 正序掃描頭部,找到不相同的為止
  // 2. 倒序掃描尾部,找到不相同的為止
  // 3. if (同一位置添加節(jié)點)       沒有命中
  // 4. else if (同一位置刪除節(jié)點)  沒有命中
  // 5. else 處理未知數(shù)組序列
  else {
    // 5.1 創(chuàng)建新數(shù)組未知序列的 key -> 索引 map
    // 5.2 遍歷舊數(shù)組未知序列,使用 key 值,根據(jù)keyToNewIndexMap 找出哪些節(jié)點需要patch、移除、移動
    // 5.3 移動和掛載新節(jié)點
    // 如果有節(jié)點移動,得到 newIndexToOldIndexMap 的最長遞增子序列的索引
    const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
    // 用于節(jié)點移動判斷
    let j = increasingNewIndexSequence.length - 1
    // 倒序新數(shù)組的未知序列,因為插入節(jié)點時使用 insertBefore 即向前插,倒序遍歷可以使用上一個更新的節(jié)點作為錨點
    for (i = toBePatched - 1; i >= 0; i--) {
      // 當前在整個新數(shù)組中,未知序列的索引,s2 是頭部相同節(jié)點的偏移量
      const nextIndex = s2 + i
      // 當前在整個新數(shù)組中,未知序列的節(jié)點
      const nextChild = c2[nextIndex]
      // 當前節(jié)點的下一個節(jié)點,如果當前節(jié)點是最后一個節(jié)點,那么取整個父節(jié)點的下一個節(jié)點作為插入點
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1] : parentAnchor
      // 如果仍然是默認值 0 證明是一個全新的節(jié)點
      if (newIndexToOldIndexMap[i] === 0) {
        // 掛載新的節(jié)點
        patch(null, nextChild, container, anchor, ...)
      // 存在節(jié)點移動
      } else if (moved) {
        // 當前索引不是最長遞增子序列里的值,需要移動
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor)
        // 當前索引是最長遞增子序列里的值,j 指向下一個
        } else {
          j--
        }
      }
    }
  }
  // ...
}

在得到最長遞增子序列索引后,設(shè)置一個變量j,它的初始值是最長遞增子序列索引的length - 1,即指向其末尾,主要是用來判斷節(jié)點是否需要移動。

下面會倒序遍歷新數(shù)組的未知序列,因為無論是在patch中還是下面移動節(jié)點的move方法,其插入節(jié)點的操作都是使用insertBefore向前插入。在每一次倒序遍歷的時候,如果需要的話我們可以很輕松的選取上一次已經(jīng)處理完畢的節(jié)點作為基準,把當前節(jié)點,插入到它的前面。

進入到每一輪的遍歷,其實會出現(xiàn)三種情況:

  1. 使用newIndexToOldIndexMap用當前的新數(shù)組索引查找舊數(shù)組索引,發(fā)現(xiàn)是初始值0,表示舊數(shù)組中沒有這個節(jié)點,那么使用patch方法掛載一個新的節(jié)點即可。

  2. 當前的索引不在最長遞增子序列中(j < 0會越界,所以提前可以確定不存在),說明當前節(jié)點需要移動,那么調(diào)用move(nextChild, container, anchor)即可。

  3. 當前的索引恰好是最長遞增子序列的值,說明該節(jié)點不需要移動,維護j變量。

到這兒,完成了對于未知序列的更新就完成了,下面看一下當前這個例子的具體執(zhí)行過程。

具體執(zhí)行過程

沒有key值的情況

上面的流程一直在假設(shè)每一個節(jié)點都有一個獨一無二的key值,如果我們不寫key值會怎樣呢?

因為一般數(shù)組渲染都會使用v-for,所以在這里這個沒有key值的情況指所有的新舊數(shù)組節(jié)點都沒有key,而非有的節(jié)點存在key,有的節(jié)點不存在key

如果沒有寫key值,在patchChildren函數(shù)內(nèi),會根據(jù)patchFlag進入patchUnkeyedChildren這個函數(shù)內(nèi)。

// runtime-core/src/renderer.ts
const patchChildren = (n1, n2, ...) => {
  // ...
  
  if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
    patchKeyedChildren(...)
    return
  } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
    // 無 key 值的情況
    patchUnkeyedChildren(...)
    return
  }
  
  // ...
}

其實對于不寫key值的diff處理非常的簡單粗暴,會先取新舊數(shù)組長度較小的作為公共長度,然后以這個較小的長度挨個進行遍歷并對新舊數(shù)組的節(jié)點patch。

之后判斷:

  • 如果新數(shù)組的長度大于舊數(shù)組,說明有新增的節(jié)點,那么只需要接著掛載即可。
  • 如果新數(shù)組的長度小于舊數(shù)組,說明有刪除的節(jié)點,那么只需要從尾部開始刪除即可。
// runtime-core/src/renderer.ts
const patchUnkeyedChildren = (c1, c2, ...) => {
  c1 = c1 || EMPTY_ARR
  c2 = c2 || EMPTY_ARR
  const oldLength = c1.length
  const newLength = c2.length
  // 選取較小長度作為公共部分
  const commonLength = Math.min(oldLength, newLength)
  // 依次 patch 
  for (let i = 0; i < commonLength; i++) {
    const nextChild = c2[i]
    patch(c1[i], nextChild, ...)
  }
  if (oldLength > newLength) {
    // remove old
    unmountChildren(...)
  } else {
    // mount new
    mountChildren(...)
  }
}

舉個例子??:

沒有key值diff

我們可以很明顯的看出這種情況的不足:沒有利用任何一個舊節(jié)點,全部進行無腦的patch更新。

最后

到此,你已經(jīng)看完了vue3更新時的整個流程和完整的diff算法~如果有收獲的話,點個贊支持一下吧~

參考資料

  • https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/renderer.ts

  • https://juejin.cn/post/6919376064833667080

  1. 幾張動圖帶你回顧JS的變量提升
  2. 幾張動圖帶你回顧event loop
  3. 看了就會的瀏覽器幀原理
瀏覽 84
點贊
評論
收藏
分享

手機掃一掃分享

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

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 怡春院中文字幕| 国产性爱自拍视频| 精品无人区无码乱码毛片国产| 国产亚洲99久久精品| 欧美操逼视频| 99在线精品视频| 青娱乐超碰在线| a网站在线观看| 天天日天天添| 国产一级婬乱片AV片AAA毛片| 影音先锋久久久| 亚洲乱伦图| 青青草原免费在线视频| 2024无码| 久久思热国产| 日韩AV无码专区亚洲AV紧身裤| 一品国精和二品国精的文化意义 | 亚洲一区视频在线| 亚洲网站免费在线观看| 亚洲综合伊人无码| 亚洲无码三级片在线观看| 美日韩视频欧美一区二区视频 | PORNY九色视频9l自拍| 欧美成人免费精品| 香蕉av在线| 国内综合久久| 在线观看黄a| 欧美一在线一综合| 日本欧美在线观看高清| 欧美无人区码suv| 国产人人操| 国产视频中文字幕| 亚洲中文字幕在线免费观看视频 | 人人妻人人操人人| 人人爽夜夜爽| 男人天堂社区| 一本色道久久综合无码人妻| 黄色AV免费看| 欧美色999| 成人做爰100部免费网站| 国产91精品探花一区二区| 男人午夜天堂| 无码看片| 丰满人妻精品一区二区在线| 91在线无码精品秘入口三人| 天天做夜夜操| 91麻豆精品91久久久ios版| 色综合天天综合| 特级西西444www精品视频| 日本无码成人| 高清无码视频18| 一本色道无码道| 蜜桃视频在线观看视频| 天天视频国产| 国产香蕉精品视频| 精品国产久| 国产口爆在线观看| 日韩啪啪网站| 操日本逼| 人人操在线观看| 亚洲AV男人天堂| 91人妻精| 欧美一级内射| 欧美日韩激情| 精品人妻一区二区三区在| 人人妻人人澡| 一级黄色蜜芽视频| 日本黄色影视| 欧美一区三区视频z| 天天综合字幕一区二区| 成人视频高清无码| 狠狠色噜噜狠狠狠7777米奇网| 四川少妇搡BBw搡BBBB搡| 四色五月婷婷| 亚洲av资源在线观看| 18禁网站在线播放| 亚洲AA视频| 日皮网站在线观看| 日韩无码免费电影| 97国产高清| 欧美男人的天堂| 永久免费av| 91小视频在线观看| 91AV在线看| 超碰在线中文字幕| 国产午夜激情| 欧美精品午夜福利无码| 做爱网站在线观看| 欧美老妇大BBBBXXXX| 一级成人电影| 午夜偷拍视频| 久久久免费观看视频| 国产成人午夜精品无码区久久麻豆| 一区二区三区久久久久〖网:.〗 | 欧美激情色色| 99精品视频北条麻妃国产版 | 成人AAA| 成人精品在线观看| 美女一级变态毛片| 无码免费中文字幕| 国产日韩欧美成人| 无码视频中文字幕| 农村三级片| 日韩在线视频中文字幕码无| 男人亚洲天堂| 最新无码视频| 亚洲在线免费| 青榴视频免费观看| 又大又长又粗91| 在线免费看A| 日韩群交视频| 粗长哭叫打桩H体育生| 国产丝袜无码| 无码91| 九九国产视频| 一区二区三区四区无码| 日本黄A级A片国产免费| 国产伦精品一区二区三区色大师 | 超碰天天操| 久久久成人视频| 欧美一级操逼| 国产大奶一区二区| 日韩无码不卡电影| 高清无码成人视频| 成人做爰黄A片免费看| 天天操天天射天天日| 国产在线拍偷自揄拍无码一区二区| 日韩人妻在线观看| 欧美在线a| 午夜成人三级| 蜜桃黄色视频| 91麻花| 天天爱天天爽| 亚洲AV激情无码专区在线播放| 江苏妇搡BBBB搡BBB| 亚洲天媒在线播放| 俺也色俺也干| 4438成人网站| 18AV在线观看| 欧美中文字幕在线视频| 水蜜桃在线视频| 亚欧成人在线视频| 福利在线看| 波多野结衣无码在线视频| 黄片天堂| 欧美精品秘一区二区三区蜜臀| 三级网站在线| 欧美精产国品一| 天堂v视频| 日产电影一区二区三区| 夜夜骑夜夜| 久久综合九九| 欧美日韩一区二区三区在线电影| 十八无码成人免费网站| 亚洲黄色无码视频| 无套内射免费视频| 亚洲无码av中文字幕| 日韩爱爱爱| 黄色一级片免费看| 蜜桃视频一区二区三区| 亚洲成人福利电影| 天天看天天爽| 哪里能看毛片| 久草这里只有精品| 欧美性爱香蕉视频| 扒开让我91看片在线看| av资源在线看| 最新中文字幕在线视频| 亚洲激情在线| 国产狼友| 日韩一区二区不卡| 狠狠撸狠狠操| 中文字字幕在线中文乱码更新时间| 日本少妇久久| 国产成人精品无码片子的价格 | 777.av| 在线视频亚洲| 亚洲AV无码成人精品区天堂小说| 日本一区二区三区四区在线观看| 91羞羞| 国产无码Av| 亚洲免费高清| 四川揉BBB搡BBB| 亚洲色图网站| 四季AV综合网站| 无码超碰| 欧洲精品在线视频| 欧美人操逼视频| 无码人妻一区二区三区免费n鬼沢 北京熟妇槡BBBB槡BBBB | 亚洲小视频在线| 黄色毛片网站| 日韩18在线| 日韩中文字幕精品| 成人午夜无码福利视频| 亚洲精品视频在线观看网站| 天天操夜夜操狠狠| 亚洲黄色一区| 欧美黄色电影网站| AV无码资源| 国内精品久久久久久久久98| 国产乱子伦视频国产印度| 婷婷五月丁香五月| 日韩精品人妻| 日韩成年视频| 亚洲一区在线视频| 岛国av无码免费| 一级黄色录像视频| 粉粉嫩嫩的18虎白女| 免费中文资源在线观看| www.俺去啦| 国产AV无码成人精品区| 欧美操逼逼| 国产精品99久久免费黑人人妻| 国产麻豆精品成人免费视频| 玩弄人妻少妇500系列视频| 亚洲人气无码AV| 亚洲天堂视频网站| 四季AV一区二区夜夜嗨| 三级视频在线观看| 中文字幕丰满的翔田千里| 亚洲区成人777777精品| 亚洲成人免费视频| 99视频+国产日韩欧美| 欧美成人精品欧美一级| 黄色在线观看免费| 亚洲中文AV在线| 色婷婷一区二区三区久久午夜 | 五月天乱伦小说| 国产精品内射婷婷一级二| 无码A级| 丰满人妻一区二区三区视频54| 欧美男人天堂| AV在线播放中文字幕| 黄色激情五月| 亚洲精品午夜福利| 夜夜撸夜夜操| 干老女人视频| 亚洲有码中文字幕| 欧美一级三级| 91丨PORNY丨丰满人妻网站| 一区在线观看| 噜噜色小说| 日本欧美视频| 97男人的天堂| 狠狠色噜噜狠狠狠7777| 中文字幕在线亚洲| 蜜桃av久久久亚洲精品| 欧美激情影院| 天天拍天天干| AV电影一区| 日韩欧美一| 兔子先生和優奈玩游戲脫衣服,運氣報表優奈輸到脫精光 | 欧美在线成人视频| 亚洲欧美视频| 1000部毛片A片免费视频| 视色av| 亚洲无码激情在线| 仓井空一区二区| 国产黄色影院| 国产又粗又长又硬又大毛苴茸图片| 五月婷婷综合在线| 欧美视频一区| 亚洲狼人综合网| 波多野结衣亚洲| 蜜柚Av| 99久久婷婷国产综合精品hsex | 精品视频日韩| 亚洲精品内射| 加勒比久久88| 能看的AV网站| 亚洲第一网无码性色| 日韩成人av在线| 免费看一级一级人妻片| 嫩BBB槡BBBB槡BBBB| 亚洲欧洲免费| 天堂素人| BBB搡BBB搡BBB搡BBB| 日韩黄色电影网站| www.91在线看| 欧美三级欧美一级| 大香蕉伊人青青草| 东京热在线视频观看| 亚洲AVwww| 摸BBB搡BBB搡BBBB| 亚洲精品99| 在线播放高清无码| 婷婷伊人綜合中文字幕| 91麻豆国产在线| 在线观看免费A片| 五月影院| 在线日韩AV| 日本东京热视频| 99精品视频国产| 婷婷五月中文字幕| 无码蜜桃一区二区| 老司机无码视频| 国产亚洲欧美日韩高清| 免费高潮视频| 一道本视频在线免费观看| 色中色在线视频| 97男人的天堂| 成人电影久久久| 人人看人人摸| 免费无码一级A片大黄在线观看| 欧美成人精品一区二区| 欧美色色视频| 99热这里只有精品9| 国产乱码精品一品二品| 亚洲乱淫| 亚洲不卡一区二区三区| 国产精品性爱视频| 狠狠做深爱婷婷久久综合一区| 熟女视频网站| 影音先锋91视频| 91精品国产综合久久蜜臀使用方法 | 大香蕉伊人av| 伊人五月丁香| 一区二区三区在线免费观看| 黄色搞逼视频| 日本欧美亚洲| 亚洲日韩在线视频播放| 亚洲无码一区二区在线观看| 全部在线A片免费播放| 色五月婷婷综合| 久热在线资源福利站| 秋霞国产| 国产激情精品视频| 国产成人无码免费看片| 天天日天天干天天爽| 无码人妻一区二区三区免费n鬼沢 北京熟妇槡BBBB槡BBBB | 欧美爱爱免费看| 国产欧美日韩综合| 日本欧美在线观看高清| www,色婷婷| 波多野结衣亚洲| 国产乱伦影片| 超碰在线观看免费| 国精品无码人妻一区二区三区免费| 91久久久久久久91| 欧美日韩一级电影| 亚洲天堂女| 环亚无码| 综合色国产精品欧美在线| 中文字幕高清| 国产精品视频免费观看| 亚洲欧美成人在线视频| 欧美综合网| 99热免费精品| 亚洲成人影片| 亚洲AV无码精品国产| 国产综合自拍| 国产农村妇女精品一二区| 日韩在线免费视频| 亚洲一级免费在线观看| 色视频在线观看免费| 伊人成人大香蕉| 欧美一区三区视频z| 熟女视频91| 俺来也影院| 色婷婷综合激情| 婷婷五月天丁香成人社区| 国产综合久久久7777777 | 韩国高清无码视频| 在线三级av| 亚洲高清无码专区| 人成在线观看| 综合久久久| 国语操逼| 免费视频一区| 亚洲天堂男人的天堂| 色婷婷激情五月天| 欧美Aⅴ| av天天看| 成人免费区一区二区三区| 欧美自拍视频在线| 无码人妻一区二区三区免费n鬼沢 北京熟妇槡BBBB槡BBBB | 亚洲在线无码| 久久久久9999| 五月婷婷婷婷| 苍井空无码一区二区三区| 国精产品久拍自产在线网站| 怡红院爽妇网| 亚洲AV激情无码专区在线播放| 日韩在线免费看| 东京热男人的天堂| 1区2区视频| 91网站18| 91国语又粗又大对白| 日韩精品成人AV| 无码人妻一区二区三区免费n鬼沢 北京熟妇槡BBBB槡BBBB | 丝袜美女足交| 亚洲无码不卡视频| 午夜一区二区三区免费| 成人国产精品| 日本亲子乱婬一级A片| 亚洲中文字幕av| 69国产在线| 操综合| 免费成人黄色网址| 色欲AV秘无码一区二区三区| 99久久综合| 3d动漫精品一区二区三区在线观看| 国产嫩草久久久一二三久久免费观看 | 天天操天天干欧美精品| 色色大香蕉| 国产嫩草影院| 欧美一页| 国产亚洲色婷婷久久99精品| 亚洲最大三级片| 91日日夜夜| 少妇搡BBBB搡BBB搡18禁| 日本久久视频| 综合欧美国产视频二区| 一区二区三区色| 国产午夜福利视频| 七十路の高齢熟女千代子下载| 久久嫩草精品久久久久精| 蜜臀久久久99久久久久久久| 亚洲蜜桃av一区| 69精品无码成人久久久久久| 日韩无码流出| 天a堂8在线www| 一级A片亲子乱中文| 亚洲成人人妻| 天天操中文字幕| 刘玥一区二区| 啪啪人妻| 人成在线免费视频| 狠狠躁日日躁夜夜躁A片男男视频| 日韩欧美不卡色不卡| 日本欧美在线观看| www.97cao| 尤物在线播放| 久久免费国产视频| 精品无人区无码乱码毛片国产| 色婷婷激情AV| 天天看天天摸| 欧洲AV片| 久热中文| 自拍视频在线观看| 人妻熟女视频| 日韩一级片视频| 婷婷激情视频| 中文字幕在线欧美| 西西444www| 五月天婷婷丁香网| 91美女操逼视频| 无码福利导航| 婷婷开心五月天| 啊啊啊国产| 黄色一级爱爱| 日韩人妻在线观看| 丁香五月激情在线| 黄色性视频| 日韩电影无码| 中文无码熟妇人妻| 日本一区二区视频在线观看 | 欧美黄色网视频| 亚洲天堂无码在线观看| 精品中文字幕在线| 丝袜二区| 日本三级视频| 日本国产在线观看| 一本久道无码| 日本色色网站免费| 亚洲精品日韩无码| 熟女少妇视频| 大香蕉啪啪啪啪| 中文字幕A片无码免费看| 亚洲色综合久久五月| 在线观看AV资源| 亚洲小说图片AV在线| 欧美一级黃色A片免费看小优视频 无码人妻精品一区二区三千菊电影 | 国产白丝在线观看| 41ts午夜福利| 亚洲成人无码高清| 亚州无码| 日韩成人一区二区三区| 成人免费一区| 亚洲99热| 夜夜骚AV一二三区无码| 大伊人久久| 爆操网站| 无套内射免费视频| 亚洲无码偷拍| 激情视频在线免费观看| 91久久午夜无码鲁丝片久久人妻| 男人天堂无码av| 最新一区二区三区| 北条麻妃人妻中文字幕91影视| 日韩无码福利| 成人AV免费观看| 38t6电影网logo高清图片| 南京搡BBBB搡BBBB| 欧美性爱福利视频| 久久综合17p| 亚洲国产视频在线观看| 91国产爽黄在线相亲| 91视频免费播放| 成人在线网址| 日韩精品毛片一区二区视频免费| 水蜜桃一区二区| 国精产品一二四区黑人| 国产精品美女久久久久AV爽| 黄色一级片免费在线观看| 国产福利视频在线观看| 欧美第一区| a√天堂中文在线8| 欧美日韩视频一区二区| 撸撸视频| 久久丝袜| 亚洲AV无码成人精品区大猫| 91无码人妻精品1国产四虎| 麻豆精品一区二区三区| 乱伦专区| 人人妻人人操人人| 夜夜爽妓女77777毛片A片| 亚洲成人高清在线| 国产香蕉视频免费| 日韩欧美不卡| 亭亭五月丁香| 91肏屄视频| 激情小说在线视频| 91爱看| 一级片免费在线观看| 成人爽a毛片一区二区免费| 特黄av| 波多野结衣无码一区| 国产无遮挡又黄又爽在线观看| 日韩人妻中文字幕| 99热都是精品| 免费视频久久| 精品欧美成人片在线| 色狠狠AV| 迷奸91| 91在线精品一区二区| 青娱乐伊人| 大伊香蕉在线| 99热999| 日本成人激情视频| 日韩A片| 69黄色视频| AV无码在线播放| 亚洲色色色| 干老女人逼| 国产精品无码毛片| H片在线观看| 亚洲免费视频在线| 国产精品18禁| 免费在线黄色视频| 激情丁香五月婷婷| 大香蕉色婷婷| 久久99草| 一级片在线| 91精品久久香蕉国产线看观看| 亚洲成人观看| 69式荫蒂被添全过程| 中文字幕精品视频在线| 七区九区一区在线| 少妇无码一区| 日韩中文字幕在线视频| 成人三级av| 超碰三级| 黄色国产免费| 亚洲视频,中文字幕| 波多野结衣无码一区| 国外操逼视频| 9991区二区三区四区| 亚洲成人无码在线观看| 蜜桃av秘无码一区二区| 丁香五月影院| 久久久91精品国产一区苍井空| 久久久xxx| 少妇做爱特级AAA| 欧美日韩岛国| 亚洲精品色色| 亚洲免费清高| 精品无码视频| 麻豆91在线| 91视频18| 日韩AV在线免费| 看黄片com| 色视频国产| 亚洲欧美在线视频观看| 色国产视频| 怕怕怕视频| 水果派成人播放无码| 亚洲综合伊人| 久久婷婷视频| 亚洲91在线| 成人在线小视频| 国产亚洲一区二区三区| 三个黑人猛躁我一晚上| 亚洲.无码.制服.日韩.中文字幕| 99国产免费视频| 欧美爱爱网站| 嗯啊在线视频| 免费看黄片| 北条麻妃无码在线视频| 影音先锋av在线资源站| 狼友在线视频| 无码免费看| 熟女人妻人蜜桃视频| 国产乱国产乱300精品| 欧美成人第一页| 色婷婷一区二区三区久久午夜 | 色老板在线视频| 吴梦梦《女教师时间暂停》| 四虎永久在线精品无码| 九九热只有精品| www.天天日| 久久久网| 中文AV在线播放| 香蕉网址| 欧美日韩在线视频播放| 内射视频免费观看| 高潮AV在线观看| 一级片免费网站| 成人午夜福利高清视频| 91无码人妻一区二区成人aⅴ| 国产成人AV一区二区三区在线观看| 激情小视频在线观看| 日日摸日日添日日躁AV| 丁香视频在线观看| 青青草社区视频| 久久婷婷精品| 六月婷婷五月天| 丝袜东京热AV高清| 国产18女人水真多免费看| 一级操逼视频免费观看| 午夜无码鲁丝午夜免费| 欧美日本成人网站入口| 翔田千里被操120分钟| 色情一级A片成人片| 欧美日韩成人| 欧美自拍视频| 黄片视频免费看| 日韩二区| 日韩人妻精品无码| 国产精品免费在线| 91久久国产综合久久91| 日韩av在线免费观看| 午夜av在线| 日韩欧美不卡| 男女拍拍| 婷婷五月天激情网| 91探花秘在线播放| 中文字幕日韩精品人妻| 娇小,学生,高潮,videos| 亚洲色婷婷| 日本在线播放| 久久三级片| 日本高清色清di免费观看| 小日本91在线观看| 国产性爱精品影片免费看| 懂色AV| 木下凛凛子AV888AV在线观看 | 欧美中文字幕| 国产探花视频在线免费观看 | 影音先锋91| 大地资源第5页在线| 妞干网国产| 国产18水真多18精品| 人人摸在线视频| 国产免费av网站| 黄色国产网站| 自拍超碰在线| 夜色视频网| 琪琪av| 日韩熟妇无码中文字慕| 91农村站街老熟女露脸| av资源免费| 91蝌蚪网| 欧美黑吊大战白妞欧美大片| 爱搞在线观看| 无码专区一区二区三区| 午夜激情在线观看| 欧美91| 欧美日韩免费在线播放电影在线播放电影在线播放电影免费 | 国产免费自拍视频| 99九九久久| 欧美精品一二三| 操逼一区二区| 欧美精产国品一二三| 波多野结衣av在线| 日本处女性高潮喷水视频| www.爆操| 人妻少妇中文字幕久久牛牛| 国产一级A片免费看| 操逼视频下载| 秋霞丝鲁片一区二区三区手机在绒免| 天天看天天爽| 色五月婷婷久久| 豆花天天吃最新视频| 亚洲区成人777777精品| 特逼视频| 中文在线字幕电视剧免费平台| 免费无码视频在线观看| 一级无码毛片| 操杨幂| 欧美aaa| 加勒比久久88| 国产乱叫456在线| 一级黄色性爱视频| 我和岳m愉情XXXⅩ视频| 欧洲成人在线观看| 国产色吧| 人人澡人人看| 午夜激情网站| 男女操逼免费观看| 久久久女女女女999久久| 港澳日韩黄片| 91欧美精品成人综合在线观看 | 人人妻人人操人人爱| 日韩最新高清无码| 在线观看日韩精品| 99精品免费观看| 久久免费在线视频| 婷婷色在线播放| 婷婷精品国产a久久综合| 欧美黄色电影网站| 初学影院WWWBD英语完整版在线观看 | 久久中文字幕电影| 久操手机在线| 三级无码高清| 国产噜噜噜噜久久久久久久久| 蜜臀99久久精品久久久懂爱| 操骚屄视频| 色99999| 国产福利在线视频| 青娱乐成人网| 亚洲乱码在线| 久久人妻精品| 玖玖资源站中文字幕| 香蕉三级片| 中文视频在线观看| 日韩人妻无码精品| 日本一级理论片在线大全| 操B视频在线播放| 日韩欧美小电影| 欧美一卡| 91久久国产综合久| 操逼网页| 999日本不卡影院| 91香蕉国产成人App| 99精品99| 欧美日韩亚洲综合| jjzz国产| 熟妇人妻中文字幕无码老熟妇| 国产福利在线视频| 四虎在线观看视频| 亚洲国产一| 亚洲精品美女| 欧美色色网站| 激情综合五月天| 91爱爱| 成人黄网站免费观看| 夜间福利视频| 国产TS变态重口人妖| 337p大胆色噜噜噜噜噜| 青青国产| 国家一级A片| 躁BBB躁BBB躁BBBBBB日视频 | 中文字幕AV第一页| 桃色五月天| 日本精品二区| 黄片网站在线观看| 亚洲欧美日韩在线| 欧美插菊花综合网| 欧美激情另类| 国产精品久久视频| 国产欧美综合视频一区二区在线| 特级西西444www高清大胆免费看 | 黄色片免费视频网站| 久久久老熟女一区二区三区91| 无码做爱视频| 凹凸熟女凹凸BBWBBW| 18禁网站在线| 中文字幕中文字幕一区| 五月天狠狠| 亚洲精品一二| 国产高清免费| 成人做爰黄A片免费看三区蜜臀| 久久黄色A片| 欧美亚洲视频在线观看| 国产成人无码AⅤ片免费播放| 人人射人人操| 在线观看日韩三级片av| 欧美男女交配视频| 无码入口| 精品人妻二区中文字幕| 精品人妻一区二区三区含羞草| 欧美aaaaaa| 一区二区三区四区av| 人人澡人人爽| 伊人久操| 77q视频| 亚洲五月丁香婷婷| 在线精品福利| 伊人网成人| 久久综合久久鬼色| 亚洲精品国产精品国自产网站| aaa久久| 成人伊人| 日韩在线观看视频免费| 久久国产亚洲| 狼友精品| 五月天激情视频| 婷婷在线观看免费| 坏男人内射老太太| 日韩中文欧美| 日韩字幕久久| 欧美色图色就是色| 97超级碰| 91精品综合| 99青青草| 在线性视频| 亚洲精品人妻在线| 99精品丰满人妻无码一区二区| 高清毛片AAAAAAAAA片| 美女天天日| 中文字幕黄色电影| 亚洲男人天堂| 乱伦一区二区三区| 久久久精品免费视频| 国产乱伦免费| 日韩中文字幕永久| 伊人99re| 九九热热| 成人免费区一区二区三区| 日韩一级片在线| 岛国免费AV| 色墦五月丁香| 激情五月天av| 天堂资源中文在线| 国产亚洲色婷婷| 免费观看黄色成人网站| a√免费看| 91狠狠| 亚洲AV第一页| 女生自慰网站免费| 国产成人一级片| 迷奸91| 精品视频免费在线观看| 免费看操片| 麻豆回家视频区一区二| 无码在线视频播放| AV午夜| 黄色片免费在线观看| 超碰人人干人人操| 成人AV中文字幕| 69成人网| 综合天堂| 日韩午夜电影| 国产va| www.大香蕉伊人| 人人看人人摸人人搞| 人人看人人摸人人操| 男人的天堂婷婷| 91精品无码视频| 国产中文字幕在线| 三级理论网站| 黄色小说视频网站| 国产黄色免费看| 日韩一级免费观看| 欧美高清视频| 国产亚洲久一区二区写真| 欧美成人精品激情在线观看| 99成人视频| 思思久久高颜值| 国产亚洲精品成人a| 福利网站在线观看| 亚洲色图p| 黃色一級片黃色一級片尖叫声-百度-百 | 亚洲精品天堂无码| 日韩区一中文字幕a∨| 亚洲专区免费| 在线观看黄色小电影| 东北老女人操逼视频| 91无码精品久久久一区第1集| 国产农村乱婬片A片AAA图片| 免费A网站| 成人精东影业JDAV3密友| 免费三级片网址| 97精品综合久久| 少妇69p| 成人AV片导航| GOGO人体做爰大胆视频| 日本亚洲精品秘入口A片| 久久丝袜视频| 丝袜美女足交| 国产乱伦AV网站| 国产免费黄色电影| 国产操逼图片| 69AV无码| 日逼片A| 日韩啊啊啊| 黃色一级A片一級片| 无码国精品一区二区免费蜜桃| 91成人无码看片在线观看网址|