订阅观察
之前实现了Vue数据改变响应的基本原理,那么我们怎么知道数据改变后具体要修改什么地方的数据或Dom呢。
let vm = new Vue({
template:
`<div>{{text}}<div>`,
data: {
text:233
}
});
//执行时显示
<div>233<div>
//修改text的值
vm.data.text=111
//我们该如何刷新这里的值呢
<div>111<div>
订阅者Dep
我们建立一个Dep,专门用来收集需要更改的Watcher依赖
class Dep {
constructor() {
/* 用来存放所有相关的Watcher依赖 */
this.subs = []
}
/* 添加一个Watcher依赖 */
addsubs(sub) {
this.subs.push(sub)
}
/* window.target中是否有依赖存在 */
depend() {
if (window.target) {
this.addsubs(window.target)
}
}
/* 通知所有相关的Watcher依赖更新视图 */
notify() {
this.subs.forEach(sub => sub.update())
}
}
这样一个Watcher依赖收集的Dep就完成了,你先品品。
我们在把他添加到defineReactive函数中(defineReactive可以看上一章)
defineReactive(obj, key, val) {
if (this.GetType(val) === 'object') {
this.observer(val)
}
let dep = new Dep
Object.defineProperty(obj, key, {
enumerable: true, //可枚举性
configurable: true, //可配置性
get: function reactiveGetter() {
//添加依赖
dep.depend()
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal
/* 通知所有Watcher依赖更新视图 */
dep.notify()
}
});
}
为每一个属性都添加一个独立的Dep,用来收集他相对应的Watcher依赖
let dep = new Dep
let vm = new Vue({
template:
`<div>{{text1}}<div>`,
data: {
text1:233,
text2:666
}
});
以这段为例:
text1中有一个单独的Dep,并且有一个对应的Watcher依赖
text2中有一个单独的Dep,但没有任何依赖
在最初获取时将对应的依赖添加到Dep中
get: function reactiveGetter() {
//添加依赖
dep.depend()
return val;
}
在vm.data.text1 = 555时,触发text1的set属性
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal
/* 通知所有Watcher依赖更新视图 */
dep.notify()
}
通知text1中所有的依赖进行更新
好了你在品品。
window.target和sub.update()可能不知道是干什么的,这个往下看Watcher依赖
观察者Watcher
收集依赖的地方有很多。这里就说一下watch,vm.$watch
收集
class Watcher {
constructor(vm, path, fn) {
this.vm = vm
this.getter = parsePath(path)
this.fn = fn
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm._data)
window.target = undefined
return value
}
/* 更新视图的方法 */
update() {
const oldValue = this.value
this.value = this.get()
this.fn.call(this.vm, this.value, oldValue)
}
}
const bailRE = /[^\w.$]/
function parsePath(path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i in segments) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
let vm = new Vue({
data: {
a: 1,
b: {
c: 233
}
}
})
vm.$watch('b.c', function (newval, oldval) {
console.log('newval', newval)
console.log('oldval', oldval)
})
好了,我们一个个分析
以这个为例
vm.$watch('b.c', function (newval, oldval) {})
constructor(vm, path, fn) {
//vm就是Vue的实例
this.vm = vm
//getter获取的是一个闭包函数,这个在下面的get函数中会使用到
this.getter = parsePath(path)
//监听到数据改变后的回调函数
this.fn = fn
//调用get函数
this.value = this.get()
}
get() {
//设置window.target为这个Watcher依赖
window.target = this
//执行getter获取闭包函数
let value = this.getter.call(this.vm, this.vm._data)
//清除window.target
window.target = undefined
return value
}
最后再看一下`parsePath`函数
const bailRE = /[^\w.$]/
function parsePath(path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i in segments) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
//path='b.c'
this.getter = parsePath(path)
这步返回的是一个闭包函数,且segments=['b','c']
this.getter.call(this.vm, this.vm._data)
改变闭包函数的this指向(虽然里面没用到this)。且值是this.vm._data,Vue中data的所有数据
这样这里就很清楚了,获取到data中'b.c'的值
function (obj) {
for (let i in segments) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
最后说一下整个`watch`的流程吧
1 先是设置vm、getter和fn的初始值
2 调用get函数获取当前的值(这里为例:先获取data.b.c中的值)
3 window.target设置为当前的Watcher依赖
4 访问data.b.c,这样会出触发data.b.c的存取描述符中的get属性。
触发Dep.depend(),触发时window.target中有当前的Watcher依赖。
写入到data.b.c的Dep的subs中。
5 获取data.b.c的数据,并保存在Value中
Watcher中的constructor和get就是为指定的变量的Dep中添加Watcher依赖
插一句
depend() {
if (window.target) {
this.addsubs(window.target)
}
}
为什么要这样设置。看完上面大概就明白了,在Watcher中会多次触发这个变量。
在parsePath函数中更是会多次触发变量的get属性。
'b.c'为例就会触发b的set和c的set。而此时window.target是undefined,push到subs中是无用的。
收集依赖将完了,接着说通知依赖
通知
vm._data.b.c = 555
这时会触发c的存取描述符中的set属性。
调用c的Dep中的notify()函数,通知所有Watcher依赖更新视图。
c触发get时Dep.subs中只有watch一个依赖。执行watch的update()
update() {
const oldValue = this.value
this.value = this.get()
this.fn.call(this.vm, this.value, oldValue)
}
this.value中保留的就是原值,再次调用get()获取更改后的值。
最后执行watch的回调函数,并用call改变this指向传参。
这样一个watch就完成了。
总结一下通知
1 当watch监听的变量改变时触发get属性,统一执行变量所有的依赖
2 执行watch监听这个依赖,执行update其中使用call执行回调函数
最后的最后
class Vue {
...
$watch(path, fn) {
return new Watcher(this, path, fn)
}
}
好了一个订阅观察就完成了。Watcher不只$watch用到,这里就了解一下原理。
最后这里说一下Object.defineProperty存取描述符的一个问题:
set只能在数据改变时触发,新增和删除属性都是无法触发set的(不过get会触发)
但set不触发就不会向依赖发出通知。($set和$detele就是解决这个问题的,这个我先读一下)
下一章我在写数组是怎么侦测的。
