欧美一级特黄大片做受成人-亚洲成人一区二区电影-激情熟女一区二区三区-日韩专区欧美专区国产专区

Vue中的虛擬DOM如何構(gòu)建

這篇文章主要介紹了Vue中的虛擬DOM如何構(gòu)建的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Vue中的虛擬DOM如何構(gòu)建文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。

梅州ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書(shū)未來(lái)市場(chǎng)廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書(shū)銷(xiāo)售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話(huà)聯(lián)系或者加微信:13518219792(備注:SSL證書(shū)合作)期待與您的合作!

Vue中的虛擬DOM如何構(gòu)建

虛擬DOM技術(shù)使得我們的頁(yè)面渲染的效率更高,減輕了節(jié)點(diǎn)的操作從而提高性能。

一、真實(shí)DOM和其解析流程

      本節(jié)我們主要介紹真實(shí)   DOM 的解析過(guò)程,通過(guò)介紹其解析過(guò)程以及存在的問(wèn)題,從而引出為什么需要虛擬DOM。一圖勝千言,如下圖為 webkit 渲染引擎工作流程圖

Vue中的虛擬DOM如何構(gòu)建

      所有的瀏覽器渲染引擎工作流程大致分為5步:創(chuàng)建        DOM 樹(shù) —> 創(chuàng)建 Style Rules -> 構(gòu)建 Render 樹(shù) —> 布局 Layout -—> 繪制 Painting。

  • 第一步,構(gòu)建 DOM 樹(shù):用 HTML 分析器,分析 HTML 元素,構(gòu)建一棵 DOM 樹(shù);

  • 第二步,生成樣式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 樣式,生成頁(yè)面的樣式表;

  • 第三步,構(gòu)建 Render 樹(shù):將 DOM 樹(shù)和樣式表關(guān)聯(lián)起來(lái),構(gòu)建一棵 Render 樹(shù)(Attachment)。每個(gè) DOM 節(jié)點(diǎn)都有 attach 方法,接受樣式信息,返回一個(gè) render 對(duì)象(又名 renderer),這些 render 對(duì)象最終會(huì)被構(gòu)建成一棵 Render 樹(shù);

  • 第四步,確定節(jié)點(diǎn)坐標(biāo):根據(jù) Render 樹(shù)結(jié)構(gòu),為每個(gè) Render 樹(shù)上的節(jié)點(diǎn)確定一個(gè)在顯示屏上出現(xiàn)的精確坐標(biāo);

  • 第五步,繪制頁(yè)面:根據(jù) Render 樹(shù)和節(jié)點(diǎn)顯示坐標(biāo),然后調(diào)用每個(gè)節(jié)點(diǎn)的 paint 方法,將它們繪制出來(lái)。

注意點(diǎn):

1、DOM 樹(shù)的構(gòu)建是文檔加載完成開(kāi)始的?構(gòu)建 DOM 樹(shù)是一個(gè)漸進(jìn)過(guò)程,為達(dá)到更好的用戶(hù)體驗(yàn),渲染引擎會(huì)盡快將內(nèi)容顯示在屏幕上,它不必等到整個(gè) HTML 文檔解析完成之后才開(kāi)始構(gòu)建 render 樹(shù)和布局。

2、Render 樹(shù)是 DOM 樹(shù)和 CSS 樣式表構(gòu)建完畢后才開(kāi)始構(gòu)建的?這三個(gè)過(guò)程在實(shí)際進(jìn)行的時(shí)候并不是完全獨(dú)立的,而是會(huì)有交叉,會(huì)一邊加載,一邊解析,以及一邊渲染。

3、CSS 的解析注意點(diǎn)?CSS 的解析是從右往左逆向解析的,嵌套標(biāo)簽越多,解析越慢。

4、JS 操作真實(shí) DOM 的代價(jià)?用我們傳統(tǒng)的開(kāi)發(fā)模式,原生 JSJQ 操作 DOM 時(shí),瀏覽器會(huì)從構(gòu)建 DOM 樹(shù)開(kāi)始從頭到尾執(zhí)行一遍流程。在一次操作中,我需要更新 10 個(gè) DOM 節(jié)點(diǎn),瀏覽器收到第一個(gè) DOM 請(qǐng)求后并不知道還有 9 次更新操作,因此會(huì)馬上執(zhí)行流程,最終執(zhí)行10 次。例如,第一次計(jì)算完,緊接著下一個(gè) DOM 更新請(qǐng)求,這個(gè)節(jié)點(diǎn)的坐標(biāo)值就變了,前一次計(jì)算為無(wú)用功。計(jì)算 DOM 節(jié)點(diǎn)坐標(biāo)值等都是白白浪費(fèi)的性能。即使計(jì)算機(jī)硬件一直在迭代更新,操作 DOM 的代價(jià)仍舊是昂貴的,頻繁操作還是會(huì)出現(xiàn)頁(yè)面卡頓,影響用戶(hù)體驗(yàn)

二、Virtual-DOM 基礎(chǔ)

2.1、虛擬 DOM 的好處

      虛擬 DOM 就是為了解決瀏覽器性能問(wèn)題而被設(shè)計(jì)出來(lái)的。如前,若一次操作中有 10 次更新 DOM 的動(dòng)作,虛擬 DOM 不會(huì)立即操作 DOM,而是將這 10 次更新的 diff 內(nèi)容保存到本地一個(gè) JS 對(duì)象中,最終將這個(gè) JS 對(duì)象一次性 attchDOM 樹(shù)上,再進(jìn)行后續(xù)操作,避免大量無(wú)謂的計(jì)算量。所以,用 JS 對(duì)象模擬 DOM 節(jié)點(diǎn)的好處是,頁(yè)面的更新可以先全部反映在 JS 對(duì)象(虛擬 DOM )上,操作內(nèi)存中的 JS 對(duì)象的速度顯然要更快,等更新完成后,再將最終的 JS 對(duì)象映射成真實(shí)的 DOM,交由瀏覽器去繪制。

2.2、算法實(shí)現(xiàn)

2.2.1、用 JS 對(duì)象模擬 DOM 樹(shù)

(1)如何用 JS 對(duì)象模擬 DOM 樹(shù)

例如一個(gè)真實(shí)的 DOM 節(jié)點(diǎn)如下:

<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
  <li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>

我們用 JavaScript 對(duì)象來(lái)表示 DOM 節(jié)點(diǎn),使用對(duì)象的屬性記錄節(jié)點(diǎn)的類(lèi)型、屬性、子節(jié)點(diǎn)等。

element.js 中表示節(jié)點(diǎn)對(duì)象代碼如下:

/**
 * Element virdual-dom 對(duì)象定義
 * @param {String} tagName - dom 元素名稱(chēng)
 * @param {Object} props - dom 屬性
 * @param {Array<Element|String>} - 子節(jié)點(diǎn)
 */
function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
    // dom 元素的 key 值,用作唯一標(biāo)識(shí)符
    if(props.key){
       this.key = props.key
    }
    var count = 0
    children.forEach(function (child, i) {
        if (child instanceof Element) {
            count += child.count
        } else {
            children[i] = '' + child
        }
        count++
    })
    // 子元素個(gè)數(shù)
    this.count = count
}

function createElement(tagName, props, children){
 return new Element(tagName, props, children);
}

module.exports = createElement;

根據(jù) element 對(duì)象的設(shè)定,則上面的 DOM 結(jié)構(gòu)就可以簡(jiǎn)單表示為:

var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
])

現(xiàn)在 ul 就是我們用  JavaScript 對(duì)象表示的 DOM 結(jié)構(gòu),我們輸出查看 ul 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下:

Vue中的虛擬DOM如何構(gòu)建

(2)渲染用 JS 表示的 DOM 對(duì)象

但是頁(yè)面上并沒(méi)有這個(gè)結(jié)構(gòu),下一步我們介紹如何將 ul 渲染成頁(yè)面上真實(shí)的 DOM 結(jié)構(gòu),相關(guān)渲染函數(shù)如下:

/**
 * render 將virdual-dom 對(duì)象渲染為實(shí)際 DOM 元素
 */
Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props
    // 設(shè)置節(jié)點(diǎn)的DOM屬性
    for (var propName in props) {
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
    }

    var children = this.children || []
    children.forEach(function (child) {
        var childEl = (child instanceof Element)
            ? child.render() // 如果子節(jié)點(diǎn)也是虛擬DOM,遞歸構(gòu)建DOM節(jié)點(diǎn)
            : document.createTextNode(child) // 如果字符串,只構(gòu)建文本節(jié)點(diǎn)
        el.appendChild(childEl)
    })
    return el
}

我們通過(guò)查看以上 render 方法,會(huì)根據(jù)  tagName 構(gòu)建一個(gè)真正的 DOM 節(jié)點(diǎn),然后設(shè)置這個(gè)節(jié)點(diǎn)的屬性,最后遞歸地把自己的子節(jié)點(diǎn)也構(gòu)建起來(lái)。

我們將構(gòu)建好的 DOM 結(jié)構(gòu)添加到頁(yè)面 body 上面,如下:

ulRoot = ul.render();
document.body.appendChild(ulRoot);

這樣,頁(yè)面 body 里面就有真正的 DOM 結(jié)構(gòu),效果如下圖所示:

Vue中的虛擬DOM如何構(gòu)建

2.2.2、比較兩棵虛擬 DOM 樹(shù)的差異 — diff 算法

diff 算法用來(lái)比較兩棵 Virtual DOM 樹(shù)的差異,如果需要兩棵樹(shù)的完全比較,那么 diff 算法的時(shí)間復(fù)雜度為O(n^3)。但是在前端當(dāng)中,你很少會(huì)跨越層級(jí)地移動(dòng) DOM 元素,所以 Virtual DOM 只會(huì)對(duì)同一個(gè)層級(jí)的元素進(jìn)行對(duì)比,如下圖所示, div 只會(huì)和同一層級(jí)的 div 對(duì)比,第二層級(jí)的只會(huì)跟第二層級(jí)對(duì)比,這樣算法復(fù)雜度就可以達(dá)到 O(n)。

Vue中的虛擬DOM如何構(gòu)建

(1)深度優(yōu)先遍歷,記錄差異

在實(shí)際的代碼中,會(huì)對(duì)新舊兩棵樹(shù)進(jìn)行一個(gè)深度優(yōu)先的遍歷,這樣每個(gè)節(jié)點(diǎn)都會(huì)有一個(gè)唯一的標(biāo)記:

Vue中的虛擬DOM如何構(gòu)建

在深度優(yōu)先遍歷的時(shí)候,每遍歷到一個(gè)節(jié)點(diǎn)就把該節(jié)點(diǎn)和新的的樹(shù)進(jìn)行對(duì)比。如果有差異的話(huà)就記錄到一個(gè)對(duì)象里面。

// diff 函數(shù),對(duì)比兩棵樹(shù)
function diff(oldTree, newTree) {
  var index = 0 // 當(dāng)前節(jié)點(diǎn)的標(biāo)志
  var patches = {} // 用來(lái)記錄每個(gè)節(jié)點(diǎn)差異的對(duì)象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對(duì)兩棵樹(shù)進(jìn)行深度優(yōu)先遍歷
function dfsWalk(oldNode, newNode, index, patches) {
  var currentPatch = []
  if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
    // 文本內(nèi)容改變
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 節(jié)點(diǎn)相同,比較屬性
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // 比較子節(jié)點(diǎn),如果子節(jié)點(diǎn)有'ignore'屬性,則不需要比較
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else if(newNode !== null){
    // 新節(jié)點(diǎn)和舊節(jié)點(diǎn)不同,用 replace 替換
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}

從以上可以得出,patches[1] 表示 p ,patches[3] 表示 ul ,以此類(lèi)推。

(2)差異類(lèi)型

DOM 操作導(dǎo)致的差異類(lèi)型包括以下幾種:

  • 節(jié)點(diǎn)替換:節(jié)點(diǎn)改變了,例如將上面的 div 換成 h2;

  • 順序互換:移動(dòng)、刪除、新增子節(jié)點(diǎn),例如上面 div 的子節(jié)點(diǎn),把 pul 順序互換;

  • 屬性更改:修改了節(jié)點(diǎn)的屬性,例如把上面 liclass 樣式類(lèi)刪除;

  • 文本改變:改變文本節(jié)點(diǎn)的文本內(nèi)容,例如將上面 p 節(jié)點(diǎn)的文本內(nèi)容更改為 “Real Dom”;

以上描述的幾種差異類(lèi)型在代碼中定義如下所示:

var REPLACE = 0 // 替換原先的節(jié)點(diǎn)
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了節(jié)點(diǎn)的屬性
var TEXT = 3 // 文本內(nèi)容改變

(3)列表對(duì)比算法

      子節(jié)點(diǎn)的對(duì)比算法,例如      p, ul, div 的順序換成了 div, p, ul。這個(gè)該怎么對(duì)比?如果按照同層級(jí)進(jìn)行順序?qū)Ρ鹊脑?huà),它們都會(huì)被替換掉。如 pdivtagName 不同,p 會(huì)被 div 所替代。最終,三個(gè)節(jié)點(diǎn)都會(huì)被替換,這樣 DOM 開(kāi)銷(xiāo)就非常大。而實(shí)際上是不需要替換節(jié)點(diǎn),而只需要經(jīng)過(guò)節(jié)點(diǎn)移動(dòng)就可以達(dá)到,我們只需知道怎么進(jìn)行移動(dòng)。

      將這個(gè)問(wèn)題抽象出來(lái)其實(shí)就是字符串的最小編輯距離問(wèn)題(Edition Distance),最常見(jiàn)的解決方法是 Levenshtein Distance , Levenshtein Distance 是一個(gè)度量?jī)蓚€(gè)字符序列之間差異的字符串度量標(biāo)準(zhǔn),兩個(gè)單詞之間的 Levenshtein Distance 是將一個(gè)單詞轉(zhuǎn)換為另一個(gè)單詞所需的單字符編輯(插入、刪除或替換)的最小數(shù)量。Levenshtein Distance 是1965年由蘇聯(lián)數(shù)學(xué)家 Vladimir Levenshtein 發(fā)明的。Levenshtein Distance 也被稱(chēng)為編輯距離(Edit Distance),通過(guò)動(dòng)態(tài)規(guī)劃求解,時(shí)間復(fù)雜度為 O(M*N)。

定義:對(duì)于兩個(gè)字符串 a、b,則他們的 Levenshtein Distance 為:

Vue中的虛擬DOM如何構(gòu)建

示例:字符串 ab,a=“abcde” ,b=“cabef”,根據(jù)上面給出的計(jì)算公式,則他們的 Levenshtein Distance 的計(jì)算過(guò)程如下:

Vue中的虛擬DOM如何構(gòu)建

本文的 demo 使用插件 list-diff2 算法進(jìn)行比較,該算法的時(shí)間復(fù)雜度偉 O(n*m),雖然該算法并非最優(yōu)的算法,但是用于對(duì)于 dom 元素的常規(guī)操作是足夠的。該算法具體的實(shí)現(xiàn)過(guò)程這里不再詳細(xì)介紹,該算法的具體介紹可以參照:github.com/livoras/lis…

(4)實(shí)例輸出

兩個(gè)虛擬 DOM 對(duì)象如下圖所示,其中 ul1 表示原有的虛擬 DOM 樹(shù),ul2 表示改變后的虛擬 DOM 樹(shù)

var ul1 = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
]) 
var ul2 = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 21']),
	el('li', { class: 'item' }, ['Item 23'])
  ]),
  el('p',{},['Hello World'])
]) 
var patches = diff(ul1,ul2);
console.log('patches:',patches);

我們查看輸出的兩個(gè)虛擬 DOM 對(duì)象之間的差異對(duì)象如下圖所示,我們能通過(guò)差異對(duì)象得到,兩個(gè)虛擬 DOM 對(duì)象之間進(jìn)行了哪些變化,從而根據(jù)這個(gè)差異對(duì)象(patches)更改原先的真實(shí) DOM 結(jié)構(gòu),從而將頁(yè)面的 DOM 結(jié)構(gòu)進(jìn)行更改。

Vue中的虛擬DOM如何構(gòu)建

2.2.3、將兩個(gè)虛擬 DOM 對(duì)象的差異應(yīng)用到真正的 DOM 樹(shù)

(1)深度優(yōu)先遍歷 DOM 樹(shù)

      因?yàn)椴襟E一所構(gòu)建的         JavaScript 對(duì)象樹(shù)和 render 出來(lái)真正的 DOM 樹(shù)的信息、結(jié)構(gòu)是一樣的。所以我們可以對(duì)那棵 DOM 樹(shù)也進(jìn)行深度優(yōu)先的遍歷,遍歷的時(shí)候從步驟二生成的 patches 對(duì)象中找出當(dāng)前遍歷的節(jié)點(diǎn)差異,如下相關(guān)代碼所示:

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  // 從patches拿出當(dāng)前節(jié)點(diǎn)的差異
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  // 深度遍歷子節(jié)點(diǎn)
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }
  // 對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行DOM操作
  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
}

(2)對(duì)原有 DOM 樹(shù)進(jìn)行 DOM 操作

我們根據(jù)不同類(lèi)型的差異對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行不同的 DOM 操作 ,例如如果進(jìn)行了節(jié)點(diǎn)替換,就進(jìn)行節(jié)點(diǎn)替換 DOM 操作;如果節(jié)點(diǎn)文本發(fā)生了改變,則進(jìn)行文本替換的 DOM 操作;以及子節(jié)點(diǎn)重排、屬性改變等 DOM 操作,相關(guān)代碼如 applyPatches 所示 :

function applyPatches (node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

(3)DOM結(jié)構(gòu)改變

通過(guò)將第 2.2.2 得到的兩個(gè) DOM 對(duì)象之間的差異,應(yīng)用到第一個(gè)(原先)DOM 結(jié)構(gòu)中,我們可以看到 DOM 結(jié)構(gòu)進(jìn)行了預(yù)期的變化,如下圖所示:

Vue中的虛擬DOM如何構(gòu)建

2.3、結(jié)語(yǔ)

相關(guān)代碼實(shí)現(xiàn)已經(jīng)放到 github 上面,有興趣的同學(xué)可以clone運(yùn)行實(shí)驗(yàn),github地址為:github.com/fengshi123/…

Virtual DOM 算法主要實(shí)現(xiàn)上面三個(gè)步驟來(lái)實(shí)現(xiàn):

  • JS 對(duì)象模擬 DOM 樹(shù) — element.js

    <div id="virtual-dom">
    <p>Virtual DOM</p>
    <ul id="list">
      <li class="item">Item 1</li>
      <li class="item">Item 2</li>
      <li class="item">Item 3</li>
    </ul>
    <div>Hello World</div>
    </div>
  • 比較兩棵虛擬 DOM 樹(shù)的差異 — diff.js

Vue中的虛擬DOM如何構(gòu)建

  • 將兩個(gè)虛擬 DOM 對(duì)象的差異應(yīng)用到真正的 DOM 樹(shù) — patch.js

    function applyPatches (node, currentPatches) {
      currentPatches.forEach(currentPatch => {
        switch (currentPatch.type) {
          case REPLACE:
            var newNode = (typeof currentPatch.node === 'string')
              ? document.createTextNode(currentPatch.node)
              : currentPatch.node.render()
            node.parentNode.replaceChild(newNode, node)
            break
          case REORDER:
            reorderChildren(node, currentPatch.moves)
            break
          case PROPS:
            setProps(node, currentPatch.props)
            break
          case TEXT:
            node.textContent = currentPatch.content
            break
          default:
            throw new Error('Unknown patch type ' + currentPatch.type)
        }
      })
    }

三、Vue 源碼 Virtual-DOM 簡(jiǎn)析

我們從第二章節(jié)(Virtual-DOM 基礎(chǔ))中已經(jīng)掌握 Virtual DOM 渲染成真實(shí)的 DOM 實(shí)際上要經(jīng)歷 VNode 的定義、diff、patch 等過(guò)程,所以本章節(jié) Vue 源碼的解析也按這幾個(gè)過(guò)程來(lái)簡(jiǎn)析。

3.1、VNode 模擬 DOM 樹(shù)

3.1.1、VNode 類(lèi)簡(jiǎn)析

Vue.js 中,Virtual DOM 是用 VNode 這個(gè) Class 去描述,它定義在 src/core/vdom/vnode.js 中 ,從以下代碼塊中可以看到 Vue.js 中的 Virtual DOM 的定義較為復(fù)雜一些,因?yàn)樗@里包含了很多 Vue.js 的特性。實(shí)際上 Vue.jsVirtual DOM 是借鑒了一個(gè)開(kāi)源庫(kù)  snabbdom 的實(shí)現(xiàn),然后加入了一些 Vue.js 的一些特性。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}

這里千萬(wàn)不要因?yàn)?VNode 的這么屬性而被嚇到,或者咬緊牙去摸清楚每個(gè)屬性的意義,其實(shí),我們主要了解其幾個(gè)核心的關(guān)鍵屬性就差不多了,例如:

  • tag 屬性即這個(gè)vnode的標(biāo)簽屬性

  • data 屬性包含了最后渲染成真實(shí)dom節(jié)點(diǎn)后,節(jié)點(diǎn)上的class,attribute,style以及綁定的事件

  • children 屬性是vnode的子節(jié)點(diǎn)

  • text 屬性是文本屬性

  • elm 屬性為這個(gè)vnode對(duì)應(yīng)的真實(shí)dom節(jié)點(diǎn)

  • key 屬性是vnode的標(biāo)記,在diff過(guò)程中可以提高diff的效率

3.1.2、源碼創(chuàng)建 VNode 過(guò)程

(1)初始化vue

我們?cè)趯?shí)例化一個(gè) vue 實(shí)例,也即 new Vue( ) 時(shí),實(shí)際上是執(zhí)行 src/core/instance/index.js  中定義的 Function 函數(shù)。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

通過(guò)查看 Vuefunction,我們知道 Vue 只能通過(guò) new 關(guān)鍵字初始化,然后調(diào)用 this._init 方法,該方法在 src/core/instance/init.js 中定義。

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
      
    // 省略一系列其它初始化的代碼
      
    if (vm.$options.el) {
      console.log('vm.$options.el:',vm.$options.el);
      vm.$mount(vm.$options.el)
    }
  }

(2)Vue 實(shí)例掛載

Vue 中是通過(guò) $mount 實(shí)例方法去掛載 dom 的,下面我們通過(guò)分析 compiler 版本的 mount 實(shí)現(xiàn),相關(guān)源碼在目錄 src/platforms/web/entry-runtime-with-compiler.js 文件中定義:。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  
   // 省略一系列初始化以及邏輯判斷代碼  
 
  return mount.call(this, el, hydrating)
}

我們發(fā)現(xiàn)最終還是調(diào)用用原先原型上的 $mount 方法掛載 ,原先原型上的 $mount 方法在 src/platforms/web/runtime/index.js 中定義 。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

我們發(fā)現(xiàn)$mount 方法實(shí)際上會(huì)去調(diào)用 mountComponent 方法,這個(gè)方法定義在 src/core/instance/lifecycle.js 文件中

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代碼
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虛擬 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 實(shí)例化一個(gè)渲染W(wǎng)atcher,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

從上面的代碼可以看到,mountComponent 核心就是先實(shí)例化一個(gè)渲染Watcher,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法,在此方法中調(diào)用 vm._render 方法先生成虛擬 Node,最終調(diào)用 vm._update 更新 DOM。

(3)創(chuàng)建虛擬 Node

Vue_render 方法是實(shí)例的一個(gè)私有方法,它用來(lái)把實(shí)例渲染成一個(gè)虛擬 Node。它的定義在 src/core/instance/render.js 文件中:

 Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    let vnode
    try {
      // 省略一系列代碼  
      currentRenderingInstance = vm
      // 調(diào)用 createElement 方法來(lái)返回 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`){}
    }
    // set parent
    vnode.parent = _parentVnode
    console.log("vnode...:",vnode);
    return vnode
  }

Vue.js 利用 _createElement 方法創(chuàng)建 VNode,它定義在 src/core/vdom/create-elemenet.js 中:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    
  // 省略一系列非主線(xiàn)代碼
  
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 場(chǎng)景是 render 函數(shù)不是編譯生成的
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 場(chǎng)景是 render 函數(shù)是編譯生成的
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // 創(chuàng)建虛擬 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElement 方法有 5 個(gè)參數(shù),context 表示 VNode 的上下文環(huán)境,它是 Component 類(lèi)型;tag表示標(biāo)簽,它可以是一個(gè)字符串,也可以是一個(gè) Component;data 表示 VNode 的數(shù)據(jù),它是一個(gè) VNodeData 類(lèi)型,可以在 flow/vnode.js 中找到它的定義;children 表示當(dāng)前 VNode 的子節(jié)點(diǎn),它是任意類(lèi)型的,需要被規(guī)范為標(biāo)準(zhǔn)的 VNode 數(shù)組;

3.1.3、實(shí)例查看

為了更直觀查看我們平時(shí)寫(xiě)的 Vue 代碼如何用 VNode 類(lèi)來(lái)表示,我們通過(guò)一個(gè)實(shí)例的轉(zhuǎn)換進(jìn)行更深刻了解。

例如,實(shí)例化一個(gè) Vue 實(shí)例:

  var app = new Vue({
    el: '#app',
    render: function (createElement) {
      return createElement('div', {
        attrs: {
          id: 'app',
          class: "class_box"
        },
      }, this.message)
    },
    data: {
      message: 'Hello Vue!'
    }
  })

我們打印出其對(duì)應(yīng)的 VNode 表示:

Vue中的虛擬DOM如何構(gòu)建

3.2、diff 過(guò)程

3.2.1、Vue.js 源碼的 diff 調(diào)用邏輯

Vue.js 源碼實(shí)例化了一個(gè) watcher,這個(gè) ~ 被添加到了在模板當(dāng)中所綁定變量的依賴(lài)當(dāng)中,一旦 model 中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化,這些響應(yīng)式的數(shù)據(jù)所維護(hù)的 dep 數(shù)組便會(huì)調(diào)用 dep.notify() 方法完成所有依賴(lài)遍歷執(zhí)行的工作,這包括視圖的更新,即 updateComponent 方法的調(diào)用。watcherupdateComponent方法定義在  src/core/instance/lifecycle.js 文件中 。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代碼
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虛擬 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 實(shí)例化一個(gè)渲染W(wǎng)atcher,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

完成視圖的更新工作事實(shí)上就是調(diào)用了vm._update方法,這個(gè)方法接收的第一個(gè)參數(shù)是剛生成的Vnode,調(diào)用的vm._update方法定義在 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
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 第一個(gè)參數(shù)為真實(shí)的node節(jié)點(diǎn),則為初始化
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 如果需要diff的prevVnode存在,那么對(duì)prevVnode和vnode進(jìn)行diff
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

在這個(gè)方法當(dāng)中最為關(guān)鍵的就是 vm.__patch__ 方法,這也是整個(gè) virtual-dom 當(dāng)中最為核心的方法,主要完成了prevVnodevnodediff 過(guò)程并根據(jù)需要操作的 vdom 節(jié)點(diǎn)打 patch,最后生成新的真實(shí) dom 節(jié)點(diǎn)并完成視圖的更新工作。

接下來(lái),讓我們看下 vm.__patch__的邏輯過(guò)程, vm.__patch__ 方法定義在 src/core/vdom/patch.js 中。

function patch (oldVnode, vnode, hydrating, removeOnly) {
    ......
    if (isUndef(oldVnode)) {
      // 當(dāng)oldVnode不存在時(shí),創(chuàng)建新的節(jié)點(diǎn)
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 對(duì)oldVnode和vnode進(jìn)行diff,并對(duì)oldVnode打patch  
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } 
	......
  }
}

patch 方法中,我們看到會(huì)分為兩種情況,一種是當(dāng) oldVnode 不存在時(shí),會(huì)創(chuàng)建新的節(jié)點(diǎn);另一種則是已經(jīng)存在 oldVnode ,那么會(huì)對(duì) oldVnodevnode 進(jìn)行 diffpatch 的過(guò)程。其中 patch 過(guò)程中會(huì)調(diào)用 sameVnode 方法來(lái)對(duì)對(duì)傳入的2個(gè) vnode 進(jìn)行基本屬性的比較,只有當(dāng)基本屬性相同的情況下才認(rèn)為這個(gè)2個(gè)vnode 只是局部發(fā)生了更新,然后才會(huì)對(duì)這2個(gè) vnode 進(jìn)行 diff,如果2個(gè) vnode 的基本屬性存在不一致的情況,那么就會(huì)直接跳過(guò) diff 的過(guò)程,進(jìn)而依據(jù) vnode 新建一個(gè)真實(shí)的 dom,同時(shí)刪除老的 dom節(jié)點(diǎn)。

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

diff 過(guò)程中主要是通過(guò)調(diào)用 patchVnode 方法進(jìn)行的:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    ...... 
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 如果vnode沒(méi)有文本節(jié)點(diǎn)
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children屬性存在且vnode的children屬性也存在  
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren,對(duì)子節(jié)點(diǎn)進(jìn)行diff  
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 如果oldVnode的text存在,那么首先清空text的內(nèi)容,然后將vnode的children添加進(jìn)去  
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 刪除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有子節(jié)點(diǎn),而vnode沒(méi)有,那么就清空這個(gè)節(jié)點(diǎn)  
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點(diǎn)的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }
    ......
  }

從以上代碼得知,

diff 過(guò)程中又分了好幾種情況,oldCholdVnode的子節(jié)點(diǎn),chVnode的子節(jié)點(diǎn):

  • 首先進(jìn)行文本節(jié)點(diǎn)的判斷,若 oldVnode.text !== vnode.text,那么就會(huì)直接進(jìn)行文本節(jié)點(diǎn)的替換;

  • vnode  沒(méi)有文本節(jié)點(diǎn)的情況下,進(jìn)入子節(jié)點(diǎn)的 diff;

  • 當(dāng) oldChch 都存在且不相同的情況下,調(diào)用 updateChildren 對(duì)子節(jié)點(diǎn)進(jìn)行 diff

  • oldCh不存在,ch 存在,首先清空 oldVnode 的文本節(jié)點(diǎn),同時(shí)調(diào)用 addVnodes 方法將 ch 添加到elm真實(shí) dom 節(jié)點(diǎn)當(dāng)中;

  • oldCh存在,ch不存在,則刪除 elm 真實(shí)節(jié)點(diǎn)下的 oldCh 子節(jié)點(diǎn);

  • oldVnode 有文本節(jié)點(diǎn),而 vnode 沒(méi)有,那么就清空這個(gè)文本節(jié)點(diǎn)。

3.2.2、子節(jié)點(diǎn) diff 流程分析

(1)Vue.js 源碼

      這里著重分析下updateChildren方法,它也是整個(gè) diff 過(guò)程中最重要的環(huán)節(jié),以下為 Vue.js 的源碼過(guò)程,為了更形象理解 diff 過(guò)程,我們給出相關(guān)的示意圖來(lái)講解。

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 為oldCh和newCh分別建立索引,為之后遍歷的依據(jù)
    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

    // 直到oldCh或者newCh被遍歷完后跳出循環(huán)
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--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]
      } 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 {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

在開(kāi)始遍歷 diff 前,首先給 oldChnewCh 分別分配一個(gè) startIndexendIndex 來(lái)作為遍歷的索引,當(dāng)oldCh 或者 newCh 遍歷完后(遍歷完的條件就是 oldCh 或者 newCh 標(biāo)題名稱(chēng):Vue中的虛擬DOM如何構(gòu)建
網(wǎng)址分享:http://aaarwkj.com/article6/ggooog.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁(yè)設(shè)計(jì)公司品牌網(wǎng)站設(shè)計(jì)、ChatGPT響應(yīng)式網(wǎng)站、電子商務(wù)、品牌網(wǎng)站制作

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

末满18周岁禁止观看| 欧美老熟妇子乱视频在线| 久草视频免费福利资源站| 国产亚洲精品一区二区三在线观看| av免费观看一区二区三区| 亚洲天堂免费观看av| 色哟哟亚洲精品在线视频| 日韩在线观看精品亚洲| 国产在线不卡免费精品| 亚洲午夜一区二区精品| 日韩欧美一区二区三区在线| 欧美亚洲清纯唯美另类| 国产成人精品免费视频大| 国产精品熟女一区二区三区| 亚洲成综合人在线播放| 一本色道久久亚洲综合精品蜜桃 | 精品一区二区三区推荐| 婷婷激情六月中文字幕| 中文字幕加勒比东京热| 久热精品视频在线观看| 午夜视频在线观看免费高清国产| 日本在线高清精品人妻| 桃色av一区二区三区| 久草视频在线免费资源站| 国产熟女碰碰人人a久久| 免费av在线观看日韩| 日韩中文字幕乱码卡一| 久久婷婷精品国产亚洲av| 欧美精品青青久久久久久| 麻豆文化传媒免费网址| 日本大片一区二区免费看| 极品大胸美女被啪啪的高潮 | 欧美日韩福利一区二区三区| 国产精品久久久久久久av三级| 亚洲国产成人91精品| 国产蜜臀视频一区二区三区| 中文字幕欧美人妻在线| 精品亚洲天堂一区二区三区| 亚洲国产精品一区二区| 国产精品久久久久久爽| 亚洲伦理av在线观看|