我们都知道在Vue.js中,MVVM相对传统的DOM操作,有一个很大的优点就是能实现数据的双向绑定。
先用代码来体验一下:
<div id="app">
{{message}} {{name}}
</div>
<script src="vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'Hello',
name: 'vue'
}
})
</script>
运行结果:
现在来改变一下其中的一个数据,在控制台输入app.name = ‘world’ ,我们可以看到此时运行结果变成了:
要理解Vue是如何实现数据双向绑定的,就要搞懂两个问题:
①app.name修改数据,Vue内部是如何监听name数据的改变的?
②当数据发生改变时,Vue是如何知道要通知谁,并且实现界面刷新?
我们先来解决第一个问题。
在ES5中有一个Object.defineProperty()方法,能监听各个属性的get和set方法(也称为数据劫持)。该方法有三个参数,前两个参数分别是要监听的数据及其属性,第三个参数是对get和set方法的监听。
const obj = {
message:'Hello',
name:'world'
}
//遍历获得每个属性值
Object.keys(obj).forEach(key => {
let value = obj[key]
})
Object.defineProperty(obj,key,{
set(newValue){
console.log('监听' + key + 改变);
value = newValue
},
get(){
console.log('监听' + key + '对应的值');
return value
}
})
//obj.name = 'vue'
//监听name改变
//obj.message
//监听message对应的值
通过defineProperty()方法,只要数据有任何改变,都可以监听和劫持。但是这些属性的变化和监听,要告诉谁呢,接下来看看第二个问题是怎么解决的。
Vue数据双向绑定原理实现是靠发布-订阅者模式,给需要变化的数据增加一个观察者,将新值和旧值进行对比,如果数据发生变化就采取相应的方法。
//发布订阅者
class Dep{
constructor(){
//订阅的数组
this.subs = []
}
//增加订阅者
addSub(watcher){
this.subs.push(watcher)
}
//通知更新
notify(){
this.subs.forEach(item => {
item.update()
})
}
}
//订阅者
class Watcher {
constructor(name){
this.name = name
}
update(){
console.log(this.name + '发生update')
}
}
const dep = new Dep()
const watcher = new Watcher('张三')
dep.addSub(watcher)
dep.notify()//张三发生update
还是这张图,我们根据这张图来做一个小结:
创建一个Vue实例,将Vue实例中的data数据传送给Obverse,在Obverse中用Object.defineProperty()方法对各个属性进行监听。同时创建Dep对象,一个属性对应一个Dep对象。Dep里调用addSub方法增加订阅者。
将el模板传送给Compile,解析el模板中的指令,一个指令对应创建一个Watcher,然后这个Watcher会指向对应属性的Dep对象。第一次创建Vue实例时,会初始化视图,在View中显示第一次的属性。
这时数据发生了改变,例如改变message属性,Obverse监听到数据发生了改变,就会在Dep对象里调用notify()方法,通知Watcher,Watcher会调用update方法对View视图进行更新,从而实现了数据的响应。
而在Vue 3.x中,将数据劫持中的Object.defineProperty方法改成了用ES6中的Proxy。
Proxy可以监听整个对象,省去遍历提升效率。我们来看看是怎么实现的:
let arr = [1,2,3]
let p = new Proxy(arr,{
//三个参数,第一个参数是目标对象,第二个参数是get方法,第三个参数是set方法
get(data,property,receiver){
// 1.目标对象
// 2.被获取的属性值
// 3.Proxy或继承Proxy
console.log('get被调用了')
}
set(data,property,value,receiver){
console.log('数组内部发生了变化,更新后的值为:' + value)
}
})
//p.[1] = 5; //数组内部发生了变化,更新后的值为:5
那两种方法对比,Proxy有什么优势呢?
用Object.defineProperty()方法无法监听到数组内部的数据变化来实现内部数据的检测。(但是用concat()方法可以)而用Proxy可以监听到数组内部的变化,也可以直接监听对象而非属性。
Proxy有多种拦截方法,如apply,deleteProperty等等,是Object.defineProperty()不具备的。
**Proxy是返回值是一个对象,可以直接进行操作,**而defineProperty()要先遍历所有对象属性值才能进行操作
但是相对来说,Object.defineProperty()兼容性高一些。