Vue源碼解析之(computed&watch&觀察者模式)

前言:好久沒寫博客了,今年確實有點小忙,學習跟工作都很充實,身邊也有小夥伴也經常說:技術是學不完的,能不能專一搞點自己喜歡的東西呢?學完新技術然後過幾年又沒了,豈不是白學了呢? 哈哈~ 說的確定有一點道理啊,但是沒辦法啊,新技術還是得去了解啊,至少要去看看別人的文檔跟設計思想吧,也不怕小夥伴笑哈,最近還在補js跟css基礎知識,學無止境,加油吧~騷年!

前面有寫過兩篇vue的源碼的文章,有興趣的童鞋可以去看看,Vue源碼解析(一)Vue源碼解析二(render&mount)接觸vue也有蠻長一段時間了,不得不說,vue的設計思想跟整體架構還是很牛逼的,所以帶着崇拜跟學習的心態我們一起看看vue的computed跟watch屬性。

一、觀察者模式

在說computed跟watch之前,我們先來認識一種設計模式 “觀察者模式”,觀察者模式分爲“Subject主題”、"Observer觀察者 ",在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實現事件處理系統。

字面意思比較抽象,我們直接上代碼,還記得在大學的時候老師就講過“觀察者模式”,雖然舉的例子不很貼切,但是還算是比較好的詮釋,老師跟我們講了一個 “小美與她的兩個男盆友” 的故事。

第一步:定義subject

export default class Subject {
    constructor(name,desc) {
        this.name = name;
        this.desc = desc;
    }
    add() {
        throw new Error("this method should be overrided!!");
    }

    notify() {
        throw new Error("this method should be overrided!!");
    }

    cry() {
        throw new Error("this method should be overrided!!");
    }
}

第二步:定義Observer

export default class Observer {
    constructor(name,desc) {
        this.name = name;
        this.desc = desc;
    }

    say() {
        throw new Error("this method should be overrided!!");
    }
}

第三步:定義具體的subject對象(girl女孩)

import ISubject from './Subject';

export default class Girl extends ISubject {
    constructor(name, desc) {
        super(name, desc);
        this.boys = [];
    }

    add(boy) {
        this.boys.push(boy);
    }

    notify() {
        this.boys.forEach((boy) => {
            boy.say();
        });
    }

    cry() {
        console.log(this.name + " : " + this.desc);
        this.notify();
    }
}

第四步:定義具體的observer對象(boy男孩)

import Observer from './Observer';

export default class Boy extends Observer {
    constructor(name, desc) {
        super(name, desc);
    }

    say() {
        console.log(this.name + ": " + this.desc);
    }
}

測試:

import Girl from './Girl';
import Boy from './Boy';

const girl1 = new Girl("小美", "今天心情很不好,哄不好的那種~");

const boy1 = new Boy("小黑", "親愛的,你購物車需要清一下了!");
const boy2 = new Boy("小白", "Darling 逛街去!");

girl1.add(boy1);
girl1.add(boy2);
girl1.cry();

結果:
在這裏插入圖片描述

小美心情不好就給兩個男盆友打電話,兩個男盆友都聽到了然後各種安慰。

好啦! 故事也完了,我們進入今天的主題“vue的computed跟watch”

二、computed&watch

做過vue的小夥伴應該都知道computed的用法,沒用過的可以去看看vue的官網,我就不囉嗦了,先定義一下我們最終需要實現的效果:

import MyVue from './computed/MyVue';

const myVue = new MyVue({
    data() {
        return {
            firstName: "Yasin",
            lastName: "Yin"
        };
    },
    computed: {
        fullName() {
            const fullName = this.firstName + "-" + this.lastName;
            console.log("fullName", fullName);
            return fullName;
        }
    },
    watch: {
        firstName(oldValue, newValue) {
            console.log("firstName值改變了 oldValue=> " + oldValue + " newValue=> " + newValue);
        }
    }
});
setTimeout(() => {
    myVue.firstName = "Tom";
}, 100);

小夥伴應該很熟悉這個代碼吧,沒錯! 這也是官網對computed的例子,現在我們就簡單的來實現一下,小夥伴跟緊了哦~~

第一步:定義一個Dep
Dep裏面應該有什麼內容呢?小夥伴猜應該能猜到,我們demo是這樣定義的:

fullName() {
            const fullName = this.firstName + "-" + this.lastName;
            console.log("fullName", fullName);
            return fullName;
        }

所以當firstName跟lastName變化的時候,我們的fullName方法需要執行,所以這裏的Dep就相當於我們上面所說的“女孩”,只不過這裏面有兩個女孩~~ (哈哈,現在變成男孩腳踏兩隻船了)

Dep.js:

export default class Dep {
    constructor() {
        this.subs = [];
    }

    /**
     * 添加觀察者
     * @param sub
     */
    addSub(sub) {
        if (!this.subs.includes(sub)) {
            this.subs.push(sub);
        }
    }

    /**
     * 與觀察者建立關係
     */
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    /**
     * 通知所有的觀察者
     */
    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}
//當前待申請的觀察者
Dep.target = null;

//設置當前觀察者(成爲觀察者是需要申請的))
export function pushTarget(target) {
    Dep.target = target;
}

第二步:定義一個Watcher對象

Watcher相當於我們前面說的男孩的概念,會有自己的名字信息,跟具體的表達方式等等。

/**
 * @author YASIN
 * @version [React-Native V01, 2019/10/11]
 * @date 2019/10/11
 * @description Watcher
 */
import Dep, {pushTarget} from './Dep';

export default class Watcher {
    constructor(vm, expOrFn, cb, lazy = false) {
        this.vm = vm; //當前vue實例
        const watchers = vm._watchers = []; //vue實例中所有的觀察者
        watchers.push(this);
        this.cb = cb; //被觀察的事物發生變化時候的回調(男孩的say方法)
        this.deps = []; //觀察的事物的集合(女孩)
        this.getter = expOrFn; //獲取觀察者的信息(男孩的名字跟表達)
        if (!lazy) { //是否立馬申請成爲觀察者(還說是等到女孩子哭的時候去撿漏)
            this.value = this.get();
        }
    }

    /**
     * 申請成爲觀察者
     */
    get() {
        pushTarget(this);
        let value;
        const vm = this.vm;
        value = this.getter.call(vm, vm);
        return value;
    }

    /**
     * 建立與被觀察對象的關係
     */
    addDep(dep) {
        if (!this.deps.includes(dep)) {
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

    /**
     * 展示觀察者具體動作
     */
    update() {
        this.run();
    }

    /**
     * 獲取觀察者的具體動作
     */
    run() {
        const value = this.get();
        if (
            value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            this.cb.call(this.vm, oldValue, value);
        }
    }

    /**
     * 列出自己所觀察的所有被觀察的事物.
     */
    depend() {
        let i = this.deps.length;
        while (i--) {
            this.deps[i].depend();
        }
    }
}

第三步:定義響應式可以觀察的對象Observer

/**
 * @author YASIN
 * @version [React-Native V01, 2019/10/11]
 * @date 2019/10/11
 * @description observer
 */
import Dep from './Dep';

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(obj, key, val) {
    //創建一個可觀察的對象
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = val;
            //是否有需要申請的觀察者
            if (Dep.target) {
                //建立與觀察者的聯繫
                dep.depend();
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = val;
            // eslint-disable-next-line no-self-compare
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return;
            }
            val = newVal;
            //通知所有的觀察者
            dep.notify();
        }
    });
}

export class Observer {
    constructor(value) {
        this.walk(value);
    }

    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    }
}

第四步:封裝Vue類

爲了跟vue不衝突我們創建一個叫MyVue的類,然後創建構造方法:

export default class MyVue {
	constructor(opts) {
        if (opts.data) {
            this.initData(opts.data);
        }
        if (opts.computed) {
            this.initComputed(opts.computed);
        }
        if (opts.watch) {
            this.initWatch(opts.watch);
        }
    }
     /**
     * 初始化data
     * @param value
     */
    initData(value) {
    }
    /**
     * 初始化computed
     * @param computed
     */
    initComputed(computed) {
    }
    /**
     * 初始化watch
     * @param watch
     */
    initWatch(watch) {
    }
}

我們在vue中都知道,當我們在data中聲明對象的時候,vue會把data對象的所有屬性值都映射到vm上,所以在vue中我們可以直接使用this.xxxx就可以訪問data的屬性:

 /**
     * 初始化data
     * @param value
     */
    initData(value) {
        this._data = value(); //獲取data的值付給vm的_data
        const keys = Object.keys(this._data);
        let i = keys.length;
        while (i--) { //遍歷所有的data屬性
            const key = keys[i];
            //代理到vm上,
            this.proxy(this, `_data`, key);
        }
        new Observer(this._data);
    }

    /**
     * 代理
     * @param target vm
     * @param sourceKey 目標值
     * @param key 屬性值
     */
    proxy(target, sourceKey, key) {
        sharedPropertyDefinition.get = function proxyGetter() {
            return this[sourceKey][key];
        };
        sharedPropertyDefinition.set = function proxySetter(val) {
            this[sourceKey][key] = val;
        };
        //定義響應式屬性
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

這樣我們就可以把data函數中返回的對象映射到vm的實例上了~

然後我們初始化computed:

/**
     * 初始化computed
     * @param computed
     */
    initComputed(computed) {
        const watchers = this._computedWatchers = Object.create(null);
        //遍歷所有的computed
        for (const key in computed) {
            //對每個computed創建一個監聽者
            watchers[key] = new Watcher(
                this,
                computed[key],
                noop,
                false
            );
            //把computed的屬性也映射到vm實例上
            if (!(key in this)) {
                this.defineComputed(this, key);
            }
        }
    }

    /**
     * 定義computed
     * @param target vm
     * @param key 屬性
     */
    defineComputed(target, key) {
        sharedPropertyDefinition.set = noop;
        sharedPropertyDefinition.get = this.createComputedGetter(key);
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

    /**
     * 當使用computed的內容的時候,我們需要知道有誰需要成爲觀察者(誰關注了我),
     * 然後建立與觀察者的聯繫
     * 把computed的"所有女朋友"介紹給"關注computed"的人
     * @param key 男孩名字
     * @returns {computedGetter}
     */
    createComputedGetter(key) {
        return function computedGetter() {
            const watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
                //當使用computed的內容的時候,我們需要知道有誰需要成爲觀察者(誰關注了我)
                if (Dep.target) {
                    //然後建立與觀察者的聯繫
                    //把computed的"所有女朋友"介紹給"關注computed"的人
                    watcher.depend();
                }
                //返回自己的值
                return watcher.value;
            }
        };
    }

這裏強調一下“當有人也觀察了computed”的時候,computed需要把自己所觀察的事物都介紹給自己的粉絲(另外一個computed)。

最後實現我們的watch:

watch是最簡單的部分了,也就是創建一個觀察者,然後關注某個人就可以了:

 /**
     * 初始化watch
     * @param watch
     */
    initWatch(watch) {
        //遍歷所有的watcher
        for (const key in watch) {
            //創建一個watcher
            const watcher = new Watcher(this, () => {
                return this[key]; //需要觀察的事物名稱
            }, watch[key] //回調地址
            );
            watch[key].call(this, watcher.value); //立馬執行一次
        }
    }

好啦!! 寫了一大堆東西,現在我們來測試一下我們的代碼~~

import MyVue from './computed/MyVue';

const myVue = new MyVue({
    data() {
        return {
            firstName: "Yasin",
            lastName: "Yin"
        };
    },
    computed: {
        fullName() {
            const fullName = this.firstName + "-" + this.lastName;
            console.log("fullName", fullName);
            return fullName;
        }
    },
    watch: {
        firstName(oldValue, newValue) {
            console.log("firstName值改變了 oldValue=> " + oldValue + " newValue=> " + newValue);
        }
    }
});


做過vue的小夥伴應該是可以知道答案的吧:

在這裏插入圖片描述

然後我們模擬一下當firstName改變的時候fullName會不會變:

setTimeout(() => {
    myVue.firstName = "Tom";
}, 100);

結果:
在這裏插入圖片描述
好啦~~整個過程結合demo就算是講完了,下面看一下整個流程。

當有computed的時候—>爲每個computed的屬性(fullName)創建成爲監聽者,同時申請

自己成爲監聽者—>被觀察的事物(firstName、lastName)看到有人需要成爲監聽者就立馬建

立與他的關係—>fullName成功的成爲了監聽者—>當被觀察的事物變化的時候firstName–>

通知監聽者fullName—>監聽者fullName執行回調方法獲取最新值

好啦~~ computed跟watch就可以講完了,歡迎指正,大牛勿噴!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章