Vue源码解析 1.1 订阅观察

183 阅读2分钟

订阅观察

之前实现了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.targetsub.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中的constructorget就是为指定的变量的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就是解决这个问题的,这个我先读一下)

下一章我在写数组是怎么侦测的。

铁子们,给个三联吧。