17-Vue的MVVM响应式原理

233 阅读7分钟

Vue的响应式原理

Vue采用的是数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。 MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板命令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化(input)=>数据model变更的双向绑定效果。

实现

要实现双向数据绑定,就必须实现以下几点:

  1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新的值并通知订阅者。
  2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定相应的回调函数,从而更新视图。

初始化MVue.js

class MVue {
    constructor(options) {
        //将属性绑定
        this.$el = options.el;
        this.$data = options.data;
        //将 options 参数保存,后面处理数据时需要用到
        this.$options = options;

        //对 el 进行判断,有值才进行操作
        if (this.$el) {
            //1.实现一个数据观察者
            //2.实现一个指令解析器
            new Compile(this.$el, this);
        }
    }
}

实现指令解析器Compile

实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

class Compile {
    /**
     * 
     * @param {传入的元素参数} el 
     * @param {MVue实例参数} vm 
     */
    constructor(el, vm) {
        //判断 el 是否是一个元素节点,如果是,直接赋值,如果不是,获取后再赋值
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        //1.获取文档碎片对象,放入缓存中,减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        //2.编译模板
        this.compile(fragment);
        //3.将子节点添加到根节点
        this.el.appendChild(fragment);
    }

    /**
     * 编译器
     * @param {} fragment 
     */
    compile(fragment) {
        //1.获取子节点
        const childNodes = fragment.childNodes;
        //遍历所有子节点,对元素节点和文本节点分别进行处理
        [...childNodes].forEach(child => {
            if (this.isElementNode(child)) {
                //元素节点,编译元素节点
                this.compileElement(child);
            } else {
                //文本节点,编译文本节点
                this.compileText(child);
            }

            //对于元素节点可能还存在孩子节点,需要进行递归遍历
            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }

    /**
     * 判断是否是通过 : 绑定
     * @param {:src等} attrName 
     */
    isBindName(attrName) {
        return attrName.startsWith(':');
    }

    /**
     * 判断是否是通过 @ 绑定
     * @param {@click等} attrName 
     */
    isEventName(attrName) {
        return attrName.startsWith('@');
    }

    /**
     * 判断是否是指令
     * @param {} attrName 
     */
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }

    /**
     * 判断是否是元素节点
     * @param {} node 
     */
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

编译元素节点

 /**
     * 编译元素节点
     * @param {} node 
     */
    compileElement(node) {
        //获取节点的所有属性
        [...node.attributes].forEach(attr => {
            //对 v-text="msg" 等进行解构取值
            const {
                name,
                value
            } = attr;

            //对 name 进行判断,是否是指令
            //v-text  v-html  v-model  v-on:click  v-bind:src
            if (this.isDirective(name)) {
                //分割,获取指令 text、html、model、on:click、bind:src
                const [, directive] = name.split('-');
                //对 on:click 进行处理
                const [dirName, eventName] = directive.split(':');
                //对不同的指令分别进行处理
                //参数: 当前节点  指令绑定的值(v-text='msg')msg等   MVue实例   v-on指令对应的事件名
                //更新数据,数据驱动视图
                compileUtil[dirName](node, value, this.vm, eventName);
                //移除标签上的指令
                node.removeAttribute('v-' + directive);
            } else if (this.isEventName(name)) {
                //处理 @ 绑定方式
                let [, eventName] = name.split('@');
                compileUtil['on'](node, value, this.vm, eventName);
            } else if (this.isBindName(name)) {
                //处理 : 绑定方式
                let [, eventName] = name.split(':');
                compileUtil['bind'](node, value, this.vm, eventName);
            }
        })
    }

编译文本节点

/**
     * 编译文本节点
     * @param {} node 
     */
    compileText(node) {
        //正则匹配 {{}}
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            compileUtil['text'](node, content, this.vm);
        }
    }

CompileUtil

const compileUtil = {
    /**
     * 对不同的书写形式获取值进行处理
     * @param {} expr 
     * @param {} vm 
     */
    getValue(expr, vm) {
        //注意 reduce 方法的原理,它会将取到的值返回给data,然后再次遍历取data中对应的属性的值
        //第一次 data[person] 取到person对象 返回给data
        //第二次 person[name] 取到name值 返回
        return expr.split('.').reduce((data, currentValue) => {
            return data[currentValue]
        }, vm.$data); //将MVue实例中的data作为参数传入
    },
    /**
     * 对v-text指令进行处理
     * @param {} node 
     * @param {指令中绑定的msg等} expr 
     * @param {} vm 
     */
    text(node, expr, vm) {
        //直接取值对于 <div v-text='person.name'></div> 这种形式的来说不可行
        //const value = vm.$data[expr];
        //通过函数对不同书写形式进行处理,获取值
        //const value = this.getValue(expr, vm);
        //对于文本中的 {{}} 需要重新进行处理
        let value;
        if (expr.indexOf('{{') !== -1) {
            //replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
            //用函数中返回的值替换掉 expr 中原来的值 替换掉 {{person.name}}
            //函数中的 args 参数就是 expr 
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getValue(args[1], vm);
            })
        } else {
            value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);
    },
    /**
     * 对v-html指令进行处理
     * @param {} node 
     * @param {} expr 
     * @param {} vm 
     */
    html(node, expr, vm) {
        const value = this.getValue(expr, vm);
        this.updater.htmlUpdater(node, value);
    },
    /**
     * 对v-model指令进行处理
     * @param {} node 
     * @param {} expr 
     * @param {} vm 
     */
    model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        this.updater.modelUpdater(node, value);
    },
    /**
     * 对v-on指令进行处理
     * @param {} node 
     * @param {绑定的方法名} expr 
     * @param {} vm 
     * @param {事件名} eventName 
     */
    on(node, expr, vm, eventName) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        //绑定this指向 绑定vm,阻止冒泡
        node.addEventListener(eventName, fn.bind(vm), false);
    },
    /**
     * 
     * @param {} node 
     * @param {属性值} expr 
     * @param {} vm 
     * @param {src等属性名} eventName 
     */
    bind(node, expr, vm, eventName) {
        const value = this.getValue(expr, vm);
        this.updater.bindUpdater(node, value, eventName);
    },
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        },
        bindUpdater(node, value, eventName) {
            node.setAttribute(eventName, value);
        }
    }
}

优化编译

编译需要将根节点的所有子节点全部拿出来,然后进行替换,每次替换都会导致页面的回流与重绘,非常影响页面的性能。

在原生js中有个文档碎片,它能够将所有的子节点放入缓存中,当需要的时候直接从缓存中获取,减少页面的回流和重绘。

创建文档碎片

createDocumentFragment()方法,是用来创建一个虚拟的节点对象,节点对象包含所有属性和方法。或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。

当你想提取文档的一部分,改变,增加,或删除某些内容及插入到文档末尾可以使用createDocumentFragment() 方法。你也可以使用文档的文档对象来执行这些变化,但要防止文件结构被破坏,createDocumentFragment() 方法可以更安全改变文档的结构及节点。

语法:const f = document.createDocumentFragment();

/**
     * 创建虚拟节点并返回,减少页面的回流与重绘
     * @param {} el 
     */
    node2Fragment(el) {
        const f = document.createDocumentFragment();
        let firstChild;
        //赋值并判断是否有值
        while (firstChild = el.firstChild) {
            /*
            Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。
            如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将
            它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
            如果某个节点已经拥有父节点,在被传递给此方法后,它首先会被移除,再被插入到新的位置。
            */
            f.appendChild(firstChild);
        }
        return f;
    }

实现Observer

通过Object.defineProperty()来劫持监听所有属性,当数据发生变化时通知Dep容器

class Observer {
    constructor(data) {
        this.observe(data);
    }

    /**
     * 监听数据
     * @param {} data 
     */
    observe(data) {
        if (data && typeof data === 'object') {
            //获取data中属性的键
            //获取的数据中可能还有对象,遍历判断
            /*Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,
            数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。*/
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            })
        }
    }

    /**
     * 递归遍历,对所有数据进行监听
     * @param {} obj 
     * @param {} key 
     * @param {} value 
     */
    defineReactive(obj, key, value) {
        //递归遍历
        this.observe(value);

        //劫持数据时创建 Dep 和 Observer 进行关联
        const dep = new Dep();

        //数据劫持
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            //初始化的时候调用get()
            get() {
                //数据发生变化时,向 Dep 中添加订阅者
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            //通过箭头函数处理this指向问题
            set: (newValue) => {
                //对新值进行监听,否则更改后的新值将不能被监听到
                this.observe(newValue)

                //对新值进行判断
                if (newValue != value) {
                    value = newValue;
                }
                //告诉Dep通知数据发生变化
                dep.notify();
            }
        });
    }
}

实现Dep

Dep容器主要有两个作用:

  1. 将所有订阅者进行添加
  2. 数据发生变化时,通知所有订阅者
class Dep {
    //1.添加所有订阅者
    //2.数据发生变化时,通知所有订阅者
    constructor() {
        //用于保存所有订阅者
        this.subs = [];
    }
    /**
     * 添加订阅者
     * @param {} watcher 
     */
    addSub(watcher) {
        this.subs.push(watcher);
    }
    /**
     * 通知所有订阅者
     */
    notify() {
        this.subs.forEach(w => w.update())
    }
}

实现Watcher

Watcher是连接Observer和Compile的桥梁,能够订阅并收到每个属性变化的通知,执行指令绑定的相应的回调函数,更新视图。它所需要做的两个事情:

  1. 实例化时向Dep中添加自己
  2. 当属性的值发生变化时,dep.notify()通知Watcher调用update()方法,触发绑定的回调函数,更新视图
class Watcher {
    /**
     * 
     * @param {MVue实例} vm 
     * @param {指令中绑定的msg、person.name等} expr 
     * @param {回调函数} cb 
     */
    constructor(vm, expr, cb) {
        //保存
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //保存旧值
        this.oldValue = this.getOldValue();
    }

    getOldValue() {
        //new MVue()时,创建Watcher对象,将当前观察者挂载到 Dep 上
        Dep.target = this;
        //通过 MVue.js 中的 compileUtil 获取值
        const oldValue = compileUtil.getValue(this.expr, this.vm);
        //获取完旧值之后进行销毁,否则再次更新数据时会导致创建多个订阅者
        Dep.target = null;
        return oldValue;
    }

    update() {
        const newValue = compileUtil.getValue(this.expr, this.vm);
        if (newValue != this.oldValue) {
            //将新值回调返回
            this.cb(newValue);
        }
    }
}

代理

在vue中可以直接通过this.person.name = 'Alex';来进行赋值,而我们现在是通过this.$data.person.name = 'Alex';来进行赋值,需要通过 Object.defineProperty()this.$data进行代理来实现直接使用this调用。

proxyData(data) {
        for (const key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newValue) {
                    data[key] = newValue;
                }
            })
        }
    }

思考

在哪里绑定Watcher?在Watcher中怎样获取值?

在Compile中对各个指令进行编译,初始化视图时,创建Watcher进行绑定,并将vm(当前MVue实例)、expr(指令绑定的属性msg、person.name等)、回调函数作为参数传入。 在Watcher中将传入的参数获取、保存,调用compileUtil.getValue()获取值,并进行新值和旧值的判断,更新视图。

Dep怎样和Observer进行关联?

初始化时,Observer会对所有属性进行劫持监听,此时创建const dep = new Dep(),进行关联,同时在Dep的 Object.defineProperty()的get方法中将订阅者Watcher进行添加。

Dep怎样获取Watcher?

初始化时在Watcher的getOldValue()将当前的订阅者Watcher实例绑定到Dep上,Dep.target = this;,然后在Object.defineProperty中将Watcher添加dep.addSub(Dep.target);

整合

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue的MVVM响应式原理</title>
</head>

<body>
    <div id="app">
        <h2>{{person.name}}---{{person.age}}</h2>
        <h3>{{person.hobby}}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-text='msg'></div>
        <div v-text='person.name'></div>
        <div v-html='htmlStr'></div>
        <input type="text" v-model='msg'>
        <button v-on:click='handleClick'>按钮on</button>
        <button @click='handleClick'>按钮@</button>
        <img style="width: 200px;" v-bind:src="imgSrc" alt="">
        <img style="width: 100px;" :src="imgSrc" alt="">
    </div>
    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>
    <script>
        let vm = new MVue({
            el: "#app",
            data: {
                person: {
                    name: 'BooBoo',
                    age: 21,
                    hobby: 'music'
                },
                msg: 'MVVM实现原理',
                htmlStr: '<h3>Hello Vue</h3>',
                imgSrc: './img/img.jpg'
            },
            methods: {
                handleClick() {
                    this.person.name = 'Alex';
                    // console.log(this.$data);
                    // this.$data.person.name = 'Alex';
                }
            },
        })
    </script>
</body>

</html>

MVue.js

const compileUtil = {
    /**
     * 设置值,视图=>数据=>视图
     * @param {} expr 
     * @param {} vm 
     * @param {} inputValue 
     */
    setValue(expr, vm, inputValue) {
        return expr.split('.').reduce((data, currentValue) => {
            //直接赋值就行
            data[currentValue] = inputValue;
        }, vm.$data);
    },
    /**
     * 对不同的书写形式获取值进行处理
     * @param {} expr 
     * @param {} vm 
     */
    getValue(expr, vm) {
        //注意 reduce 方法的原理,它会将取到的值返回给data,然后再次遍历取data中对应的属性的值
        //第一次 data[person] 取到person对象 返回给data
        //第二次 person[name] 取到name值 返回
        return expr.split('.').reduce((data, currentValue) => {
            return data[currentValue];
        }, vm.$data); //将MVue实例中的data作为参数传入
    },
    /**
     * 对 {{person.name}}-{{person.age}} 进行处理
     * @param {} expr 
     * @param {} vm 
     */
    getContentVal(expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm);
        })
    },
    /**
     * 对v-text指令进行处理
     * @param {} node 
     * @param {指令中绑定的msg、person.name等} expr 
     * @param {} vm 
     */
    text(node, expr, vm) {
        //直接取值对于 <div v-text='person.name'></div> 这种形式的来说不可行
        //const value = vm.$data[expr];
        //通过函数对不同书写形式进行处理,获取值
        //const value = this.getValue(expr, vm);
        //对于文本中的 {{}} 需要重新进行处理
        let value;
        if (expr.indexOf('{{') !== -1) {
            //replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
            //用函数中返回的值替换掉 expr 中原来的值 替换掉 {{person.name}}
            //函数中的 args 参数就是 expr 
            //  /\{\{(.+?)\}\}/g 匹配 {{}} 
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                //绑定订阅者,将来数据发生变化触发回调,进行更新
                new Watcher(vm, args[1], () => { //expr传入的可能是 {{person.name}}-{{person.age}}
                    //更新视图
                    this.updater.textUpdater(node, this.getContentVal(expr, vm));
                });
                return this.getValue(args[1], vm);
            })
        } else {
            value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);
    },
    /**
     * 对v-html指令进行处理
     * @param {} node 
     * @param {} expr 
     * @param {} vm 
     */
    html(node, expr, vm) {
        const value = this.getValue(expr, vm);
        //绑定Watcher对数据进行监听
        new Watcher(vm, expr, (newValue) => {
            //更新数据
            this.updater.htmlUpdater(node, newValue);
        })
        //更新数据
        this.updater.htmlUpdater(node, value);
    },
    /**
     * 对v-model指令进行处理
     * @param {} node 
     * @param {} expr 
     * @param {} vm 
     */
    model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        //绑定更新函数  数据=>视图
        new Watcher(vm, expr, (newValue) => {
            //更新数据
            this.updater.modelUpdater(node, newValue);
        })
        // 视图=>数据=>视图
        //绑定 input 事件
        node.addEventListener('input', (e) => {
            //设置值
            this.setValue(expr, vm, e.target.value);
        })

        this.updater.modelUpdater(node, value);
    },
    /**
     * 对v-on指令进行处理
     * @param {} node 
     * @param {绑定的方法名} expr 
     * @param {} vm 
     * @param {事件名} eventName 
     */
    on(node, expr, vm, eventName) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        //绑定this指向 绑定vm,阻止冒泡
        node.addEventListener(eventName, fn.bind(vm), false);
    },
    /**
     * 
     * @param {} node 
     * @param {属性值} expr 
     * @param {} vm 
     * @param {src等属性名} eventName 
     */
    bind(node, expr, vm, eventName) {
        const value = this.getValue(expr, vm);
        this.updater.bindUpdater(node, value, eventName);
    },
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        },
        bindUpdater(node, value, eventName) {
            node.setAttribute(eventName, value);
        }
    }
}

class Compile {
    /**
     * 
     * @param {传入的元素} el 
     * @param {MVue实例} vm 
     */
    constructor(el, vm) {
        //判断 el 是否是一个元素节点,如果是,直接赋值,如果不是,获取后再赋值
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        //1.获取文档碎片对象,放入缓存中,减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        //2.编译模板
        this.compile(fragment);
        //3.将子节点添加到根节点
        this.el.appendChild(fragment);
    }

    /**
     * 编译器
     * @param {} fragment 
     */
    compile(fragment) {
        //1.获取子节点
        const childNodes = fragment.childNodes;
        //遍历所有子节点,对元素节点和文本节点分别进行处理
        [...childNodes].forEach(child => {
            if (this.isElementNode(child)) {
                //元素节点,编译元素节点
                this.compileElement(child);
            } else {
                //文本节点,编译文本节点
                this.compileText(child);
            }

            //对于元素节点可能还存在孩子节点,需要进行递归遍历
            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }

    /**
     * 编译元素节点
     * @param {} node 
     */
    compileElement(node) {
        //获取节点的所有属性
        [...node.attributes].forEach(attr => {
            //对 v-text="msg" 等进行解构取值
            const {
                name,
                value
            } = attr;

            //对 name 进行判断,是否是指令
            //v-text  v-html  v-model  v-on:click  v-bind:src
            if (this.isDirective(name)) {
                //分割,获取指令 text、html、model、on:click、bind:src
                const [, directive] = name.split('-');
                //对 on:click 进行处理
                const [dirName, eventName] = directive.split(':');
                //对不同的指令分别进行处理
                //参数: 当前节点  指令绑定的值(v-text='msg')msg等   MVue实例   v-on指令对应的事件名
                //更新数据,数据驱动视图
                compileUtil[dirName](node, value, this.vm, eventName);
                //移除标签上的指令
                node.removeAttribute('v-' + directive);
            } else if (this.isEventName(name)) {
                //处理 @ 绑定方式
                let [, eventName] = name.split('@');
                compileUtil['on'](node, value, this.vm, eventName);
            } else if (this.isBindName(name)) {
                //处理 : 绑定方式
                let [, eventName] = name.split(':');
                compileUtil['bind'](node, value, this.vm, eventName);
            }
        })
    }

    /**
     * 编译文本节点
     * @param {} node 
     */
    compileText(node) {
        //正则匹配 {{}}
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            compileUtil['text'](node, content, this.vm);
        }
    }

    /**
     * 创建虚拟节点并返回,减少页面的回流与重绘
     * @param {} el 
     */
    node2Fragment(el) {
        const f = document.createDocumentFragment();
        let firstChild;
        //赋值并判断是否有值
        while (firstChild = el.firstChild) {
            /*
            Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。
            如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将
            它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
            如果某个节点已经拥有父节点,在被传递给此方法后,它首先会被移除,再被插入到新的位置。
            */
            f.appendChild(firstChild);
        }
        return f;
    }

    /**
     * 判断是否是通过 : 绑定
     * @param {:src等} attrName 
     */
    isBindName(attrName) {
        return attrName.startsWith(':');
    }

    /**
     * 判断是否是通过 @ 绑定
     * @param {@click等} attrName 
     */
    isEventName(attrName) {
        return attrName.startsWith('@');
    }

    /**
     * 判断是否是指令
     * @param {} attrName 
     */
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }

    /**
     * 判断是否是元素节点
     * @param {} node 
     */
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

class MVue {
    constructor(options) {
        //将属性绑定
        this.$el = options.el;
        this.$data = options.data;
        //将 options 参数保存,后面处理数据时需要用到
        this.$options = options;

        //对 el 进行判断,有值才进行操作
        if (this.$el) {
            //1.实现一个数据观察者(观察数据中所有的属性)
            new Observer(this.$data);
            //2.实现一个指令解析器
            new Compile(this.$el, this);
            //代理 this.$data
            this.proxyData(this.$data);
        }
    }

    proxyData(data) {
        for (const key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newValue) {
                    data[key] = newValue;
                }
            })
        }
    }
}

Observer.js

class Watcher {
    /**
     * 
     * @param {MVue实例} vm 
     * @param {指令中绑定的msg、person.name等} expr 
     * @param {回调函数} cb 
     */
    constructor(vm, expr, cb) {
        //保存
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //保存旧值
        this.oldValue = this.getOldValue();
    }

    getOldValue() {
        //new MVue()时,创建Watcher对象,将当前观察者挂载到 Dep 上
        Dep.target = this;
        //通过 MVue.js 中的 compileUtil 获取值
        const oldValue = compileUtil.getValue(this.expr, this.vm);
        //获取完旧值之后进行销毁,否则再次更新数据时会导致创建多个订阅者
        Dep.target = null;
        return oldValue;
    }

    update() {
        const newValue = compileUtil.getValue(this.expr, this.vm);
        if (newValue != this.oldValue) {
            //将新值回调返回
            this.cb(newValue);
        }
    }
}

class Dep {
    //1.添加所有订阅者
    //2.数据发生变化时,通知所有订阅者
    constructor() {
        //用于保存所有订阅者
        this.subs = [];
    }
    /**
     * 添加订阅者
     * @param {} watcher 
     */
    addSub(watcher) {
        this.subs.push(watcher);
    }
    /**
     * 通知所有订阅者
     */
    notify() {
        this.subs.forEach(w => w.update())
    }
}


class Observer {
    constructor(data) {
        this.observe(data);
    }

    /**
     * 监听数据
     * @param {} data 
     */
    observe(data) {
        if (data && typeof data === 'object') {
            //获取data中属性的键
            //获取的数据中可能还有对象,遍历判断
            /*Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,
            数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。*/
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            })
        }
    }

    /**
     * 递归遍历,对所有数据进行监听
     * @param {} obj 
     * @param {} key 
     * @param {} value 
     */
    defineReactive(obj, key, value) {
        //递归遍历
        this.observe(value);

        //劫持数据时创建 Dep 和 Observer 进行关联
        const dep = new Dep();

        //数据劫持
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            //初始化的时候调用get()
            get() {
                //数据发生变化时,向 Dep 中添加订阅者
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            //通过箭头函数处理this指向问题
            set: (newValue) => {
                //对新值进行监听,否则更改后的新值将不能被监听到
                this.observe(newValue)

                //对新值进行判断
                if (newValue != value) {
                    value = newValue;
                }
                //告诉Dep通知数据发生变化
                dep.notify();
            }
        });
    }
}