前面我们已经处理好了数据,并将数据都显示到了页面上,接下来要做的就是实现数据的响应式。 在 Vue
的响应式模式中我们需要用到前几篇说过的观察者模式来实现,观察者模式中就有 Dep(目标)
和 Watcher(观察者)
。
观察者模式
先回顾一下 Dep
类和 Watcher
类是怎么写的:
Dep 类
js
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher 类
js
class Watcher {
update () {
console.log('update')
}
}
在案例中使用观察者模式
我们需要给 data
中的每一个属性创建一个对应的 dep
对象,在视图初始化的时候遍历视图上的元素,将视图上用到 data
属性的地方都创建一个对应的 watcher
对象,并将这个 watcher
添加到对应属性的 dep
对象的 subs
数组中去,这一步叫收集依赖。然后在 data
中的属性发生变化时,调用 dep
对象的 notify
发送通知,调用其中 watcher
对象的 update
方法更新视图。
observer 中收集依赖
- 给
data
中的每一个属性创建一个对应的dep
对象
我们可能自然的想到在将 data
中的数据转换为响应式的时候,就遍历了整个 data
对象,那我们就可以同样在这个遍历中给每个 data
创建对应的 dep
对象:
js
defineReactive (obj, key, val) {
let that = this
// 负责收集依赖,并发送通知
+ let dep = new Dep()
this.walk(val)
...
}
- 将
watcher
添加到对应属性的dep
对象的subs
数组中去
在调用 defineReactive
的时候,视图还没有初始化,watcher
实例还没有被创建,所以我们不能直接在这个方法里调用 dep.addSub
添加观察者。但是我们其实很难把握视图初始化的时间,所以我们可以把 dep.addSub
放在 data
属性的 getter
中,当视图解析到这个属性时,就会调用这个属性的 getter
方法,此时我们再将创建的 watcher
添加到 subs
数组中。
那还有一个问题是我们怎么将 watcher
传递到 getter
方法中,最简单的就是直接在 Dep
类上添加一个 target
静态属性,在获取 data
中的属性触发 getter
方法之前,将 watcher
实例赋值给 Dep.target
,然后在 getter
方法之后重置 Dep.target
为 null
。
js
defineReactive (obj, key, val) {
...
Object.defineProperty(obj, key, {
...
get () {
// 收集依赖
+ Dep.target && dep.addSub(Dep.target)
return val
},
...
})
}
同步更新 watcher 类:
js
class Watcher {
constructor (vm, key) {
Dep.target = this // 将 watcher 实例赋值给 Dep.target
vm[key] // 触发属性的 getter,调用addSub
Dep.target = null
}
// 当数据发生变化的时候更新视图
update () {
console.log('update')
}
}
observer 中发送通知
data
属性改变时,调用dep
对象的notify
发送通知
修改 data
属性时,会触发其 setter
,所以我们需要在属性的 set
方法中调用 dep.notify
发送通知:
js
defineReactive (obj, key, val) {
...
Object.defineProperty(obj, key, {
...
set (newValue) {
...
that.walk(newValue)
// 发送通知
+ dep.notify()
}
})
}
compiler 中更新视图
- 遍历视图上的元素,将视图上用到
data
属性的地方都创建一个对应的watcher
对象
以插值表达式为例,当 data
中的 msg
发生改变时,视图上的 {{msg}}
也要发生改变。
这是对于 Dom
的操作,所以写在 compiler
类中最为合适。而处理插值表达式、各种指令所做的 Dom
操作也不同,所以要写在不同的处理方法中。对于插值表达式,我们需要在 compilerText
方法中创建相关属性的 watcher
实例。
在创建 watcher
实例的时候还要传入一个 cb
回调函数,表示数据改变后视图对应要执行的方法。这个 cb
方法还需要接收改变后的值 newValue
,以做出响应。
js
compileText(node) {
...
if (reg.test(value)) {
+ let key = ''
node.textContent = value.replace(reg, (match, p1) => {
+ key = p1.trim()
return this.vm[key]
})
// 创建watcher对象,当数据改变更新视图
+ new Watcher(this.vm, key, (newValue) => {
+ node.textContent = newValue
+ })
}
}
完善 Watcher 类
为了获取到属性最新的值,我们要通过 vm[key]
获取,所以在构造函数中要存储 vm
和 key
两个属性。同时在更新视图前,需要判断修改后的 data
属性是否和以前不同,如果是一样的不做更新。 同步更新 watcher
类:
js
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.oldValue = vm[key]
Dep.target = null
}
update () {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) return
this.cb(newValue)
}
}
代码测试
以上已经实现了数据的响应式,接下来我们将 dep
文件和 watcher
文件导入到 html
中,做一个测试。
当我们在控制台修改 vm.msg = 'test'
后发现视图上的对应的插值表达式也做出了相应改变。
但是当我们想要设置 vm.msg = 'Hello Vue'
时,视图却并不改变,这是因为我们在视图初始化的时候存储了 oldValue
的值为 'Hello Vue'
,在 update
中判断新旧值相等就不会执行视图的更新,这个问题我们后续再处理,暂时不管。
添加指令的 watcher
我们已经完成了插值表达式的响应式,按照这个逻辑再去添加 v-text
和 v-model
指令的响应式很简单,只需要在 textUpdater
和 modelUpdater
方法里创建 watcher
实例传入不同的回调即可:
js
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
textUpdater (node, value, key) {
node.textContent = value
+ new Watcher(this.vm, key, (newValue) => {
+ node.textContent = newValue
+ })
}
modelUpdater (node, value, key) {
node.value = value
+ new Watcher(this.vm, key, (newValue) => {
+ node.value = newValue
+ })
}
这里可以解释一下为什么在 update
方法中我们不直接调用 updateFn
而是使用 call
来调用,就是为了绑定 this
的值,这样才可以在 textUpdater
和 modelUpdater
方法里直接访问 this.vm
来获取 vue
实例,当然直接使用 this[attrName + 'Updater'](node, this.vm[key], key)
也可以。
到这里我们的响应式代码就差不多写完了,接下来再处理一些问题就可以了。