<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手帶你理解Vue響應式原理

          共 18477字,需瀏覽 37分鐘

           ·

          2020-07-28 16:11

          來源 |?https://www.cnblogs.com/chanwahfung/p/13175515.html


          前言

          響應式原理作為?Vue?的核心,使用數據劫持實現數據驅動視圖。在面試中是經??疾榈闹R點,也是面試加分項。
          本文將會循序漸進的解析響應式原理的工作流程,主要以下面結構進行:
          1. 分析主要成員,了解它們有助于理解流程

          2. 將流程拆分,理解其中的作用

          3. 結合以上的點,理解整體流程

          文章稍長,但部分是代碼,還請耐心觀看。為了方便理解原理,文中的代碼會進行簡化,如果可以請對照源碼學習。

          主要成員

          在響應式原理中,ObserveDep、Watcher?這三個類是構成完整原理的主要成員。
          • Observe,響應式原理的入口,根據數據類型處理觀測邏輯

          • Dep,依賴收集器,屬性都會有一個Dep,方便發(fā)生變化時能夠找到對應的依賴觸發(fā)更新

          • Watcher,用于執(zhí)行更新渲染,組件會擁有一個渲染Watcher,我們常說的收集依賴,就是收集?Watcher

          下面來看看這些類的實現,包含哪些主要屬性和方法。

          Observe:我會對數據進行觀測

          溫馨提示:代碼里的序號對應代碼塊下面序號的講解
          // 源碼位置:/src/core/observer/index.jsclass Observe { constructor(data) { this.dep = new Dep() // 1 def(data, '__ob__', this) if (Array.isArray(data)) { // 2 protoAugment(data, arrayMethods) // 3 this.observeArray(data) } else { // 4 this.walk(data) } } walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) } observeArray(data) { data.forEach(item => { observe(item) }) }}

          1. 為觀測的屬性添加?__ob__?屬性,它的值等于?this,即當前?Observe?的實例

          2. 為數組添加重寫的數組方法,比如:pushunshift、splice?等方法,重寫目的是在調用這些方法時,進行更新渲染

          3. 觀測數組內的數據,observe?內部會調用?new Observe,形成遞歸觀測

          4. 觀測對象數據,defineReactive?為數據定義?get?和?set?,即數據劫持

          Dep:我會為數據收集依賴

          // 源碼位置:/src/core/observer/dep.jslet id = 0class Dep{ constructor() { this.id = ++id // dep 唯一標識 this.subs = [] // 存儲 Watcher } // 1 depend() { Dep.target.addDep(this) } // 2 addSub(watcher) { this.subs.push(watcher) } // 3 notify() { this.subs.forEach(watcher => watcher.update()) }}
          // 4Dep.target = null
          export function pushTarget(watcher) { Dep.target = watcher}
          export function popTarget(){ Dep.target = null}
          export default Dep
          1. 數據收集依賴的主要方法,Dep.target?是一個?watcher?實例

          2. 添加?watcher?到數組中,也就是添加依賴

          3. 屬性在變化時會調用?notify?方法,通知每一個依賴進行更新

          4. Dep.target?用來記錄?watcher?實例,是全局唯一的,主要作用是為了在收集依賴的過程中找到相應的?watcher

          pushTarget?和?popTarget?這兩個方法顯而易見是用來設置?Dep.target的。Dep.target?也是一個關鍵點,這個概念可能初次查看源碼會有些難以理解,在后面的流程中,會詳細講解它的作用,需要注意這部分的內容。

          Watcher:我會觸發(fā)視圖更新

          // 源碼位置:/src/core/observer/watcher.jslet id = 0export class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id // watcher 唯一標識 this.vm = vm this.cb = cb this.options = options // 1 this.getter = exprOrFn this.deps = [] this.depIds = new Set()
          this.get() } run() { this.get() } get() { pushTarget(this) this.getter() popTarget(this) } // 2 addDep(dep) { // 防止重復添加 dep if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } } // 3 update() { queueWatcher(this) }}
          1. this.getter?存儲的是更新視圖的函數

          2. watcher?存儲?dep,同時?dep?也存儲?watcher,進行雙向記錄

          3. 觸發(fā)更新,queueWatcher?是為了進行異步更新,異步更新會調用?run?方法進行更新頁面

          響應式原理流程

          對于以上這些成員具有的功能,我們都有大概的了解。下面結合它們,來看看這些功能是如何在響應式原理流程中工作的。

          數據觀測

          數據在初始化時會通過?observe?方法來調用?Observe
          // 源碼位置:/src/core/observer/index.jsexport function observe(data) { // 1 if (!isObject(data)) { return } let ob; // 2 if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) { ob = data.__ob__ } else { // 3 ob = new Observe(data) } return ob}
          在初始化時,observe?拿到的?data?就是我們在?data?函數內返回的對象。
          1. observe?函數只對?object?類型數據進行觀測

          2. 觀測過的數據都會被添加上?__ob__?屬性,通過判斷該屬性是否存在,防止重復觀測

          3. 創(chuàng)建?Observe?實例,開始處理觀測邏輯

          對象觀測

          進入?Observe?內部,由于初始化的數據是一個對象,所以會調用?walk?方法:
          walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) })}
          defineReactive?方法內部使用?Object.defineProperty?對數據進行劫持,是實現響應式原理最核心的地方。
          function defineReactive(obj, key, value) { // 1 let childOb = observe(value) // 2 const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { // 3 dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal // 4 childOb = observe(newVal) // 5 dep.notify() return value } })}
          1. 由于值可能是對象類型,這里需要調用?observe?進行遞歸觀測

          2. 這里的?dep?就是上面講到的每一個屬性都會有一個?dep,它是作為一個閉包的存在,負責收集依賴和通知更新

          3. 在初始化時,Dep.target?是組件的渲染?watcher,這里?dep.depend?收集的依賴就是這個?watcher,childOb.dep.depend?主要是為數組收集依賴

          4. 設置的新值可能是對象類型,需要對新值進行觀測

          5. 值發(fā)生改變,dep.notify?通知?watcher?更新,這是我們改變數據后能夠實時更新頁面的觸發(fā)點

          通過?Object.defineProperty?對屬性定義后,屬性的獲取觸發(fā)?get?回調,屬性的設置觸發(fā)?set?回調,實現響應式更新。
          通過上面的邏輯,也能得出為什么?Vue3.0?要使用?Proxy?代替?Object.defineProperty?了。Object.defineProperty?只能對單個屬性進行定義,如果屬性是對象類型,還需要遞歸去觀測,會很消耗性能。而?Proxy?是代理整個對象,只要屬性發(fā)生變化就會觸發(fā)回調。

          數組觀測

          對于數組類型觀測,會調用?observeArray?方法:
          observeArray(data) { data.forEach(item => { observe(item) })}
          與對象不同,它執(zhí)行?observe?對數組內的對象類型進行觀測,并沒有對數組的每一項進行?Object.defineProperty?的定義,也就是說數組內的項是沒有?dep?的。
          所以,我們通過數組索引對項進行修改時,是不會觸發(fā)更新的。但可以通過?this.$set?來修改觸發(fā)更新。那么問題來了,為什么?Vue?要這樣設計?
          結合實際場景,數組中通常會存放多項數據,比如列表數據。這樣觀測起來會消耗性能。還有一點原因,一般修改數組元素很少會直接通過索引將整個元素替換掉。例如:
          export default { data() { return { list: [ {id: 1, name: 'Jack'}, {id: 2, name: 'Mike'} ] } }, cretaed() { // 如果想要修改 name 的值,一般是這樣使用 this.list[0].name = 'JOJO' // 而不是以下這樣 // this.list[0] = {id:1, name: 'JOJO'} // 當然你可以這樣更新 // this.$set(this.list, '0', {id:1, name: 'JOJO'}) }}

          數組方法重寫

          當數組元素新增或刪除,視圖會隨之更新。這并不是理所當然的,而是?Vue?內部重寫了數組的方法,調用這些方法時,數組會更新檢測,觸發(fā)視圖更新。這些方法包括:
          • push()

          • pop()

          • shift()

          • unshift()

          • splice()

          • sort()

          • reverse()

          回到?Observe?的類中,當觀測的數據類型為數組時,會調用?protoAugment?方法。
          if (Array.isArray(data)) { protoAugment(data, arrayMethods) // 觀察數組 this.observeArray(data)} else { // 觀察對象 this.walk(data)}
          這個方法里把數組原型替換為?arrayMethods?,當調用改變數組的方法時,優(yōu)先使用重寫后的方法。
          function protoAugment(data, arrayMethods) { data.__proto__ = arrayMethods}
          接下來看看?arrayMethods?是如何實現的:
          // 源碼位置:/src/core/observer/array.js// 1let arrayProto = Array.prototype// 2export let arrayMethods = Object.create(arrayProto)
          let methods = [ 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
          methods.forEach(method => { arrayMethods[method] = function(...args) { // 3 let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // 4 inserted && ob.observeArray(inserted) // 5 ob.dep.notify() return res }})
          1. 將數組的原型保存起來,因為重寫的數組方法里,還是需要調用原生數組方法的

          2. arrayMethods?是一個對象,用于保存重寫的方法,這里使用?Object.create(arrayProto)?創(chuàng)建對象是為了使用者在調用非重寫方法時,能夠繼承使用原生的方法

          3. 調用原生方法,存儲返回值,用于設置重寫函數的返回值

          4. inserted?存儲新增的值,若?inserted?存在,對新值進行觀測

          5. ob.dep.notify?觸發(fā)視圖更新

          依賴收集

          依賴收集是視圖更新的前提,也是響應式原理中至關重要的環(huán)節(jié)。

          偽代碼流程

          為了方便理解,這里寫一段偽代碼,大概了解依賴收集的流程:
          // data 數據let data = { name: 'joe'}
          // 渲染watcherlet watcher = { run() { dep.tagret = watcher document.write(data.name) }}
          // deplet dep = [] // 存儲依賴 dep.tagret = null // 記錄 watcher
          // 數據劫持let oldValue = data.nameObject.defineProperty(data, 'name', { get(){ // 收集依賴 dep.push(dep.tagret) return oldValue }, set(newVal){ oldValue = newVal dep.forEach(watcher => { watcher.run() })
          }})
          初始化:
          1. 首先會對?name?屬性定義?get?和?set

          2. 然后初始化會執(zhí)行一次?watcher.run?渲染頁面

          3. 這時候獲取?data.name,觸發(fā)?get?函數收集依賴。

          更新:
          修改?data.name,觸發(fā)?set?函數,調用?run?更新視圖。

          真正流程

          下面來看看真正的依賴收集流程是如何進行的。
          function defineReactive(obj, key, value) { let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 收集依賴 if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } })}
          首先初始化數據,調用?defineReactive?函數對數據進行劫持。
          export class Watcher { constructor(vm, exprOrFn, cb, options){ this.getter = exprOrFn this.get() } get() { pushTarget(this) this.getter() popTarget(this) }}
          初始化將?watcher?掛載到?Dep.targetthis.getter?開始渲染頁面。渲染頁面需要對數據取值,觸發(fā)?get?回調,dep.depend?收集依賴。
          class Dep{ constructor() { this.id = id++ this.subs = [] } depend() { Dep.target.addDep(this) }}
          Dep.target?為?watcher,調用?addDep?方法,并傳入?dep?實例。
          export class Watcher { constructor(vm, exprOrFn, cb, options){ this.deps = [] this.depIds = new Set() } addDep(dep) { if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } }}
          addDep?中添加完?dep?后,調用?dep.addSub?并傳入當前?watcher?實例。
          class Dep{ constructor() { this.id = id++ this.subs = [] } addSub(watcher) { this.subs.push(watcher) }}
          將傳入的?watcher?收集起來,至此依賴收集流程完畢。
          補充一點,通常頁面上會綁定很多屬性變量,渲染會對屬性取值,此時每個屬性收集的依賴都是同一個?watcher,即組件的渲染?watcher

          數組的依賴收集

          methods.forEach(method => { arrayMethods[method] = function(...args) { let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // 對新增的值觀測 inserted && ob.observeArray(inserted) // 更新視圖 ob.dep.notify() return res }})
          還記得重寫的方法里,會調用?ob.dep.notify?更新視圖,__ob__?是我們在?Observe?為觀測數據定義的標識,值為?Observe?實例。那么?ob.dep?的依賴是在哪里收集的?
          function defineReactive(obj, key, value) { // 1 let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 2 if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } })}
          1. observe?函數返回值為?Observe?實例

          2. childOb.dep.depend?執(zhí)行,為?Observe?實例的?dep?添加依賴

          所以在數組更新時,ob.dep?內已經收集到依賴了。

          整體流程

          下面捋一遍初始化流程和更新流程,如果你是初次看源碼,不知道從哪里看起,也可以參照以下的順序。由于源碼實現比較多,下面展示的源碼會稍微刪減一些代碼

          初始化流程

          入口文件:
          // 源碼位置:/src/core/instance/index.jsimport { initMixin } from './init'import { stateMixin } from './state'import { renderMixin } from './render'import { eventsMixin } from './events'import { lifecycleMixin } from './lifecycle'import { warn } from '../util/index'
          function Vue (options) { this._init(options)}
          initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)
          export default Vue
          _init:
          // 源碼位置:/src/core/instance/init.jsexport function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++
          // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // mergeOptions 對 mixin 選項和傳入的 options 選項進行合并 // 這里的 $options 可以理解為 new Vue 時傳入的對象 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) }
          // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props // 初始化數據 initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
          if (vm.$options.el) { // 初始化渲染頁面 掛載組件 vm.$mount(vm.$options.el) } }}
          上面主要關注兩個函數,initState?初始化數據,vm.$mount(vm.$options.el)?初始化渲染頁面。
          先進入?initState
          // 源碼位置:/src/core/instance/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // data 初始化 initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}
          function initData (vm: Component) { let data = vm.$options.data // data 為函數時,執(zhí)行 data 函數,取出返回值 data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data // 這里就開始走觀測數據的邏輯了 observe(data, true /* asRootData */)}
          observe?內部流程在上面已經講過,這里再簡單過一遍:
          1. new Observe?觀測數據

          2. defineReactive?對數據進行劫持

          initState?邏輯執(zhí)行完畢,回到開頭,接下來執(zhí)行?vm.$mount(vm.$options.el)?渲染頁面:
          $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)}
          mountComponent:
          // 源碼位置:/src/core/instance/lifecycle.jsexport function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { vm.$el = el callHook(vm, 'beforeMount')
          let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}`
          mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag)
          mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { // 數據改變時 會調用此方法 updateComponent = () => { // vm._render() 返回 vnode,這里面會就對 data 數據進行取值 // vm._update 將 vnode 轉為真實dom,渲染到頁面上 vm._update(vm._render(), hydrating) } }
          // 執(zhí)行 Watcher,這個就是上面所說的渲染wacther new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false
          // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm}
          Watcher:
          // 源碼位置:/src/core/observer/watcher.js let uid = 0
          export default class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id this.vm = vm this.cb = cb this.options = options // exprOrFn 就是上面?zhèn)魅氲?updateComponent this.getter = exprOrFn
          this.deps = [] this.depIds = new Set()
          this.get() } get() { // 1. pushTarget 將當前 watcher 記錄到 Dep.target,Dep.target 是全局唯一的 pushTarget(this) let value const vm = this.vm try { // 2. 調用 this.getter 相當于會執(zhí)行 vm._render 函數,對實例上的屬性取值, //由此觸發(fā) Object.defineProperty 的 get 方法,在 get 方法內進行依賴收集(dep.depend),這里依賴收集就需要用到 Dep.target value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } // 3. popTarget 將 Dep.target 置空 popTarget() this.cleanupDeps() } return value }}
          至此初始化流程完畢,初始化流程的主要工作是數據劫持、渲染頁面和收集依賴。

          更新流程

          數據發(fā)生變化,觸發(fā)?set?,執(zhí)行?dep.notify
          // 源碼位置:/src/core/observer/dep.js let uid = 0
          /** * A dep is an observable that can have multiple * directives subscribing to it. */export default class Dep { static target: ?Watcher; id: number; subs: Array;
          constructor () { this.id = uid++ this.subs = [] }
          addSub (sub: Watcher) { this.subs.push(sub) }
          removeSub (sub: Watcher) { remove(this.subs, sub) }
          depend () { if (Dep.target) { Dep.target.addDep(this) } }
          notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { // 執(zhí)行 watcher 的 update 方法 subs[i].update() } }}
          wathcer.update:
          // 源碼位置:/src/core/observer/watcher.js /** * Subscriber interface. * Will be called when a dependency changes. */update () { /* istanbul ignore else */ if (this.lazy) { // 計算屬性更新 this.dirty = true } else if (this.sync) { // 同步更新 this.run() } else { // 一般的數據都會進行異步更新 queueWatcher(this) }}
          queueWatcher:
          // 源碼位置:/src/core/observer/scheduler.js
          // 用于存儲 watcherconst queue: Array = []// 用于 watcher 去重let has: { [key: number]: ?true } = {}/** * Flush both queues and run the watchers. */function flushSchedulerQueue () { let watcher, id
          // 對 watcher 排序 queue.sort((a, b) => a.id - b.id)
          // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null // run方法更新視圖 watcher.run() }}/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // watcher 加入數組 queue.push(watcher) // 異步更新 nextTick(flushSchedulerQueue) }}
          nextTick:
          // 源碼位置:/src/core/util/next-tick.js
          const callbacks = []let pending = false
          function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 // 遍歷回調函數執(zhí)行 for (let i = 0; i < copies.length; i++) { copies[i]() }}
          let timerFunc
          if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) }}
          export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 將回調函數加入數組 callbacks.push(() => { if (cb) { cb.call(ctx) } }) if (!pending) { pending = true // 遍歷回調函數執(zhí)行 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }}
          這一步是為了使用微任務將回調函數異步執(zhí)行,也就是上面的p.then。最終,會調用?watcher.run?更新頁面。
          至此更新流程完畢。

          寫在最后

          如果沒有接觸過源碼的同學,我相信看完可能還是會有點懵的,這很正常。建議對照源碼再自己多看幾遍就能知道流程了。對于有基礎的同學就當做是復習了。
          想要變強,學會看源碼是必經之路。在這過程中,不僅能學習框架的設計思想,還能培養(yǎng)自己的邏輯思維。萬事開頭難,遲早都要邁出這一步,不如就從今天開始。
          簡化后的代碼我已放在?github,有需要的可以看看。

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久精品视频在线观看 | 国产精品视频播放三级 | 西西444www大胆高清图片 | 日本成人电影在线观看 | 亚洲少妇网 |