学习vue双向绑定原理,并试着手写实现

215 阅读4分钟

一、MVVM框架
MVVM框架本质上即为MVC的进阶版。MVVM 就是将其中的View的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
MVVM框架相比于MVC框架的优点在于:视图与模型的分离,所有的交互都通过VM进行通知和交互,降低了视图和模型的耦合度,View的修改可独立于Model的修改,提高了重用性,可以在View中重用独立的视图逻辑。
这段话有点难理解,所以拆解一下来看,降低视图和模型的耦合度,从下面Vue官方示例图就可以看出,View和Model现在没有直接双向连接线了,而是连接到ViewModel上,通过ViewModel来双向连接;意思就是View没有直接绑定到Model上了,他们互相分离了,也就是解耦了,而且一个ViewModel可以绑定到多个不同的View上;可重用性,意思就是可以把一段视图逻辑放在ViewModel里,让多个View来重复使用这段视图逻辑。

20200107153930484.png

还有一个疑问,View是视图,Model是数据,都好理解,那ViewModel到底是个什么东西?这里我找到一个最简单的解释,Vue.js就是一个ViewModel。
上面说了View和Model没有直接绑定到一起了,而是通过ViewModel来绑定的,那么ViewModel可以干些什么事呢?
对于View来说,View引用了ViewModel,View上的内容发生改变后,可以同步改变ViewModel中对应的数据。同理ViewModel中的数据发生改变后 ,也可以同步改变View中对应的显示内容。
而对于Model来说,是ViewModel引用了Model,ViewModel可以通过Ajax通信来发送请求给Model,而Model也可以通过Ajax通信发送数据给ViewModel。
所以可以看出,ViewModel做的事就是处理View和Model之间的数据通知和交互。

二、数据劫持
了解了MVVM框架,理解双向绑定就很容易了。双向绑定指的就是ViewModel中的数据和View视图之间的关系,即View <<=====>> ViewModel。
双向绑定依赖ES5中一个很重要的API,Object.defineProperty,数据劫持。
它的语法是:Object.defineProperty(obj, prop, descriptor)
参数解释:
obj:需要劫持的对象。
prop:需要劫持的属性。
descriptor:即将被定义或修改的属性描述符。
这么看还是有点空洞,直接上代码!

const obj = {};
Object.defineProperty(obj,"a",{
    value: 111,   //属性对应的值
    writable: true,   //定义属性的值是否可以被重写
    enumerable: true,  //定义此属性是否可以被枚举
    configurable: `true` //定义此属性是否可配置(删除、重新设置)
})

把这段代码复制到浏览器的控制台,可以看到,obj里面多了一条属性a,a的值是111,这就相当于obj的a属性已经被我们劫持到了。那么通过什么来实现绑定呢? 这里就要用到Object.defineProperty的另外两个关键的方法了,get()和set()方法。

let str = '我是属性b的值';  //这里我们假如这个str是View中显示内容,可以理解为document.getElementById('b').value获取到的值
Object.defineProperty(obj,"b",{ 
    get: function() {
        return str
    },
    set: function(value) {
        str = value;
    }
})

通过Object.defineProperty劫持了obj的属性b,每次访问 obj.b 的值的时候,都会触发get()这个函数,每次要执行 obj.b='我是属性b修改后的值' 的操作的时候,就会触发set()函数。
不过光有Object.defineProperty这个API还不足以实现vue这种双向绑定 ,vue的双向绑定还需要用到发布者-订阅者模式。

三、发布者-订阅者模式
那么什么是发布者,什么是订阅者呢。举个例子,假如我们要买新手机,新款手机是不是要开发布会,那么开发布会的人就是发布者,我们要买手机的人就是订阅者。从这个例子也能看出,发布者只有1个,但是订阅者可以是多个。

694228-20180419195748905-771938588.png 接下来就用代码来解释一下发布者和订阅者模式是怎样配合数据劫持来实现双向绑定的。

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手写实现vue双向绑定</title>
</head>

<body>
    <div id="app">
        <div>
            <div v-test="myText"></div>
            <div v-test="myBox"></div>
            <input type="text" v-bind="myText">
        </div>
    </div>
    <script>
        // 订阅者
        class Watcher {
            constructor(el, vm, exp, attr) {
                this.el = el;
                this.vm = vm;
                this.exp = exp;
                this.attr = attr;
                this.update();
            }
            // 更新视图方法
            update() {
                this.el[this.attr] = this.vm.$data[this.exp];
            }
        }
        //创建一个Vue类
        class Vue {
            constructor(data) {
                this.data = data;
                this.$data = data.data;
                this.$el = document.querySelector(data.el);
                // 记录订阅者的容器
                this._directive = {};
                // 发布者
                this.Observe(this.$data);
                // 解析器
                this.Compile(this.$el);
            }
            // 发布者
            Observe(data) {
                // 遍历传入的对象data
                for (let key in data) {
                    this._directive[key] = []; 
                    //缓存属性的值
                    let Val = data[key];
                    // 以对象的方式添加订阅者
                    let watch = this._directive[key];
                    // 为每个属性添加Object.defineProperty
                    Object.defineProperty(this.$data, key, {
                        get: function () {
                            return Val
                        },
                        set: function (newVal) {
                            if (newVal !== Val) {//新值不等于老值
                                Val = newVal;
                                // console.log(watch, "watch");
                                // 更新视图
                                watch.forEach(ele => {
                                    // 调用更新视图的方法
                                    ele.update();
                                });
                            }
                        }
                    })
                }
            }

            // 解析器
            Compile(el) {
                // 缓存app下所有子节点
                let nodes = el.children;
                // 遍历子节点
                for (let i = 0; i < nodes.length; i++) {
                    let node = nodes[i];
                    // 递归遍历子节点
                    if (nodes[i].children) {
                        this.Compile(nodes[i]);
                    }
                    // 找出带有v-text属性的节点,并添加到发布者中记录订阅者的容器中
                    if (node.hasAttribute("v-test")) {
                        let attVal = node.getAttribute("v-test");
                        this._directive[attVal].push(new Watcher(node, this, attVal, "innerHTML")); 
                    }
                    // 找出带有v-model属性的节点,并添加到发布者中记录订阅者的容器中
                    if (node.hasAttribute("v-bind")) {
                        let attrVal = node.getAttribute("v-bind");
                        this._directive[attrVal].push(new Watcher(node, this, attrVal, "value"));
                        // 为v-model属性的节点添加input事件
                        node.addEventListener("input", (function () {
                            return function () {
                                this.$data[attrVal] = node.value;
                            }
                        })().bind(this));
                    }
                }
            }
        }
        //  实例化Vue
        const app = new Vue({
            el: '#app',
            data: {
                myText: "aaaaa",
                myBox: "bbbbb"
            }
        });
    </script>
</body>

</html>

我是个正在不断学习中的小白,记录下学习前端的知识点,文中如有错误,望各位技术大牛指正,小弟感激不尽!