Vue的双向绑定,Model如何改变View,View又是如何改变Model的?

Vue的双向绑定,Model如何改变View,View又是如何改变Model的?

数据绑定如图所示
盒子模型

双向数据绑定, 就是数据层和视图层中的数据同步, 在写入数据时视图层实时的跟着更新。
实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。就必须要实现以下几点:

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

第一步,使数据对象变得“可观测”


var Book = {}
var name = ''
Object.defineProperty(Book, 'name', {
  set: function (value) {
    name = value;
    console.log('你取了一个书名叫做' + value)
  },
  get: function () {
    return '《' + name + '》'
  }
})
 
Book.name = 'vue.js实战'
console.log(Book.name)

// 你取了一个书名叫做vue.js实战
// 《vue.js实战》

我们通过Object.defineProperty( )设置了对象Bookname属性,对其getset进行重写操作,顾名思义,get就是在读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数,所以当执行 Book.name = ‘vue.js实战’ 这个语句时,控制台会打印出 “你取了一个书名叫做vue.js实战”,紧接着,当读取这个属性时,就会输出 “《vue.js实战》”,因为我们在get函数里面对该值做了加工了。

我们要知道数据在什么时候被读或写了。

let person = {
    'name': 'maomin',
    'age': 23
}
let val = 'maomin';
Object.defineProperty(person, 'name', {
    get() {
        console.log('name属性被读取了')
        return val
    },
    set(newVal) {
        console.log('name属性被修改了')
        val = newVal
    }
})
console.log(person.name)
// name属性被读取了
// "maomin"


person.name='xqm'
console.log(person.name)
// name属性被修改了
// "xqm"

通过Object.defineProperty()方法给Book定义了一个name属性,并把这个属性的读和写分别使用get()和set()进行拦截,每当该属性进行读或写操作的时候就会触发get()和set()。这样数据对象已经是“可观测”的了。

核心是利用es5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。

Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

Object.defineProperty(
    obj, // 定义属性的对象
    prop, // 要定义或修改的属性的名称
    descriptor // 将要定义或修改属性的描述符【核心】
)

「写一个简单的双向绑定:」

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <input type="text" id="input"/>
    <div id="text"></div>
</body>
<script>
    let input = document.getElementById('input');
    let text = document.getElementById('text');
    let data = {value:''};
    Object.defineProperty(data,'value',{
        set:function(val){
            text.innerHTML = val;
            input.value = val;
        },
        get:function(){
            return input.value;
        }
    });
    input.onkeyup = function(e){
        data.value = e.target.value;
    }
</script>
</html>

第二步,使数据对象的所有属性变得“可观测”
上面,我们只能观测person.name的变化,那么接下来我们要让所有的属性都变得可检测。

let person = observable({
    'name': 'maomin',
    'age': 23
})
/**
 * 把一个对象的每一项都转化成可观测对象
 * @param { Object } obj 对象
 */
function observable(obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj); //返回一个表示给定对象的所有可枚举属性的字符串数组
    keys.forEach((key) => {
        defineReactive(obj, key, obj[key])
    })
    return obj;
}
/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`${key}属性被读取了`);
            return val;
        },
        set(newVal) {
            console.log(`${key}属性被修改了`);
            val = newVal;
        }
    })
}
console.log(person.age)
// age属性被读取了
// 23

person.age=24
console.log(person.age)
// age属性被修改了
// age属性被读取了
// 24

我们通过Object.keys()将一个对象返回一个表示给定对象的所有可枚举属性的字符串数组,然后遍历它,使得所有对象可以被观测到。


第三步,依赖收集,制作一个订阅器
我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。

「设计了一个订阅器Dep类:」

class Dep {
    constructor(){
        this.subs = []
    },
    //增加订阅者
    addSub(sub){
        this.subs.push(sub);
    },
    //判断是否增加订阅者
    depend () {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    },
    //通知订阅者更新
    notify(){
        this.subs.forEach((sub) =>{
            sub.update()
        })
    }
}
Dep.target = null;

创建完订阅器,然后还要修改一下defineReactive

function defineReactive (obj,key,val) {
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get(){
            dep.depend(); //判断是否增加订阅者
            console.log(`${key}属性被读取了`);
            return val;
        },
        set(newVal){
            val = newVal;
            console.log(`${key}属性被修改了`);
            dep.notify() //数据变化通知所有订阅者
        }
    })
}

我们将订阅器Dep添加订阅者的操作设计在get()里面,这是为了让订阅者初始化时进行触发,因此需要判断是否要添加订阅者。


第四步,订阅者Watcher
「设计一个订阅者Watcher类:」

class Watcher {
// 初始化
    constructor(vm,exp,cb){
        this.vm = vm; // 一个Vue的实例对象
        this.exp = exp; // 是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
        this.cb = cb; // 是Watcher绑定的更新函数;
        this.value = this.get();  // 将自己添加到订阅器的操作
    },
// 更新
    update(){
        let value = this.vm.data[this.exp];
        let oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        },
    get(){
        Dep.target = this;  // 缓存自己
        let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
}

订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,如何添加呢?我们已经知道监听器Observer是在get()执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候触发对应的get()去执行添加订阅者操作即可。那要如何触发监听器get(),再简单不过了,只要获取对应的属性值就可以触发了。

订阅者Watcher运行时,首先进入初始化,就会执行它的 this.get() 方法, 执行Dep.target = this;,实际上就是把Dep.target 赋值为当前的渲染 Watcher ,接着又执行了let value = this.vm.data[this.exp];。在这个过程中会对数据对象上的数据访问,其实就是为了触发数据对象的get()

每个对象值的get()都持有一个dep,在触发 get()的时候会调用 dep.depend()方法,也就会执行this.addSub(Dep.target),即把当前的 watcher订阅到这个数据持有的dep.subs中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。完成依赖收集后,还需要把 Dep.target恢复成上一个状态Dep.target = null; 因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。

update()是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。


总结:
实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer订阅者Watcher之间进行统一管理的。
盒子模型


实现一个Vue数据绑定:
「index.html」

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //将数据变的可观测
        el.innerHTML = this.data[exp];         // 初始化模板数据的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }
    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改变输入框内容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改变data内容
    function changeInput(){
        myVue.data.name = "改变后的data"
    }
</script>
</body>
</html>

「observer.js」(为了方便,这里将订阅器与监听器写在一块)

 // 监听器
 // 把一个对象的每一项都转化成可观测对象
 // @param { Object } obj 对象
 
function observable (obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj);
    keys.forEach((key) =>{
        defineReactive(obj,key,obj[key])
    })
    return obj;
}
 // 使一个对象转化成可观测对象
 // @param { Object } obj 对象
 // @param { String } key 对象的key
 // @param { Any } val 对象的某个key的值
 
function defineReactive (obj,key,val) {
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get(){
            dep.depend();
            console.log(`${key}属性被读取了`);
            return val;
        },
        set(newVal){
            val = newVal;
            console.log(`${key}属性被修改了`);
            dep.notify()                    //数据变化通知所有订阅者
        }
    })
}

// 订阅器Dep 
class Dep {
    
    constructor(){
        this.subs = []
    }
    //增加订阅者
    addSub(sub){
        this.subs.push(sub);
    }
    //判断是否增加订阅者
    depend () {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }

    //通知订阅者更新
    notify(){
        this.subs.forEach((sub) =>{
            sub.update()
        })
    }
    
}
Dep.target = null;

「watcher.js」

class Watcher {
    constructor(vm,exp,cb){
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        this.value = this.get();  // 将自己添加到订阅器的操作
    }
    get(){
        Dep.target = this;  // 缓存自己
        let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
    update(){
        let value = this.vm.data[this.exp];
        let oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值