在我们使用vue的时候,我们知道,如果我们修改vue实例当中data里面的值,那么对应该数据的视图也会很快得到更新,这就是响应式系统。响应式系统,实现原理就是Object.defineProperty(obj, prop, descriptor)。具体讲讲怎么实现,下面只是讲一个简单的例子,实现了依赖的收集和触发。
Object.defineProperty
,Vue.js就是基于它实现「响应式系统」的。
使用方法:
/*
obj: 目标对象
prop: 需要操作的目标对象的属性名
descriptor: 描述符
return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)...
descriptor的一些属性,简单介绍几个属性。
1、enumerable,属性是否可枚举,默认 false。
2、configurable,属性是否可以被修改或者删除,默认 false。
3、get,获取属性的方法。
4、set,设置属性的方法。
一般来说,给对象添加属性会这样写:
let object = {}
object.test = 'test'
Object.defineProperty
也能做到同样的效果
let object = {}, test = 'test'
Object.defineProperty(object, 'test', {
configurable: true, // 描述该属性的描述符能否被改变,默认值为 false
enumerable: true, // 能否被遍历,比如 for in,默认值为 false
get: function(){ // 取值的时候调用,object.test,默认值为 false
console.log('enter get')
return test
},
set: function(newValue){ // 设置值的时候使用
console.log('enter set')
test = newValue
}
})
这样,可以让属性有了控制控制属性取值和设置值的权利,运行结果:
object.test
// enter get
// test
object.test = 'test2'
// enter set
// test2
先封装改造definproperty方法,方便我们的调用:
let callback = {
target: null
}
let defineReactive = function(object, key, value){
let array = []
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get: function(){
if(callback.target){
array.push(callback.target)
}
return value
},
set: function(newValue){
if(newValue != value){
array.forEach((fun)=>fun(newValue, value))
}
value = newValue
}
})
}
可以从代码中看出来,我在函数内部声明了一个数组用于存放 callback
中的 target
,当对 object
进行 get
操作(取值操作)的时候,就会往 callback
中存放函数,进行 set
操作(设置值)的时候执行数组中的函数。此数组的意义其实就是,每次获取对象的值的时候,都会将依赖到该值的一些dom、计算属性等放到这个数组,当我们进行set操作时,运行数组里面的方法,就可以对涉及到这个值的依赖作更新,例如更新dom视图。
看看效果如何:
let object = {}
defineReactive(object, 'test', 'test')
callback.target = function(newValue, oldValue){console.log('我被添加进去了,新的值是:' + newValue)}
object.test
// test
callback.target = null
object.test = 'test2'
// 我被添加进去了,新的值是:test2
callback.target = function(newValue, oldValue){console.log('添加第二个函数,新的值是:' + newValue)}
object.test
// test
callback.target = null
object.test = 'test3'
// 我被添加进去了,新的值是:test3
// 添加第二个函数,新的值是:test3
这样我们就达成了在 object.test
的值发生改变时,运行一个函数队列(虽然这个队列挺简陋的)的目的。
换个说法,当我们取值的时候,函数自动帮我们添加了针对当前值的依赖(例如使用了该值的dom),当这个值发生变化的时候,处理了这些依赖,比如说 DOM
节点的变化(更新对应的dom视图)。
这个也是 VUE
中实现 MVVM
的最核心的代码,当然在 VUE
中,这个依赖收集的过程远比现在的代码要复杂,这里仅仅实现了依赖的收集和触发,对于依赖的管理这里的代码还做不到。
这里有一个更加易懂的解释:
当声明一个vue对象时,在执行render
函数获取虚拟dom的这个过程中,已经对render
中依赖的data
属性进行了一次获取操作,这次获取操作便可以拿到所有依赖。
其实不仅是render
,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computed
和watch
的原理
首先需要写一个依赖收集的类,每一个data
中的属性都有可能被依赖,因此每个属性在响应式转化(defineReactive
)的时候,就初始化它。代码如下:
class Dep {
constructor() {
this.subs = []
}
add(cb) {
this.subs.push(cb)
}
notify() {
console.log(this.subs)
this.subs.forEach((cb) => cb())
}
}
function defineReactive(obj, key, val, cb) {
const dep = new Dep()
Object.defineProperty(obj, key, {
// 省略
})
}
那么执行过程就是:
- 当执行render
函数的时候,依赖到的变量的get
就会被执行,然后就把这个 render
函数加到subs
里面去。
- 当set
的时候,就执行notify
,将所有的subs
数组里的函数执行,其中就包含render
的执行。
注:代码中有一个Dep.target
值,这个值时用来区分是普通的get
还是收集依赖时的get
参考自:http://blog.acohome.cn/2018/04/11/vue-defineproperty/