擼一個vue的雙向綁定

一、前言

提及雙向綁定可能你們都會說:Vue內部經過Object.defineProperty方法屬性攔截的方式,把data對象裏每一個數據的讀寫轉化成getter/setter,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以從簡入繁的形式給你們擼一遍,讓你們瞭解雙向綁定的技術細節。html

二、來一個簡單的版本

讓咱們的數據變得可觀測,實現原理不難,利用Object.defineProperty從新定義對象的屬性描述符。vue

/**
    * 把一個對象的每一項都轉化成可觀測對象
    * @param { Object } obj 對象
    */
    function observable(obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) => {
            defineReactive(obj, key, obj[key])
        })
        return obj;
    }
    /**
    * 使一個對象轉化成可觀測對象
    * @param { Object } obj 對象
    * @param { String } key 對象的key
    * @param { Any } val 對象的某個key的值
    */
    function defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
            get() {
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal) {
                console.log(`${key}屬性被修改了`);
                val = newVal;
            }
        })
    }
    let car = observable({
        'brand': 'BMW',
        'price': 3000
    })
    //測試
    console.log(car.brand);

三、一步一步實現一個觀察者模式的雙向綁定

先給一張思惟導向圖吧(圖盜的,連接:https://www.cnblogs.com/libin-1/p/6893712.html),本文章不涉及Compile部分。
這張圖我就不解釋,咱們先跟着一步一步的把代碼擼出來,再回頭來看這張圖,問題不大。

建議在讀以前必定要了解觀察者模式和發佈訂閱模式以及其區別,一篇簡單的文章總結了一下兩種模式的區別(連接:https://www.cnblogs.com/chenlei987/p/10504956.html),Vue的雙向綁定使用的就是觀察者模式,其中Dep對象就是觀察者的目標對象,而Watcher就是觀察者,而後等待Dep對象的通知更新的,其中update方法是由watcher本身管理的,並不是如發佈訂閱模式由目標對象去管理,在觀察者模式中,目標對象管理的訂閱者列表應該是Watcher自己,而不是事件/訂閱主題。react

3.一、聲明一個Vue類,並將data裏面的數據代理到Vue實例上面。

var Vue = (function (){
    class Vue{
            constructor (options = {}){

                //簡化處理
                this.$options = options;

                let data = (this._data = 
                    typeof this.$options.data == 'function' ? 
                        this.$options.data() 
                        : 
                        this.$options.data);
                Object.keys(data).forEach(key =>{ this._proxy(key) });

                // 監聽數據
                //observe(data);
            }
            _proxy (key){
                //用this這個對象去代理 this._data這個對象裏面的key
                Object.defineProperty(this, key, {
                    configurable: true,
                    enumerable: true,
                    set: (val) => {
                        this._data[key] = val
                    },
                    get: () =>{
                        return this._data[key]
                    }
                })
            }
            
        }
        return Vue;
}
let VM = new Vue({
    data (){
        return {
            a: 1,
            arr: [1,2,3,4,5,6]
        }
    },
});
//說明 _proxy代理成功了
console.log(VM.a);
VM.a = 2;
console.log(VM.a);

3.二、讓data裏面的數據變得可觀測,開啓observe之旅

注:下面我所說的"data裏面"就是指vue實例的data屬性。
上面代碼Vue類的constructor裏面我註釋了一行代碼,下面我取消註釋,而且開始定義observe函數git

// 監聽數據
 observe(data);

在定義observe方法以前,首先明白咱們observe要作什麼?
實參是data數據,咱們要遍歷整個data數據的key,爲data數據的每個key都用Object.defineProperty去從新定義它的 getter和setter函數,從而使其可觀測。面試

class Observer{
            constructor (value){
                this.value = value;
                this.walk(value);
            }
            // 遍歷屬性值並監聽
            walk(value) {
                Object.keys(value).forEach(key => this.convert(key, value[key]));
            }
            // 執行監聽的具體方法
            convert(key, val) {
                defineReactive(this.value, key, val);
            }
        }
        function defineReactive(obj, key, val) {
            const dep = new Dep();
            // 給當前屬性的值添加監聽
            let chlidOb = observe(val);
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                  
                  //do something
                  //  if (Dep.target) {
                    //    dep.depend();
                    //}
                    return val;
                },
                set: newVal => {
                    if (val === newVal) return;
                    val = newVal;
                    
                    //do something
                    // 對新值進行監聽
                    //chlidOb = observe(newVal);
                    // 通知全部訂閱者,數值被改變了
                    //dep.notify();
                },
            });
        }

        function observe(value) {
             // 當值不存在,或者不是複雜數據類型時,再也不須要繼續深刻監聽
            if (!value || typeof value !== 'object') {
                return;
            }
            return new Observer(value);
        }

看到在get和set函數裏面的do something了嗎,能夠理解爲在data裏面的每一個key的設置和獲取都被咱們截取到了,在每一個key的設置和獲取時咱們能夠幹些事情了。好比更數據對應的DOM。
要作什麼呢?
get函數: 從思惟圖圖1能夠看出須要把當前的Watcher添加進Dep對象,等待數據更新,調用回調。
set函數: 數據更新,Dep對象通知全部訂閱的watcher更新,調用回調,更新視圖。數組

3.三、Watcher

先聲明一個Watcher類,用於添加進Dep對象並通知更新視圖使用。瀏覽器

let uid = 0;
        class Watcher {
            constructor(vm, expOrFn, cb) {
                // 設置id,用於區分新Watcher和只改變屬性值後新產生的Watcher
                this.id = uid++;

                this.vm = vm; // 被訂閱的數據必定來自於當前Vue實例
                this.cb = cb; // 當數據更新時想要作的事情
                this.expOrFn = expOrFn; // 被訂閱的數據
                this.val = this.get(); // 維護更新以前的數據
            }
            // 對外暴露的接口,用於在訂閱的數據被更新時,由訂閱者管理員(Dep)調用
            update() {
                this.run();
            }
            addDep(dep) {
                // 若是在depIds的hash中沒有當前的id,能夠判斷是新Watcher,所以能夠添加到dep的數組中儲存
                // 此判斷是避免同id的Watcher被屢次儲存
                //這裏要是不限制重複,你會發如今響應的過程當中,Dep實例下的subs會成倍的增長watcher。多輸入幾個字瀏覽器就卡死了。
                if (!dep.depIds.hasOwnProperty(this.id)) {
                    dep.addSubs(this);
                    dep.depIds[this.id] = dep;
                }
            }
            run() {
                const val = this.get();
                if (val !== this.val) {
                    this.val = val;
                    this.cb.call(this.vm, val);
                }
            }
            get() {
                // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者
                Dep.target = this;
                //注意:在這裏獲取該屬性 從而就觸發了defineProperty的get方法,該watcher已經進入Dep的subs隊列了
                const val = this.vm._data[this.expOrFn]; 
                
                //初始化執行一遍回調
                this.cb.call(this.vm, val);

                //  置空,用於下一個Watcher使用
                Dep.target = null;
                return val;
            }
        }

上面代碼咱們先從constructor看起,接受三個參數,vm當前的vue實例,expOrFn實例化時該watcher實例所 表明/處理 的"data裏面"(‘data裏面’上面有解釋,這裏提醒一下)的哪一個值,cb,回調函數,也就是當數據更新後須要作什麼(天然是更新DOM咯)。
而後在constructor裏面還調用了 this.get()。詳細看一下get函數的定義,兩行代碼須要注意:app

// 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者
Dep.target = this;
//注意:在這裏獲取該屬性 從而就觸發了defineProperty的get方法,該watcher已經進入Dep的subs隊列了
const val = this.vm._data[this.expOrFn];

Dep.target = this;肯定了當前的活動的watcher實例,Dep.target咱們能夠認爲它是一個全局變量,用於存放當前活動的watcher實例。
const val = this.vm._data[this.expOrFn];獲取數據,這句話其實就已經觸發了其自身的getter方法(這點要注意,否則你連流程都理解不通)。
進入了getter方法,也就把當前活動的實例的watcher添加進dep對象等待更新。
添加進Dep對象後,置空,用於下一個Watcher使用 Dep.target = null;ide

3.四、Dep

一直在說dep對象,咱們必定要知道dep對象就是觀察者模式裏面的目標對象,用於存放watcher和負責通知更新的。
下面來定義一個Dep對象,放到class Watcher前面。 注意Dep的做用範圍.函數

class Dep{
            constructor (){
                this.depIds = {}; // hash儲存訂閱者的id,避免重複的訂閱者
                //訂閱者列表  watcher實例列表
                this.subs = [];
            }
            depend (){
                Dep.target.addDep(this);//至關於調用this.addSubs 將 watcher實例添加進訂閱列表 等待通知更新
                //原本按照咱們的理解,在denpend裏面是須要將watcher添加進 Dep對象, 等待通知更新的,因此應該調用 this.addSubs(Dep.target)
                //可是因爲須要解耦 因此 先調用 watcher的addDep 在addDep中調用Dep實例的addSubs
                //簡化理解就是 將 watcher實例添加進訂閱列表 等待通知更新
            }
            addSubs (sub) {
                //這裏的sub確定是watcher實例
                this.subs.push(sub);
            }
            notify (){
                //監聽到值的變化,通知全部訂閱者watcher更新
                this.subs.forEach((sub) =>{
                    sub.update();
                });
            }
        }
         Dep.target = null;//存儲當前活動的watcher

再改改defineReactive,把註釋打開

function defineReactive(obj, key, val) {
            const dep = new Dep();
            // 給當前屬性的值添加監聽
            let chlidOb = observe(val);
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中
                    // target指向一個Watcher實例,每一個Watcher都是一個訂閱者
                    // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法
                    if (Dep.target) {
                        dep.depend();
                    }
                    return val;
                },
                set: newVal => {
                    if (val === newVal) return;
                    val = newVal;
                    // 對新值進行監聽
                    chlidOb = observe(newVal);
                    // 通知全部訂閱者,數值被改變了
                    dep.notify();
                },
            });
        }

而後起一個watcher來監聽

3.五、讓數據響應起來

先給Vue暴露一個方法 $watcher 能夠調用實例化Watcher。

class Vue{
            constructor (options = {}){

                //簡化處理
                this.$options = options;

                let data = (this._data = 
                    typeof this.$options.data == 'function' ? 
                        this.$options.data() 
                        : 
                        this.$options.data);
                Object.keys(data).forEach(key =>{ this._proxy(key) });

                // 監聽數據
                observe(data);
            }
            // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者
            $watch(expOrFn, cb) {
                //property須要監聽的屬性  cb在監聽到更新後的回調
                new Watcher(this, expOrFn, cb);
            }
            _proxy (key){
                //用this這個對象去代理 this._data這個對象裏面的key
                Object.defineProperty(this, key, {
                    configurable: true,
                    enumerable: true,
                    set: (val) => {
                        this._data[key] = val
                    },
                    get: () =>{
                        return this._data[key]
                    }
                })
            }
        }

3.六、測試: 聲明一個實例

html部分

<h3>Vue雙向綁定</h3>
    <input type="text" id="input">
    <p id="react"></p>
    <h3>Vue數組雙向綁定</h3>
    <input type="text" id="arr-input">
    <p id="arr-reat"></p>
let reactElement = document.querySelector("#react");
    let input = document.getElementById('input');
    input.addEventListener('keyup', function (e) {
        VM.a = e.target.value;
    });

    VM.$watch('a', val => reactElement.innerHTML = val); //監聽屬性 a 當a發生改變時


    //數組的響應並不能實現
    let arrReactElement = document.querySelector("#arr-reat");
    let arrInput = document.getElementById('arr-input');
    arrInput.addEventListener('keyup', function (e) {
        VM.arr.push(e.target.value);
        console.log(VM.arr);
    });
    VM.$watch('arr', val => arrReactElement.innerHTML = val); //監聽屬性 a 當a並無發生改變時

VM.$watch就能夠實例化一個watcher,從而去劫持data裏面某個屬性的改變,在改變時調用回調函數。
數組的改變並無實現。上面的代碼見https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0.html

四、對數組的支持

在說這個以前咱們先去看一看vue官網對於數組更新檢測的說明,連接:https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B


總的來講,對於數組支持更新的只是數組原型上的方法,對於vm.items[index] = newValue是不支持的。
其實Object.defineProperty對於數組都是不支持的,根據消息vue3.0用的proxy對於數組獲得了完美的支持,可是兼容性不怎麼樣。
既然vue實現了對數組原型方法的支持,那麼咱們也來讓咱們的例子對數組方法也支持吧。
原理不難,vue對於全部的數組原型方法都寫了一層hack,讓其支持更新。那麼下面咱們就一步一步來實現。

4.一、準備一套數組原型方法的hack

/**
        * Define a expOrFn.
        */
        function def(obj, key, val, enumerable) {
            Object.defineProperty(obj, key, {
                value: val,
                enumerable: !!enumerable,
                writable: true,
                configurable: true
            });
        }

        //數組改變的監聽
        var arrayProto = Array.prototype;
        var arrayMethods = Object.create(arrayProto);
        var methodsToPatch = [
            'push',
            'pop',
            'shift',
            'unshift',
            'splice',
            'sort',
            'reverse'
        ];
        /**
        * Intercept mutating methods and emit events
        */
        methodsToPatch.forEach(function (method) {
            // cache original method
            var original = arrayProto[method];
            def(arrayMethods, method, function mutator() {
                var args = [], len = arguments.length;
                while (len--) args[len] = arguments[len];

                var result = original.apply(this, args);
                var ob = this.__ob__;
                var inserted;
                switch (method) {
                    case 'push':
                    case 'unshift':
                        inserted = args;
                        break
                    case 'splice':
                        inserted = args.slice(2);
                        break
                }
                if (inserted) { ob.observeArray(inserted); }
                // notify change
                ob.dep.notify(); //調用該數組下的 __ob__.dep 詳細可見class Observer的constructor裏的註釋
                return result
            });
        });

上面代碼準備了一個arrayMethods的對象,它繼承自Array.prototype,而且對methodsToPatch裏面的方法進行了改寫,後面咱們會把arrayMethods這個對象掛到"data裏面"每一個數組下,讓該數組調用數組原生方法,好比[].push其實調用的是arrayMethods裏面被改寫的方法,從而在該數組改變時獲取到該數組的更新。
下面開始掛載arrayMethods對象,在掛載我以前咱們看到有一個this.__ob__屬性,這裏的this指向要觀測的數組。這個__ob__就是前面的observe對象,而且每一個observe下面還有一個dep對象。下面咱們來理清楚這層關係。

class Observer{
    constructor (value){
        this.value = value;

        //下面兩行代碼雖然很簡單,可是咱們須要從這裏理清楚關係
        //假如 有數據如 {a: [1,2,3], b: 1},  而後調用oberve(vm.a),vm當前vue實例
        //會自動掛載 __ob__ 和 __ob__.dep
        // 那麼對數組a進行oberserve的對象就是a.__ob__, 它所對應的dep對象就是 a.__ob__.dep
        //詳細使用能夠在對數組的方法進行hack的時候 使用到
        def(value, '__ob__', this);//讓被監聽的數據都帶上一個不可枚舉的屬性 __ob__ 表明observe對象
        this.dep = new Dep();//首先每一個oberserve實例下有一個dep對象
        
        
        //在這裏處理數組
        if (Array.isArray(value)){
            //調用數組的hack方法, 讓數組也能被監聽  arrayMethods
            var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
            for (var i = 0, l = arrayKeys.length; i < l; i++) {
                var key = arrayKeys[i];
                def(value, key, arrayMethods[key]);
            }
        }   
        else{
            //對象 遍歷key  添加監聽
            this.walk(value);
        }
    }
    //Observer的其餘方法
    //...
}

上面代碼首先給每一個值掛載__ob__屬性(不可枚舉),而後給每一個Obeserve對象掛載Dep對象。而後根據value的類型,若是是數組就會掛載arrayMethods方法。
如今咱們來理清數組在哪裏依賴收集,在哪裏通知更新的。
在對數組hack的方法裏面(上上一段代碼)有一段ob.dep.notify(); 這裏通知更新,因此依賴收集也必定要收集到value.__ob__.dep對象裏面,兩個dep對象應該是相同的,下面咱們來看看依賴收集寫在哪裏的。

function defineReactive(obj, key, val) {
            const dep = new Dep();
            // 給當前屬性的值添加監聽
            let childOb = observe(val);
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中
                    // target指向一個Watcher實例,每一個Watcher都是一個訂閱者
                    // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法
                    if (Dep.target) {
                        dep.depend();
                        if (childOb) {
                            childOb.dep.depend();
                            if (Array.isArray(val)) {
                                dependArray(val);
                            }
                        }
                    }
                    return val;
                },
                set: newVal => {
                    if (val === newVal) return;
                    val = newVal;
                    // 對新值進行監聽
                    childOb = observe(newVal);
                    // 通知全部訂閱者,數值被改變了
                    dep.notify();
                },
            });
        }
        function dependArray(value) {
            for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
                e = value[i];
                e && e.__ob__ && e.__ob__.dep.depend();
                if (Array.isArray(e)) {
                    dependArray(e);
                }
            }
        }

數組雖然在Object.defineProperty裏面set方法沒法響應,可是get方法是沒有問題的,因此在數組get的時候,判斷val若是是array,會調用value.__ob__.dep.depend進行依賴收集。與上面依賴通知使用了贊成個dep對象,也就是掛載在自身的__ob__.dep。
寫到這裏咱們就徹底實現對數組原生方法的支持了。
下面看一下效果 代碼地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9%E6%95%B0%E7%BB%84%E7%9A%84%E6%94%AF%E6%8C%81.html

4.2測試代碼

html部分

<h3>Vue雙向綁定</h3>
    <input type="text" id="input">
    <p id="react"></p>
    <h3>Vue數組雙向綁定</h3>
    <input type="text" id="arr-input">
    <p id="arr-reat"></p>
    <h3>Vue對nextTick實現</h3>
    <button id="addBtn">加100000次</button>
    <p id="react-tick"></p>
let reactElement = document.querySelector("#react");
    let input = document.getElementById('input');
    input.addEventListener('keyup', function (e) {
        VM.a = e.target.value;
    });

    VM.$watch('a', val => reactElement.innerHTML = val); //監聽屬性 a 當a發生改變時


    //數組的響應並能實現
    let arrReactElement = document.querySelector("#arr-reat");
    let arrInput = document.getElementById('arr-input');
    arrInput.addEventListener('keyup', function (e) {
        VM.arr.push(e.target.value);
        console.log(VM.arr);
    });
    VM.$watch('arr', val => arrReactElement.innerHTML = val); //監聽屬性 a 當a發生改變時


    let reactTick = document.querySelector("#react-tick");
    VM.$watch('tickData', val => {
        console.log(val);
        reactTick.innerHTML = val;
    }); //監聽屬性 a 當a發生改變時
    document.querySelector('#addBtn').addEventListener('click', function () {
        for (let i = 0; i < 100000; i++) {
            VM.tickData = i;
        }
    }, false)

效果:

五、對nextTick的支持

vue官網對nextTick的解釋:

nextTick若是本身實現就是在下一個envet loop執行,不在本次同步任務中執行。
本身實現一個簡單的:

//nextTick的實現
let callbacks = [];
let pending = false;

function nextTick(cb) {
    callbacks.push(cb);
    if (!pending) {
        pending = true;
        setTimeout(flushCallbacks, 0);
    }
}
function flushCallbacks() {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

簡單理解: 在本次event loop中收集cb(任務),放到下一個event loop去執行。 關於不知道event loop的能夠參考這篇文章:https://www.cnblogs.com/chenlei987/p/10479433.html,我總結的很簡單。我參考的http://www.ruanyifeng.com/blog/2014/10/event-loop.html。
在理解event loop的同時也須要同時瞭解 microtask和macrotask的區別。
好了言歸正傳,在vue的'data裏面'某個屬性發生了改變,並被觀測到後,調用了watcher.update,並不會當即調用watcher.run去更新視圖,它會通過nextTick以後再更新視圖,提及來有點牽強。
仍是第四部=步的代碼,沒有實現對nextTick的優化。
代碼:

<h3>Vue雙向綁定</h3>
    <input type="text" id="input">
    <p id="react"></p>
    <h3>Vue數組雙向綁定</h3>
    <input type="text" id="arr-input">
    <p id="arr-reat"></p>
    <h3>Vue對nextTick實現</h3>
    <button id="addBtn">加1000次</button>
    <p id="react-tick"></p>

let reactTick = document.querySelector("#react-tick");
    VM.$watch('tickData', val => {
        console.log(val);
        reactTick.innerHTML = val;
    }); //監聽屬性 a 當a發生改變時
    document.querySelector('#addBtn').addEventListener('click', function () {
        for (let i = 0; i < 1000; i++) {
            VM.tickData = i;
        }
    }, false)

效果是這樣的:

如今的效果是VM.tickData加1000次,那麼cb(回調)就會調用1000次,這樣是很是影響性能的,咱們想要的效果是不管VM.tickData在本次event loop加多少次,都不會觸發回調,只須要在VM.tickData加完以後,觸發一次最終的cb(回調)就ok了。
下面咱們就來實現這種優化,代碼很少。

//nextTick的實現
            let callbacks = [];
            let pending = false;

            function nextTick(cb) {
                callbacks.push(cb);
                if (!pending) {
                    pending = true;
                    setTimeout(flushCallbacks, 0);
                }
            }
            function flushCallbacks() {
                pending = false;
                const copies = callbacks.slice(0);
                callbacks.length = 0;
                for (let i = 0; i < copies.length; i++) {
                    copies[i]();
                }
            }
            
            let has = {};
            let queue = [];
            let waiting = false;
            function queueWatcher(watcher) {
                const id = watcher.id;
                if (has[id] == null) {
                    has[id] = true;
                    queue.push(watcher);

                    if (!waiting) {
                        waiting = true;
                        nextTick(flushSchedulerQueue);
                    }
                }
            }
            function flushSchedulerQueue() {
                let watcher, id;

                for (index = 0; index < queue.length; index++) {
                    watcher = queue[index];
                    id = watcher.id;
                    has[id] = null;
                    watcher.run();
                }

                waiting = false;
            }

而後更改Watcher裏面的update方法,並不直接調用watcher.run,而是通過queueWatcher控制

update() {
    queueWatcher(this);
    // this.run();
}

代碼地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9nextTck%E7%9A%84%E6%94%AF%E6%8C%81.html

六、總結

若是面試官問我關於雙向綁定的問題,從這三個方面去回答,Object.definproperty,觀察者模式,nextTick,固然,你須要把這三個點聯繫起來去描述,相信我你把上面的看懂了,聯繫起來徹底沒問題的,你是最棒的!

七、本文參考:

https://codepen.io/xiaomuzhu/pen/jxBRgj/ https://www.jianshu.com/p/2df6dcddb0d7

相關文章
相關標籤/搜索