vue数据双向绑定原理
什么是数据响应式?
数据响应式即数据双向绑定,就是把Model绑定到view,当我们通过js修改Model,View会自动更新;若我们更新了View,Model的数据也会自动更新,这就是双向绑定。
数据响应式原理
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,
那么vue是如果进行数据劫持的???
1)vue2.0版本是利用了Object.defineProperty()这个方法重新定义对象获取属性值的get和设置属性值set的操作来实现的。
2)vue3.0版本采用了Es6的Proxy对象来实现。
我们先分别了解下defineProperty方法、Proxy对象。
什么是defineProperty?
defineProperty简言之,是定义对象的属性。它可以来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举等。
它其实并不是核心的为一个对象做数据绑定,而是给对象做属性标签。定义对象的属性。只不过是属性的get和set实现了响应式。
属性名 | 默认值 |
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
接下来,我们先研究下它对应的两个描述属性get和set。
在平常,我们很容易就可以打印出一个对象的属性数据:
var person = { name: '小白' }; console.log(person.name); // 小白
如果想要在执行console.log(person.name)的同时,直接给书名加个书名号,那要怎么处理呢?或者说要通过什么监听对象 person 的属性值。
这时候Object.defineProperty( )就派上用场了,代码如下:
var person = { name: '小白' } var value = person.name Object.defineProperty(person, 'name', { set: function (newValue) { value = newValue return '名字是' + value; }, get: function () { return '***' + value + '***' } })
结果展示:
我们通过Object.defineProperty( )设置了对象person的name属性,对其get和set进行重写操作。
顾名思义,get就是在读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数。
实现一个简单完整版的mvvm双向绑定代码。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0"> <title>vuetest</title> </head> <body> <div id="app"> <input v-model="text" id="test"></input> <div id="show"></div> </div> <!-- built files will be auto injected --> </body> <script> var obj = { a:1, b:2 }; var _value = obj.a Object.defineProperty(obj,'a',{ get:function(){ console.log("get方法") return _value }, set:function(newValue){ console.log("set方法") _value = newValue document.getElementById('test').value =_value document.getElementById('show').innerHTML =_value return _value } }); document.getElementById('test').addEventListener('input',function(e){ obj.a = e.target.value; }) </script> </html>
Object.defineProperty的缺点:
无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
https://www.cnblogs.com/YikaJ/p/4278255.html
所以vue才设置了7个变异数组(push、pop、shift、unshift、splice、sort、reverse)的 hack 方法来解决问题。
只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。如果能直接劫持一个对象,就不需要递归 + 遍历了
监听数组的改变
1)先拷贝一份数组原型链
2)定义一个方法总类
3)遍历数组,给数组原型链重新写方法,然后触发更新 dep.notify
proxy代理(详情点击 )
Proxy对象用于定义基本操作的自定义行为,和defineProperty功能类似,只不过用法有些不同
上述例子,用Proxy来替换defineProperty进行数据劫持。
var proxyObj = new Proxy(obj, {
get:function(target, key, receiver){ //我们在这里拦截到了数据
console.log("get方法",target,key,receiver)
return true
},
set:function(target,key,value, receiver){ //改变数据的值,拦截下来额
console.log("set方法",target,key,value, receiver)
target[key]= value
document.getElementById('test').value= value
document.getElementById('show').innerHTML=value
return true
}
})
document.getElementById('test').addEventListener('input',function(e){
proxyObj.a = e.target.value;
})
为什么vue3中改用proxy
1)defineProperty只能监听某个属性,不能对全对象监听,所以可以省去for in 提升效率
2)可以监听数组,不用再去单独对数组做操作
3)Proxy只是代理了原对象,不会污染原对象
那么,在vue中从一个数据到发生改变的过程是什么?
发布者-订阅者模式
‘
、
Observer是个监听器,用来监听vue中的data中定义的属性。
通过Obeject.defineProperty()来监听数据的变动,通过递归方法遍历所有属性值。如果属性发上变化了,会通知给对应的dep。
Dep 在监听器Observer和订阅者Watcher之间进行统一管理的。
主要的作用就是收集观察者Watcher和通知观察者目标更新。每个属性拥有自己的消息订阅器dep,Dep实例里面存放所有订阅了该属性的观察者对象,
当数据发生改变时,会遍历订阅者列表(dep.subs),通过dep.notify()通知Watcher。
depend 实例方法用来收集依赖,notify 实例方法用来触发依赖的执行。经过了 depend => watcher.addDep => addSub
(watcher 表示 Watcher 的一个实例)之后,subs 中收集的依赖实际上都是 Watcher 实例,再经过 notify => watcher.update
之后就可以触发实例化 Watcher 时的渲染函数和回调函数(如果有)的执行了。
Watcher类主要用来收集依赖和触发更新。 主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),
Compile是指令解析器,解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数
}
被监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器subs里面,留待后面数据变更时通知(会先通过观察者id判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器dep添加到自身deps中,方便其他地方使用。
被监听的数据进行赋值操作时(setter)时,就会触发dep.notify(),循环该数据订阅器中的观察者,进行更新操作。
为什么要进行依赖收集?
new Vue({
data(){
return {
name:'zane',
sex:'男'
}
}
})
假设页面只使用到了name,并没有使用sex,根据Object.defineProperty的转换,如果我们设置了this.sex='女',那么Vue也会去执行一遍虚拟DOM的比较,
这样就无形的浪费了一些性能,因此才需要做依赖收集,页面用到了就收集,没有用到就不收集。
首先根据上图实现整体的一个架构,用到订阅发布者的设计模式。
然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。
最后实现V-M,当文本框输入文本,触发更改模型中的数据,及更新相对应的视图。
我们可以先来看一下通过控制台输出一个定义在vue初始化数据上的对象是个什么东西。
<div id="app"> <input v-model="text" id="test"></input> <div id="show"></div> </div>
class Vue{
constructor(options){
console.log(options)
}
}
const app = new Vue({
el: '#app',
data:{
text: 'test'
}
})
打印:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } };
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. * 即浏览所有属性并将其转换为get/set。仅当值类型为“对象”时才应调用此方法。 */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } };
/** * Observe a list of Array items. * 即观察数组项列表 */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };