Vue实现数据双向绑定的设计模式到底叫什么?

面试官:“请问 vue 实现双向绑定的原理是什么?”
我:“是利用 Object.defineproperty 进行数据劫持,实现观察者模式......”
面试官:“是观察者模式么?”
我:“?是啊”
面试官:“那你能和我说一下观察者模式和发布订阅模式一样么?不一样的话区别又在哪?”
我:“???”

前言

不知道各位在面试中/被面试中有没有遇到以上的场景,观察者模式和发布订阅模式到底只是翻译不同,还是真的不是一个东西呢? vue 使用的又到底是哪种呢?让我们一起探讨一下。

为什么要使用观察者/发布订阅模式

假如你是一位托儿所的老师,当孩子们放学来到你这里时,你需要看着他们一个个的写作业,他们来自不同的学校,年纪也不尽相同,这让你的工作非常繁琐,就像:

    const children = [
        {
            name: '张三',
            homework: '三张数学卷子'
        },
        {
            name: '李四',
            homework: '一张数学卷子加两张语文卷子'
        },
        {
            name: '王二麻子',
            homework: '上课捣乱罚抄课本'
        },
        // ......
    ];
    function yourJob() {
        children.map(({homework}) => console.log(homework));
    }
    if(time === '17:00') {
        yourJob();
    }

“这样看上去好像还好嘛”

不过实际场景不可能这么简单,真实的情况很可能是:

    const children = [
        {
            name: '张三',
            homework: '三张数学卷子',
            sport: '打一场篮球'
        },
        {
            name: '李四',
            homework: '一张数学卷子加两张语文卷子',
            music: '学会唱鸡你太美'
        },
        {
            name: '王二麻子',
            homework: '上课捣乱罚抄课本',
            doHomework: false,
            playGame: '手游抽卡',
            extendActivity: '计划明天上课的捣乱计划'
        },
        // ......
    ];

“哦我的天啊,考虑到每个孩子要做的不仅不同,每天都是变化的,就算我今天努力按照情况制定好计划,明天又要推倒重来!
要是这些孩子懂事点就好了,我就不用这么费心了......”

怎么才叫懂事呢?比起挨个盯着孩子们完成任务,如果一声令下,他们都能自己去完成自己该做的,是不是就太好了?
而以上的痴心妄想写出来就像这样:

    const yourJobMap = (function () {
        let children = [];
        return {
            doYourJob() {
                children.forEach(({doOwnJob}) => doOwnJob());
            },
            comeIn(...args) {
                children = children.concat(args);
            },
            getOut(...args) {
                children = children.filter(child => !args.find(curChild => curChild === child));
            }
        };
    })();

    const {doYourJob, comeIn, getOut} = yourJobMap;
    // 孩子们来了
    comeIn(children1);
    // 有的孩子又走了
    getOut(children2);
    // 通知在场的孩子们干活啦
    doYourJob();

“这也太方便了!因为孩子们都懂事,自己知道自己要做什么,这样我只要通知他们开始工作就好,并不需要关心他们具体要做什么!”

正是如此,观察者也好,发布订阅也好,他们都解决了一个问题:对于多个不同对象基于同一个对象的变化而执行某些不同的操作时,如何更好的维护代码,也就是降低耦合——即实现对设计模式六大原则中 limit 原则(最少知道原则,尽量降低类与类之间的耦合)的体现。

什么是观察者模式

上述例子就是最简单的观察者模式,即观察者对象( observer,比如上述的孩子 )可以向某个主题( subject ,比如上述的托儿所)注册、注销等,当有事件发生的时候(到时间了,该做作业了),观察者可以接收到通知去进行自己对应的处理。

在前端实际生产中,最常用的观察者模式的实践应该是 EventListener 了吧:

    // 简单的事件监听对象
    const eventListener = (function () {
        const events = {};
        return {
            // 增加监听事件
            addEventListener(eventName, callback) {
                if(events[eventName]) {
                    events[eventName].push(callback);
                } else {
                    events[eventName] = [callback];
                }
            },
            // 移除监听事件
            removeEventListener(eventName, callback) {
                if(events[eventName]) {
                    events[eventName] = events[eventName].filter(fun => fun !== callback);
                } else {
                    events[eventName] = [];
                }
            },
            // 触发事件
            triggerEvent(eventName) {
                if(event[eventName]) {
                    event[eventName].forEach(callback => callback());
                }
            }
        };
    })();

什么是发布订阅模式

Observer vs Pub-Sub

不得不说,如果仅仅是上述的观察者模式,已经足够应对我们的“托儿所”问题了,如果发布订阅模式和观察者模式不同的话,它和观察者模式的区别在哪呢?又是为了解决什么问题呢?
再用托儿所的例子举例,现在是疫情隔离期间,作为线下的托儿所没有办法只能歇业,老师没法接触到学生了,尽管孩子们懂事,但毕竟还是需要有人监督的,那老师该怎么做呢?
老师想到一个办法,他把所有孩子的家长拉进了一个群,每天定时在群里发消息,告诉家长现在孩子要写作业/要锻炼了,然后再由家长面对面地监督孩子。渐渐的他发现,他并不知道哪些孩子真的收到了他的指令去行动了,孩子们也不知道老师到底通知了些什么,老师和孩子,仿佛从没接触过,只有家长在负责两头传话。
如果说观察者模式中, Subject 和 Observer 是一种松耦合状态,那在发布订阅模式中, Publisher 和 Subscriber 就是解耦的,它们两者之间的联系全部通过家长(?)来实现。

Observer vs Pub-Sub

什么是“家长”

在上述举例中的“家长”角色在生活中无处不在,购物链中的超市,企业中的hr等等,毕竟这个世界总是有“中间商赚差价”的。
这里就要引入另一个设计模式——代理模式了。
这个命名非常贴切,就像明星的经纪人一样,你看到的明星都是经纪人包装过的样子,黑粉的言语也会被经纪人公关处理,尽量不会让明星本人看到。你看似每天在微博上和你的偶像互动,实际上你和你的偶像是完全解耦的 hhh......
在实际生产中,es6已经为前端开发者提供了一套代理模式的 API —— Proxy ,大家可以通过 Proxy API 实际感受下代理模式:

    const Angelababy = {
        name: '杨颖',
        age: '35',
        fansCount: 5000000
    };
    const AgentOfAb = new Proxy(Angelababy, {
        get(star, key) {
            const value = star[key];
            switch(key) {
            case 'age':
                return `${value - 5}岁`;
            default:
                return value;
            }
        },
        set(star, key, value) {
            switch(key) {
            case 'fansCount':
                Reflect.set(target, key, value * 10);
            default:
                Reflect.set(target, key, value);
            }
        }
    });

真正的发布订阅模式

我们已经讨论过托儿所如何从观察者模式发展成发布订阅模式了,为了让“托儿所”与“孩子”们之间实现从松耦合变为解耦,我们带入了“家长”,所以真正的发布订阅模式应该是:

发布订阅模式 = 观察者模式 + 代理模式

所以现在的托儿所已经变成如下这种模式了:

    class Publisher {
        constructor(proxy) {
            this.observer = proxy;
        }
        doYourJob(jobName) {
            this.observer.watchYourChildren(jobName);
        }
    }
    class Watcher {
        constructor(children) {
            this.observer = children;
        }
        addChild(child) {
            this.observer.push(child);
        }
        removeChild(curChild) {
            this.observer = this.observer.filter(child => child !== curChild);
        }
        watchYourChildren(jobName) {
            this.observer.forEach(child => childDoSomething(jobName, child));
        },
        childDoSomething(jobName, child) {
            switch(jobName) {
            case 'doHomework':
            case 'sport':
                child.doHomework();
                return;
            case 'sleep':
                child.rest();
                setTimmeout(() => child.doHomework(), 60000);
                return;
            case 'music':
                child.listen('English Listening');
                return;
            default:
                return;
            }
        }
    }

    const children = [
        // 可怜的孩子们
    ];

    // 狠毒的家长
    const parent = new Watcher(children);

    // 啥也不知道的老师
    const yourJob = new Publisher(parant);

    // 你以为孩子在做
    yourJob.doYourJob('doHomework');
    yourJob.doYourJob('sport');
    yourJob.doYourJob('sleep');

    // 实际上孩子在做
    // homework homework homework

Vue使用的设计模式到底是?

Vue双向绑定原理

上面是 Vue 双向绑定的原理,我们可以清楚地看出,数据和视图并不是耦合的,而是由 Watcher 去处理两边的状态变化, Vue 中使用的正是发布订阅模式。使用这种模式不仅可以让数据的变化可以实时反映在视图上,更让 Vue 有了获取数据变化( watch ),处理数据再展示( computed ),异步变化数据等等仅靠观察者无法做到的事。

小结

其实对于观察者模式和发布订阅模式的关系与区别众说纷纭,这方面的理解也很多元,以上只是我对于自己理解的一番阐述,希望大家和平探讨,尤其是和面试候选人哦。

参考: https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容