likes
comments
collection
share

《源码系列》助你理解vue响应式源码——实现Watcher观察者

作者站长头像
站长
· 阅读数 3

前言

在上一节把数据劫持简单的实现了一下,但是目前我们只能在控制台中测试看到数据的变化,这节会在原来的基础上逐渐丰富。这节我们要实现的是Watcher观察者,即数据变化后视图也会进行更新,此外还会使用Dep订阅器来对观察者进行收集。

实现Dep订阅器

作用

  1. 添加观察者
  2. 通知观察者去更新视图

实现

既然要对观察者进行添加,那么我们可以在Observer.js文件中写出以下代码:

class Dep {
    constructor() {
        this.subs = [];
    }
    // 收集观察者
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 通知观察者
    notify() {
        this.subs.forEach(w => w.update());
    }
}

通过addSub方法实现第一个对观察者的添加,notify方法会通知每一个观察者去更新视图,所以我们还需要在观察者中定义一个update方法。

实现Watcher观察者

作用

  1. 检查旧值和新值有没有变化
  2. 有变化则调用更新视图的方法

实现

接着我们需要继续在Observe.js文件中新增Watcher类,因为我们首先需要在里面获取到旧值和新值,然后在进行比较,所以我们需要传递一些可以获取旧值和新值的参数。

class Watcher {
     constructor(vm, expr, cb) {
       this.vm = vm;
       this.expr = expr;
       this.cb = cb;
       this.oldValue=this.getOldValue()
   }
     getOldValue() {
       const oldValue=compileUtil.getVal(this.expr,this.vm);
       return oldValue
    }
}

上述代码中在getOldValue方法中通过再次调用compileUtil中的getVal方法来获取当前的值,其中cb参数是为了把新值回调到模版编译里即更新视图,也就是说会形成一个闭环,别着急接着往下看。

    update() {
        const newValue=compileUtil.getVal(this.expr,this.vm);
        if(newValue!==this.oldValue){
            this.cb(newValue);
        }
    }   

声明update方法来比较旧值和新值,如果有变化就把新的值回调回去。

《源码系列》助你理解vue响应式源码——实现Watcher观察者

再来看这张图,现在DepWatcher已经创建好了,它们之间怎么进行关联呢?由图可知,需要把ObserverDepDepWatcherCompileWatcher进行关联。

也就是说在订阅数据变化时要在往Dep中添加订阅者,所以这一步可以放在监听数据中的getter的时候进行,但是订阅者Watcher从哪里来呢?这里就用到了一巧妙的方式,就是在进行模版编译时拿v-html举例,在获取v-html指令对应的值的时候我们就给它创建一个Watcher观察者并绑定更新函数,这也就是为什么在创建Watcher类中传递cb参数的原因。

    html(node, expr, vm) {
        const value = this.getVal(expr, vm);
        // 绑定对应的watcher 订阅数据变化 绑定更新函数
        new Watcher(vm, expr, (newVal) => {
            this.updater.htmlUpdater(node, newVal)
        })
        this.updater.htmlUpdater(node, value);
    }

绑定了Watcher类之后我们可以在Watcher类中的getOldValue方法中直接把当前Watcher实例对象的this赋值给Dep,这样一来getter中的Dep就可以顺利添加观察者了。

    // Watcher
    getOldValue() {
        Dep.target = this;
        const oldValue = compileUtil.getVal(this.expr, this.vm);
        // 保证只添加一次观察者
        Dep.target = null;
        return oldValue
    }
    // Observer
    defineReactive(obj, key, value) {
        this.observer(value)
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 订阅数据变化时往Dep中添加观察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
        })
    }

在数据发生变化时需要调用Dep中的notify方法,notify方法会根据当前的值与旧值进行比较,有变化就会把新的值回调到更新页面的方法中去,这样就形成了一个闭环!

    defineReactive(obj, key, value) {
        this.observer(value)
        let dep=new Dep();
        Object.defineProperty(obj, key, {
            set: (newVal) => {
                this.observer(newVal)
                if (newVal != value) {
                    value = newVal
                    dep.notify();
                }
            }
        })
    }

这样整个流程基本上就差不多完成了,最后再总结一下吧。

总结

题目:vue是如何实现响应式的/vue中的双向数据绑定原理?

vue的双向数据绑定主要是由CompileObserverDepWatcher四部分组成,作用分别是Compile用来初始化视图,对页面中的一些指令或属性进行解析(就是根据书写的一些vue语法在data中找到对应定义的值并渲染到页面中的过程),在这个过程中Compile还会对页面用到的每个值进行观察者创建即绑定更新函数,用于更新视图。

Dep中主要通过addSub方法来添加订阅者以及使用notify方法通知watcher去更新视图。

Watcher主要用来更新视图,通过getOldValue获取到当前的值与旧值进行比较如果有变化会立即执行在模版编译阶段传递的回调函数进行数据的替换。

Observe的作用就是使用Object.defineProperty方法对所有数据进行劫持监听,在get方法中进行依赖收集并往Dep中添加订阅者,在set的时候会通知Dep中的观察者更新视图。

完结撒花!