新的响应式机制采用了ES6的ProxyApi,抛弃了Object.defineProperty()
Vue2双向数据绑定存在的问题
关于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对property执行 getter/setter转化,所以property必须在 data对象上存在才能让 Vue 将它转换为响应式的
关于数组
Vue 不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
原理
Vue2.0使用Object.defineProperty对象以及对象属性的劫持+发布订阅模式,
只要数据发生变化直接通知变化 并驱动视图更新
<input type="text" id="in"/> 输入的值为:<span id="out"></span>
var int = document.getElementById('in');
var out = document.getElementById('out');
// 定义一个对象
const data={name:'peak',age:10}
// 遍历对象 实现对对象的属性进行劫持
Object.keys(data).forEach((key) => {
Object.defineProperty(data, key, {
// 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中
enumerable: true,
// 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除
configurable: true,
get: ()=>{ // 一个给属性提供 getter 的方法
console.info(`get ${key}-${val}`)
return val;
},
set: (newVal)=>{ // 一个给属性提供 setter 的方法
// 当属性值发生变化时我们可以进行额外操作 如调用监听器
if(newVal === val ){ // 如果未发生变化 不做其他操作
return;
}
console.log(`触发视图更新函数${newVal}`);
out.innerHTML = newVal;
},
});
});
int.addEventListener('input', function(e) {
obj.name = e.target.value;
})
data.age=25 // 触发set方法
vue3双向数据绑定Proxy
Vue3中的响应式采用了ES6中的Proxy方法。
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
语法
const p = new Proxy(target, handler)
参数target表示要使用Proxy包装的对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
参数handler是一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p 的行为
let obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
get: function(target, prop, receiver) {
return prop in target ? target[prop] : 0
},
set: function(target, prop, value, receiver) {
target[prop] = 666
}
})
console.log(proxy.a) // 1
console.log(proxy.c) // 0
proxy.a = 10
console.log(proxy.a) // 666
obj.b = 10
console.log(proxy.b) // 不是666 而是10
以上代码中obj是我们要代理的目标对象,get,set方法是参数handler的两个属性,具体如下:
handler.get()接收三个参数,第一个参数target为代理的目标对象,第二个参数prop是代理的目标对象的属性,第三个参数是Proxy或者继承Proxy的对象,通常是proxy本身。
handler.set()接收四个参数,其中三个参数都与get方法相同,唯独多出来一个value表示新的属性值。
上述代码表示当访问proxy的属性时,进行拦截判断,该属性是否是目标对象的属性,如果是那么就将其值返回出来,否则就返回0。
当对proxy上的属性进行重写时,将重写的该属性赋值为666。
注意: 此时对数据的劫持,只是劫持了代理对象proxy,而跟原对象obj没有任何关系,对obj进行操作,也不会监听到。
用proxy实现一个简易版的数据响应
<body>
<h2 id="app"></h2>
<input id="input" type="text" />
</body>
let app = document.getElementById('app')
let input = document.getElementById('input')
let obj = { // 源数据
text:'hello world'
}
let proxy = new Proxy(obj, {
set: function(target, prop, value){ // input事件触发进行劫持,触发update方法
target[prop] = value
update(value)
}
})
function update(value){ // update方法用于同步dom更新
app.innerHTML = value
input.value = value
}
input.addEventListener('input',function(e){ // 监听input数据变化,并修改proxy的值
proxy.text = e.target.value
})
proxy.text = obj.text // 初始化源数据
理论:
v2是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图,实现数据和视图同步。
v2双向数据绑定的具体实现步骤:
第一步: 需要observer(观察者)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步: compile(模板解析器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步: Watcher(订阅者)是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调
第四步: MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
v3 使用发布者订阅模式,结合ES6的prosy的set和get方法来进行数据劫持,进一步结合watchr的update方法