Vue
内部使用了Object.defineProperty()
来实现数据响应式,通过这个函数可以监听到set
和get
的事件
var data = { name: 'poetries' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 自定义函数
get: function reactiveGetter() {
console.log('get value')
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
}
})
}
以上代码简单的实现了如何监听数据的
set
和get
的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,从能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集
<div>
{{name}}
</div>
- 在解析如上模板代码时,遇到
{name}
就会进行依赖收集。 - 接下来我们先来实现一个
Dep
类,用于解耦属性的依赖收集和派发更新操作
// 通过 Dep 解耦属性的依赖和更新操作
class Dep {
constructor() {
this.subs = []
}
// 添加依赖
addSub(sub) {
this.subs.push(sub)
}
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
以上的代码实现很简单,当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用
notify
。
接下来我们先来简单的了解下
Vue
组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用Object.defineProperty()
,然后实例化Watcher
,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
- 因为这一小节主要目的是学习响应式原理的细节,所以接下来的代码会简略的表达触发依赖收集时的操作。
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
以上就是
Watcher
的简单实现,在执行构造函数的时候将Dep.target
指向自身,从而使得收集到了对应的Watcher
,在派发更新的时候取出对应的Watcher
然后执行update
函数。
接下来,需要对
defineReactive
函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的
getter
来实现依赖收集。
- 现在我们就来测试下代码的效果,只需要把所有的代码复制到浏览器中执行,就会发现页面的内容全部被替换了
var data = { name: 'poetries' }
observe(data)
function update(value) {
document.querySelector('div').innerText = value
}
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'