<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    Vue進(jìn)階面試必問,異步更新機(jī)制和nextTick原理

    共 11687字,需瀏覽 24分鐘

     ·

    2020-09-18 02:14

    vue已是目前國內(nèi)前端web端三分天下之一,同時也作為本人主要技術(shù)棧之一,在日常使用中知其然也好奇著所以然,另外最近的社區(qū)涌現(xiàn)了一大票vue源碼閱讀類的文章,在下借這個機(jī)會從大家的文章和討論中汲取了一些營養(yǎng),同時對一些閱讀源碼時的想法進(jìn)行總結(jié),出產(chǎn)一些文章,作為自己思考的輸出,本人水平有限,歡迎留言討論~

    目標(biāo)Vue版本:2.5.17-beta.0

    vue源碼注釋:https://github.com/SHERlocked93/vue-analysis

    聲明:文章中源碼的語法都使用 Flow,并且源碼根據(jù)需要都有刪節(jié)(為了不被迷糊 @_@),如果要看完整版的請進(jìn)入上面的github地址,本文是系列文章,文章地址見底部~

    1. 異步更新

    上一篇文章我們在依賴收集原理的響應(yīng)式化方法 defineReactive 中的 setter 訪問器中有派發(fā)更新 dep.notify() 方法,這個方法會挨個通知在 depsubs 中收集的訂閱自己變動的watchers執(zhí)行update。一起來看看 update 方法的實現(xiàn):

    // src/core/observer/watcher.js

    /* Subscriber接口,當(dāng)依賴發(fā)生改變的時候進(jìn)行回調(diào) */
    update() {
    if (this.computed) {
    // 一個computed watcher有兩種模式:activated lazy(默認(rèn))
    // 只有當(dāng)它被至少一個訂閱者依賴時才置activated,這通常是另一個計算屬性或組件的render function
    if (this.dep.subs.length === 0) { // 如果沒人訂閱這個計算屬性的變化
    // lazy時,我們希望它只在必要時執(zhí)行計算,所以我們只是簡單地將觀察者標(biāo)記為dirty
    // 當(dāng)計算屬性被訪問時,實際的計算在this.evaluate()中執(zhí)行
    this.dirty = true
    } else {
    // activated模式下,我們希望主動執(zhí)行計算,但只有當(dāng)值確實發(fā)生變化時才通知我們的訂閱者
    this.getAndInvoke(() => {
    this.dep.notify() // 通知渲染watcher重新渲染,通知依賴自己的所有watcher執(zhí)行update
    })
    }
    } else if (this.sync) { // 同步
    this.run()
    } else {
    queueWatcher(this) // 異步推送到調(diào)度者觀察者隊列中,下一個tick時調(diào)用
    }
    }

    如果不是 computed watcher 也非 sync 會把調(diào)用update的當(dāng)前watcher推送到調(diào)度者隊列中,下一個tick時調(diào)用,看看 queueWatcher

    // src/core/observer/scheduler.js

    /* 將一個觀察者對象push進(jìn)觀察者隊列,在隊列中已經(jīng)存在相同的id則
    * 該watcher將被跳過,除非它是在隊列正被flush時推送
    */

    export function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) { // 檢驗id是否存在,已經(jīng)存在則直接跳過,不存在則標(biāo)記哈希表has,用于下次檢驗
    has[id] = true
    queue.push(watcher) // 如果沒有正在flush,直接push到隊列中
    if (!waiting) { // 標(biāo)記是否已傳給nextTick
    waiting = true
    nextTick(flushSchedulerQueue)
    }
    }
    }

    /* 重置調(diào)度者狀態(tài) */
    function resetSchedulerState () {
    queue.length = 0
    has = {}
    waiting = false
    }

    這里使用了一個 has 的哈希map用來檢查是否當(dāng)前watcher的id是否存在,若已存在則跳過,不存在則就push到 queue 隊列中并標(biāo)記哈希表has,用于下次檢驗,防止重復(fù)添加。這就是一個去重的過程,比每次查重都要去queue中找要文明,在渲染的時候就不會重復(fù) patch 相同watcher的變化,這樣就算同步修改了一百次視圖中用到的data,異步 patch 的時候也只會更新最后一次修改。

    這里的 waiting 方法是用來標(biāo)記 flushSchedulerQueue 是否已經(jīng)傳遞給 nextTick 的標(biāo)記位,如果已經(jīng)傳遞則只push到隊列中不傳遞 flushSchedulerQueuenextTick,等到 resetSchedulerState 重置調(diào)度者狀態(tài)的時候 waiting 會被置回 false 允許 flushSchedulerQueue 被傳遞給下一個tick的回調(diào),總之保證了 flushSchedulerQueue 回調(diào)在一個tick內(nèi)只允許被傳入一次。來看看被傳遞給 nextTick 的回調(diào) flushSchedulerQueue 做了什么:

    // src/core/observer/scheduler.js

    /* nextTick的回調(diào)函數(shù),在下一個tick時flush掉兩個隊列同時運行watchers */
    function flushSchedulerQueue () {
    flushing = true
    let watcher, id

    queue.sort((a, b) => a.id - b.id) // 排序

    for (index = 0; index < queue.length; index++) { // 不要將length進(jìn)行緩存
    watcher = queue[index]
    if (watcher.before) { // 如果watcher有before則執(zhí)行
    watcher.before()
    }
    id = watcher.id
    has[id] = null // 將has的標(biāo)記刪除
    watcher.run() // 執(zhí)行watcher
    if (process.env.NODE_ENV !== 'production' && has[id] != null) { // 在dev環(huán)境下檢查是否進(jìn)入死循環(huán)
    circular[id] = (circular[id] || 0) + 1 // 比如user watcher訂閱自己的情況
    if (circular[id] > MAX_UPDATE_COUNT) { // 持續(xù)執(zhí)行了一百次watch代表可能存在死循環(huán)
    warn() // 進(jìn)入死循環(huán)的警告
    break
    }
    }
    }
    resetSchedulerState() // 重置調(diào)度者狀態(tài)
    callActivatedHooks() // 使子組件狀態(tài)都置成active同時調(diào)用activated鉤子
    callUpdatedHooks() // 調(diào)用updated鉤子
    }

    nextTick 方法中執(zhí)行 flushSchedulerQueue 方法,這個方法挨個執(zhí)行 queue 中的watcher的 run 方法。我們看到在首先有個 queue.sort() 方法把隊列中的watcher按id從小到大排了個序,這樣做可以保證:

    1. 組件更新的順序是從父組件到子組件的順序,因為父組件總是比子組件先創(chuàng)建。
    2. 一個組件的user watchers(偵聽器watcher)比render watcher先運行,因為user watchers往往比render watcher更早創(chuàng)建
    3. 如果一個組件在父組件watcher運行期間被銷毀,它的watcher執(zhí)行將被跳過

    在挨個執(zhí)行隊列中的for循環(huán)中,index < queue.length 這里沒有將length進(jìn)行緩存,因為在執(zhí)行處理現(xiàn)有watcher對象期間,更多的watcher對象可能會被push進(jìn)queue。

    那么數(shù)據(jù)的修改從model層反映到view的過程:數(shù)據(jù)更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新視圖

    2. nextTick原理

    2.1 宏任務(wù)/微任務(wù)

    這里就來看看包含著每個watcher執(zhí)行的方法被作為回調(diào)傳入 nextTick 之后,nextTick 對這個方法做了什么。不過首先要了解一下瀏覽器中的 EventLoop、macro task、micro task幾個概念,不了解可以參考一下 JS與Node.js中的事件循環(huán) 這篇文章,這里就用一張圖來表明一下后兩者在主線程中的執(zhí)行關(guān)系:

    宏任務(wù)微任務(wù)

    解釋一下,當(dāng)主線程執(zhí)行完同步任務(wù)后:

    1. 引擎首先從macrotask queue中取出第一個任務(wù),執(zhí)行完畢后,將microtask queue中的所有任務(wù)取出,按順序全部執(zhí)行;
    2. 然后再從macrotask queue中取下一個,執(zhí)行完畢后,再次將microtask queue中的全部取出;
    3. 循環(huán)往復(fù),直到兩個queue中的任務(wù)都取完。

    瀏覽器環(huán)境中常見的異步任務(wù)種類,按照優(yōu)先級:

    • macro task :同步代碼、setImmediate、MessageChannel、setTimeout/setInterval
    • micro taskPromise.then、MutationObserver

    有的文章把 micro task 叫微任務(wù),macro task 叫宏任務(wù),因為這兩個單詞拼寫太像了 -。- ,所以后面的注釋多用中文表示~

    先來看看源碼中對 micro task macro task 的實現(xiàn):macroTimerFunc、microTimerFunc

    // src/core/util/next-tick.js

    const callbacks = [] // 存放異步執(zhí)行的回調(diào)
    let pending = false // 一個標(biāo)記位,如果已經(jīng)有timerFunc被推送到任務(wù)隊列中去則不需要重復(fù)推送

    /* 挨個同步執(zhí)行callbacks中回調(diào) */
    function flushCallbacks() {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
    copies[i]()
    }
    }

    let microTimerFunc // 微任務(wù)執(zhí)行方法
    let macroTimerFunc // 宏任務(wù)執(zhí)行方法
    let useMacroTask = false // 是否強(qiáng)制為宏任務(wù),默認(rèn)使用微任務(wù)

    // 宏任務(wù)
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    macroTimerFunc = () => {
    setImmediate(flushCallbacks)
    }
    } else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    MessageChannel.toString() === '[object MessageChannelConstructor]' // PhantomJS
    )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = () => {
    port.postMessage(1)
    }
    } else {
    macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
    }
    }

    // 微任務(wù)
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    microTimerFunc = () => {
    p.then(flushCallbacks)
    }
    } else {
    microTimerFunc = macroTimerFunc // fallback to macro
    }

    flushCallbacks 這個方法就是挨個同步的去執(zhí)行callbacks中的回調(diào)函數(shù)們,callbacks中的回調(diào)函數(shù)是在調(diào)用 nextTick 的時候添加進(jìn)去的;那么怎么去使用 micro taskmacro task 去執(zhí)行 flushCallbacks 呢,這里他們的實現(xiàn) macroTimerFunc、microTimerFunc 使用瀏覽器中宏任務(wù)/微任務(wù)的API對flushCallbacks 方法進(jìn)行了一層包裝。比如宏任務(wù)方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) },這樣在觸發(fā)宏任務(wù)執(zhí)行的時候 macroTimerFunc() 就可以在瀏覽器中的下一個宏任務(wù)loop的時候消費這些保存在callbacks數(shù)組中的回調(diào)了,微任務(wù)同理。同時也可以看出傳給 nextTick 的異步回調(diào)函數(shù)是被壓成了一個同步任務(wù)在一個tick執(zhí)行完的,而不是開啟多個異步任務(wù)。

    注意這里有個比較難理解的地方,第一次調(diào)用 nextTick 的時候 pending 為false,此時已經(jīng)push到瀏覽器event loop中一個宏任務(wù)或微任務(wù)的task,如果在沒有flush掉的情況下繼續(xù)往callbacks里面添加,那么在執(zhí)行這個占位queue的時候會執(zhí)行之后添加的回調(diào),所以 macroTimerFunc、microTimerFunc 相當(dāng)于task queue的占位,以后 pending 為true則繼續(xù)往占位queue里面添加,event loop輪到這個task queue的時候?qū)⒁徊?zhí)行。執(zhí)行 flushCallbackspending 置false,允許下一輪執(zhí)行 nextTick 時往event loop占位。

    可以看到上面 macroTimerFuncmicroTimerFunc 進(jìn)行了在不同瀏覽器兼容性下的平穩(wěn)退化,或者說降級策略

    1. macroTimerFuncsetImmediate -> MessageChannel -> setTimeout 。首先檢測是否原生支持 setImmediate ,這個方法只在 IE、Edge 瀏覽器中原生實現(xiàn),然后檢測是否支持 MessageChannel,如果對 MessageChannel 不了解可以參考一下這篇文章,還不支持的話最后使用 setTimeout ;為什么優(yōu)先使用 setImmediate MessageChannel 而不直接使用 setTimeout 呢,是因為HTML5規(guī)定setTimeout執(zhí)行的最小延時為4ms,而嵌套的timeout表現(xiàn)為10ms,為了盡可能快的讓回調(diào)執(zhí)行,沒有最小延時限制的前兩者顯然要優(yōu)于 setTimeout。
    2. microTimerFuncPromise.then -> macroTimerFunc 。首先檢查是否支持 Promise,如果支持的話通過 Promise.then 來調(diào)用 flushCallbacks 方法,否則退化為 macroTimerFunc ;vue2.5之后 nextTick 中因為兼容性原因刪除了微任務(wù)平穩(wěn)退化的 MutationObserver 的方式。

    2.2 nextTick實現(xiàn)

    最后來看看我們平常用到的 nextTick 方法到底是如何實現(xiàn)的:

    // src/core/util/next-tick.js

    export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
    if (cb) {
    try {
    cb.call(ctx)
    } catch (e) {
    handleError(e, ctx, 'nextTick')
    }
    } else if (_resolve) {
    _resolve(ctx)
    }
    })
    if (!pending) {
    pending = true
    if (useMacroTask) {
    macroTimerFunc()
    } else {
    microTimerFunc()
    }
    }
    if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
    _resolve = resolve
    })
    }
    }

    /* 強(qiáng)制使用macrotask的方法 */
    export function withMacroTask(fn: Function): Function {
    return fn._withTask || (fn._withTask = function() {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
    })
    }

    nextTick 在這里分為三個部分,我們一起來看一下;

    1. 首先 nextTick 把傳入的 cb 回調(diào)函數(shù)用 try-catch 包裹后放在一個匿名函數(shù)中推入callbacks數(shù)組中,這么做是因為防止單個 cb 如果執(zhí)行錯誤不至于讓整個JS線程掛掉,每個 cb 都包裹是防止這些回調(diào)函數(shù)如果執(zhí)行錯誤不會相互影響,比如前一個拋錯了后一個仍然可以執(zhí)行。
    2. 然后檢查 pending 狀態(tài),這個跟之前介紹的 queueWatcher 中的 waiting 是一個意思,它是一個標(biāo)記位,一開始是 false 在進(jìn)入 macroTimerFunc、microTimerFunc 方法前被置為 true,因此下次調(diào)用 nextTick 就不會進(jìn)入 macroTimerFunc、microTimerFunc 方法,這兩個方法中會在下一個 macro/micro tick 時候 flushCallbacks 異步的去執(zhí)行callbacks隊列中收集的任務(wù),而 flushCallbacks 方法在執(zhí)行一開始會把 pendingfalse,因此下一次調(diào)用 nextTick 時候又能開啟新一輪的 macroTimerFunc、microTimerFunc,這樣就形成了vue中的 event loop。
    3. 最后檢查是否傳入了 cb,因為 nextTick 還支持Promise化的調(diào)用:nextTick().then(() => {}),所以如果沒有傳入 cb 就直接return了一個Promise實例,并且把resolve傳遞給_resolve,這樣后者執(zhí)行的時候就跳到我們調(diào)用的時候傳遞進(jìn) then 的方法中。

    Vue源碼中 next-tick.js 文件還有一段重要的注釋,這里就翻譯一下:

    在vue2.5之前的版本中,nextTick基本上基于 micro task 來實現(xiàn)的,但是在某些情況下 micro task 具有太高的優(yōu)先級,并且可能在連續(xù)順序事件之間(例如#4521,#6690)或者甚至在同一事件的事件冒泡過程中之間觸發(fā)(#6566)。但是如果全部都改成 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。vue2.5之后版本提供的解決辦法是默認(rèn)使用 micro task,但在需要時(例如在v-on附加的事件處理程序中)強(qiáng)制使用 macro task。

    為什么默認(rèn)優(yōu)先使用 micro task 呢,是利用其高優(yōu)先級的特性,保證隊列中的微任務(wù)在一次循環(huán)全部執(zhí)行完畢。

    強(qiáng)制 macro task 的方法是在綁定 DOM 事件的時候,默認(rèn)會給回調(diào)的 handler 函數(shù)調(diào)用 withMacroTask 方法做一層包裝 handler = withMacroTask(handler),它保證整個回調(diào)函數(shù)執(zhí)行過程中,遇到數(shù)據(jù)狀態(tài)的改變,這些改變都會被推到 macro task 中。以上實現(xiàn)在 src/platforms/web/runtime/modules/events.js 的 add 方法中,可以自己看一看具體代碼。

    剛好在寫這篇文章的時候思否上有人問了個問題 vue 2.4 和2.5 版本的@input事件不一樣 ,這個問題的原因也是因為2.5之前版本的DOM事件采用 micro task ,而之后采用 macro task,解決的途徑參考 < Vue.js 升級踩坑小記> 中介紹的幾個辦法,這里就提供一個在mounted鉤子中用 addEventListener 添加原生事件的方法來實現(xiàn),參見 CodePen。

    3. 一個例子

    說這么多,不如來個例子,執(zhí)行參見 CodePen

    <div id="app">
    <span id='name' ref='name'>{{ name }}span>
    <button @click='change'>change namebutton>
    <div id='content'>div>
    div>
    <script>
    new Vue({
    el: '#app',
    data() {
    return {
    name: 'SHERlocked93'
    }
    },
    methods: {
    change() {
    const $name = this.$refs.name
    this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
    this.name = ' name改嘍 '
    console.log('同步方式:' + this.$refs.name.innerHTML)
    setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
    this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
    this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))
    }
    }
    })
    script>

    執(zhí)行以下看看結(jié)果:

    同步方式:SHERlocked93
    setter前:SHERlocked93
    setter后:name改嘍
    Promise方式:name改嘍
    setTimeout方式:name改嘍

    為什么是這樣的結(jié)果呢,解釋一下:

    1. 同步方式: 當(dāng)把data中的name修改之后,此時會觸發(fā)name的 setter 中的 dep.notify 通知依賴本data的render watcher去 update,update 會把 flushSchedulerQueue 函數(shù)傳遞給 nextTick,render watcher在 flushSchedulerQueue 函數(shù)運行時 watcher.run 再走 diff -> patch 那一套重渲染 re-render 視圖,這個過程中會重新依賴收集,這個過程是異步的;所以當(dāng)我們直接修改了name之后打印,這時異步的改動還沒有被 patch 到視圖上,所以獲取視圖上的DOM元素還是原來的內(nèi)容。
    2. setter前: setter前為什么還打印原來的是原來內(nèi)容呢,是因為 nextTick 在被調(diào)用的時候把回調(diào)挨個push進(jìn)callbacks數(shù)組,之后執(zhí)行的時候也是 for 循環(huán)出來挨個執(zhí)行,所以是類似于隊列這樣一個概念,先入先出;在修改name之后,觸發(fā)把render watcher填入 schedulerQueue 隊列并把他的執(zhí)行函數(shù) flushSchedulerQueue 傳遞給 nextTick ,此時callbacks隊列中已經(jīng)有了 setter前函數(shù) 了,因為這個 cb 是在 setter前函數(shù) 之后被push進(jìn)callbacks隊列的,那么先入先出的執(zhí)行callbacks中回調(diào)的時候先執(zhí)行 setter前函數(shù),這時并未執(zhí)行render watcher的 watcher.run,所以打印DOM元素仍然是原來的內(nèi)容。
    3. setter后: setter后這時已經(jīng)執(zhí)行完 flushSchedulerQueue,這時render watcher已經(jīng)把改動 patch 到視圖上,所以此時獲取DOM是改過之后的內(nèi)容。
    4. Promise方式: 相當(dāng)于 Promise.then 的方式執(zhí)行這個函數(shù),此時DOM已經(jīng)更改。
    5. setTimeout方式: 最后執(zhí)行macro task的任務(wù),此時DOM已經(jīng)更改。

    注意,在執(zhí)行 setter前函數(shù) 這個異步任務(wù)之前,同步的代碼已經(jīng)執(zhí)行完畢,異步的任務(wù)都還未執(zhí)行,所有的 $nextTick 函數(shù)也執(zhí)行完畢,所有回調(diào)都被push進(jìn)了callbacks隊列中等待執(zhí)行,所以在setter前函數(shù) 執(zhí)行的時候,此時callbacks隊列是這樣的:[setter前函數(shù)flushSchedulerQueue,setter后函數(shù),Promise方式函數(shù)],它是一個micro task隊列,執(zhí)行完畢之后執(zhí)行macro task setTimeout,所以打印出上面的結(jié)果。

    另外,如果瀏覽器的宏任務(wù)隊列里面有setImmediate、MessageChannel、setTimeout/setInterval 各種類型的任務(wù),那么會按照上面的順序挨個按照添加進(jìn)event loop中的順序執(zhí)行,所以如果瀏覽器支持MessageChannelnextTick 執(zhí)行的是 macroTimerFunc,那么如果 macrotask queue 中同時有 nextTick 添加的任務(wù)和用戶自己添加的 setTimeout 類型的任務(wù),會優(yōu)先執(zhí)行 nextTick 中的任務(wù),因為MessageChannel 的優(yōu)先級比 setTimeout的高,setImmediate 同理。


    本文是系列文章,隨后會更新后面的部分,共同進(jìn)步~

    1. Vue源碼閱讀 - 文件結(jié)構(gòu)與運行機(jī)制
    2. Vue源碼閱讀 - 依賴收集原理
    3. Vue源碼閱讀 - 批量異步更新與nextTick原理

    網(wǎng)上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學(xué)習(xí)過程中的總結(jié),如果發(fā)現(xiàn)錯誤,歡迎留言指出~

    參考:

    1. Vue2.1.7源碼學(xué)習(xí)
    2. Vue.js 技術(shù)揭秘
    3. 剖析 Vue.js 內(nèi)部運行機(jī)制
    4. Vue.js 文檔
    5. 記錄:window.MessageChannel那些事
    6. MDN - MessageChannel
    7. JS與Node.js中的事件循環(huán)
    8. 黃軼 - Vue.js 升級踩坑小記
    9. Vue nextTick 機(jī)制

    最后



    如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

    1. 點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)

    2. 歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

    3. 關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

    點個在看支持我吧,轉(zhuǎn)發(fā)就更好了


    瀏覽 53
    點贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

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

    手機(jī)掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    无码爱爱网站 | 影音先锋你懂得 | 天天射天天干 | 在线精品热| 黄色操逼爽烦尼玛 |