通过vue源码浅谈双向绑定原理

双向绑定的原理——Object.defineProperty和发布订阅者模式

这两个分别的概念:

(1)Object.defineProperty:其实是定义对象的属性,其实并不是为一个对象做双向数据绑定的,而是去给对象属性标签,只不过属性里的get和set实现了响应式。

(2)发布订阅者模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

上图,网上找的便于理解的图解释observer和compile,watcher三者的关系

image

接下来通过源码(减少很多理解复杂的代码)解析双向绑定的原理:

1、创建观察者observer

遍历data的属性,利用Object.defineProperty给对象每个属性添加get和set方法,当数据改变,触发set,获取数据又触发了get的方法。

image

2、创建订阅器Dep

此时我们还需要一个以来收集器(订阅器)dep来收集依赖,在添加get属性时,使用dep.depend()就把各自添加进dep里,在set中,每次改变数据时,触发dep.notify(),去更新依赖。observer的修改,得:

var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            protoAugment(value, arrayMethods);
        } else {
            copyAugment(value, arrayMethods, arrayKeys);
        }
        this.observeArray(value);
    } else {
        this.walk(value);
    }
};
Observer.prototype.walk = function walk(obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
        defineReactive$$1(obj, keys[i]);
    }
};
function defineReactive$$1(obj, key, val, customSetter, shallow) {
    var dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            val = newVal;
            dep.notify();
        }
    });
}

3、定义订阅器Dep

var Dep = function Dep() {
    this.id = uid++;
    this.subs = [];
};

Dep.prototype.addSub = function addSub(sub) {
    this.subs.push(sub);
};

Dep.prototype.depend = function depend() {
    if (Dep.target) {
        Dep.target.addDep(this);
    }
};

Dep.prototype.notify = function notify() {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

4、Watcher就是一个订阅者。

用于将dep发来的update消息处理,通过dep.addSub()和watcher.addSub()关联起来,执行Watcher绑定的更新函数,触发run(),进而触发this.cb.call(this.vm, value, oldValue);更新视图

Watcher.prototype.addDep = function addDep(dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if (!this.depIds.has(id)) {
            dep.addSub(this);
        }
    }
};
Watcher.prototype.update = function update() {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true;
    } else if (this.sync) {
        this.run();
    } else {
        queueWatcher(this);
    }
};

/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
Watcher.prototype.run = function run() {
    if (this.active) {
        var value = this.get();
        if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
        ) {
            // set new value
            var oldValue = this.value;
            this.value = value;
            if (this.user) {
                try {
                    this.cb.call(this.vm, value, oldValue);
                } catch (e) {
                    handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                }
            } else {
                this.cb.call(this.vm, value, oldValue);
            }
        }
    }
};

5、实现一个Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

image

实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher

new Watcher(vm, exp, function(value, oldValue) {
     // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
    updaterFn && updaterFn(node, value, oldValue);
});

涉及到比较重要一点,vue中数组只有push,pop等方法更新,才能双向数据绑定,下标的方式arr[0]则不能的原因:

源码中对数组的方法进行了封装

  var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
   * Intercept mutating methods and emit events
   */
  //装饰者模式
  //既保持原有方法,又触发更新
  methodsToPatch.forEach(function (method) {
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];
      //触发原有的方法
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      //触发更新
      ob.dep.notify();
      return result
    });
  });

✳✳✳另外一提,vue3已经改用proxy来实现双向绑定原理,defineProperty只能监听某个属性,不能对全局对象监听,因为要循环,而proxy可以对全局对象进行监听,省去for in提升效率,还可以监听数组,不用再去单独对数组做特异性操作

(如有错误,欢迎指正)

你可能感兴趣的:(通过vue源码浅谈双向绑定原理)