这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战
1. 什么是响应式?
数据变化,视图(也就是DOM)会自动变化。
我们在使用vue差值表达式时,改变数据后,页面上相应数据会自动改变。
2. 实现一个响应式系统,需要做些什么?
我们要实现的是:数据A改变时,视图中用到数据A的所有地方,都要发生改变。
现在将上面的步骤拆为3步:
监听数据A的变化 ---数据劫持(当数据变化时,我们可以做一些特定的事情)收集所有依赖于数据A的元素 ---依赖收集(我们要知道那些视图层的内容(DOM)依赖了哪些数据(state))通知上述这些依赖于数据A的元素更新---派发更新(数据变化后,如何通知依赖这些数据的DOM)
vue实现响应式主要做了这么几件事:
数据劫持:new Vue的时候遍历data对象,用Object.defineProperty给所有属性加上了getter和setter依赖收集:render的过程(执行render()时),会触发数据的getter,在getter的时候把当前的watcher对象收集起来派发更新:setter的时候,遍历这个数据的依赖对象(watcher对象),进行更新
3. 数据劫持
Vue2的响应式是利用Object.defineProperty()实现的,vue3改成了proxy, 本文先讨论Vue2。
在读取某个属性值时,会触发Object.defineProperty()中的get方法;
在设置某个属性的值时,会触发Object.defineProperty()中的set方法;
我们可以给某个对象的属性,通过Object.defineProperty()设置get和set方法,来监听这个属性的变化,也就实现了对这个属性的数据劫持。
举例:
let obj = {}
let val = 1;
Object.defineProperty( obj, 'a', {
get() {
console.log('有元素访问a属性啦!')
return val;
},
set(newVal) {
if (val === newVal) return
console.log('设置a属性的值为:'+newVal)
return newVal;
}
} )
console.log('obj.a', obj.a);
obj.a = 4
从这个例子中我们可以看到,需要一个全局变量val来保存这个属性的值,周转get和set,因此,我们可以定义一个名为defineReactive() 函数(用于提供一个
闭包的环境)。
// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
闭包有内外两层函数,上述代码中,defineReactive是外层函数,get和set是内层函数。val替代了上面临时的全局变量的作用。
上面的代码只能侦测对象的某一个属性,我们需要递归侦测对象的全部属性。
递归侦测对象的全部属性
新建一个Observer类,将一个正常的object转换为全部属性都被侦测的对象。
class Observer {
constructor(value) { // 这里的value是new Observer传入的参数,也就是某个正常obj
this.value = value;
this.walk();
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
const obj = { a:1, b:2 }
new Observer(obj)
Observer类中,接收的参数value是要监听的对象,Object.keys(对象),会返回一个由对象所有属性组成的数组。对此数组进行遍历,让监测的对象带着每一个属性都执行一遍defineReactive函数,即可让对象obj的全部属性都被侦测。
深度侦测对象的多层嵌套属性
上述的Observer类,已经可以侦测将某个对象的全部属性,但如果某个属性的值,还是一个对象,那么无法侦测这个位于属性值的对象。我们需要使用递归来完成嵌套属性的数据劫持。
递归函数需要:
- 结束条件: 属性值不是对象时,结束
- 递推关系:对象使用new Oberser来处理;对象的属性值也使用new Oberser来处理;对象的属性值的属性值也使用new Oberser来处理...
使用递归来完成嵌套属性的数据劫持:
// 入口函数
function observe(data) {
if (typeof data !== 'object') return
// 调用Observer
new Observer(data)
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
// 遍历该对象,并进行数据劫持
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
function defineReactive(data, key, value = data[key]) {
// 如果value是对象,递归调用observe来监测该对象
// 如果value不是对象,observe函数会直接返回
// value是对象的属性值
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue) // 设置的新值也要被监听
}
})
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj)
完成嵌套属性的数据劫持的代码,是defineReactive函数里的observe(value),在被监测对象的某个属性下,再次监测这个属性的属性值,如果这个属性值是对象,再调用Observer,对这个是对象的属性值 的 属性进行数据劫持;... 直到某个属性下,属性值不是对象,直接return;
注意: 设置的新值也要被监听。执行了observe(obj)后,用户可能再对obj添加新属性。
递归函数是指自己调用自己,首先调用observe(obj)函数,通过observe函数,调用Oberver类,在Observer类中,对obj的每个属性都调用defineReactive函数 实现对obj的全部属性进行侦测。
接着,在defineReactive函数中,又对每个属性的属性值,调用observe(属性值)函数,如果某个属性的属性值A不是对象,那么observe(属性值A)函数return,不做处理。如果某个属性的属性值A是对象,那么observe(属性值A)函数,通过observe函数,调用Oberver类,在Observer类中,对属性值A的每个属性都调用defineReactive函数 实现对属性值A的全部属性进行侦测。
接着,在defineReactive函数中,又对属性值A的每个属性的属性值,调用observe(属性值)函数,...
observe、Observer、defineReactiv的调用关系: