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就可以讲完了,欢迎指正,大牛勿喷!!

发布了128 篇原创文章 · 获赞 113 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/vv_bug/article/details/102519987