告白-vue双向绑定(二)响应式更新,原理分析+陪你读源码

191 阅读7分钟

第一篇,我主要介绍了这种编程思想,那么本文主要是介绍他们是如何实现的,我之前的文章已经对 Object.defineProperty 有了一个详细的解释,那么我们现在就直奔主题吧。

发布者(对象的监听)

我希望我讲的大家通过看一遍和自己调试一遍可以看懂。 我继续拿 yy 关注女主播 这个故事来说事, 女主播现在成了网红,所以她的一举一动都备受宅男的关注。那么得有一个工具来实现对 女主播24小时的跟进,我们才能实时的知道他的动态这个工具用来发布消息:

// 这是一个女主播的对象
let yyNvzhuo = {
    name:"女疯子",
    sex:"女",
    age:16,
    msg:"打游戏中"
}
//首先我要监听这个mm 
// 对数据进行监听
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key])
    })
}
/**
 * data 对象
 * key 属性名
 * val 属性值
*/
function defineReactive(data, key, val) {
        observe(val); // 递归到最底层 val 不是对象时候 就是最底层了
        var deps = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
             console.log(val,'看女主播的信息')
               if (Dep.target) { // 判断是否需要添加订阅者
                    deps.addSub(Dep.target); // 在这里添加一个订阅者
                }
                return val;
            },
            set: function (newVal) {
             console.log(val,'女主播的信息改变了')
                val = newVal;                
                deps.notify(); //当值改变的时候 触发
            }
        });
    }
observe(yyNvzhuo) //对女主播进行监听,这时候女主播的数据改变 或者 获取 会触发 get 或 set
yyNvzhuo.age // 16 "看女主播的信息"
yyNvzhuo.sex // 女 看女主播的信息
yyNvzhuo.age = 17 女主播的信息改变了

我们这里看下 vue.js里面是怎么写的

function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
    var dep = new Dep(); // vue的中间件
    var property = Object.getOwnPropertyDescriptor(obj, key); // 获取我们要观察对象属性值
    if (property && property.configurable === false) { //看看能不能被修改不能修改就跳出
      return
    }
    
    // cater for pre-defined getter/setters
    var getter = property && property.get; //如果 property存在 把get 属性只给 getter
    var setter = property && property.set; // 同上
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }
    
    var childOb = !shallow && observe(val); // childOb是否有被监听 
    //源码实现和考虑的功能远远比现在我们做的复杂,所以大家暂时仅仅做为参考,如果大家关注量多我会在以后时间里面一步步的讲解
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) { // 中间件是否有寄存的对象
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify(); //发布消息给 订阅函数 即watcher
      }
    });
}

到这是不是简单易懂呢?接下来我们把这个又控制台打印方式放到 body 中显示

/**
 * data 对象数据
 * 要绑定的元素
 * 要关注的属性名称
*/
function mvvm(data,el,key){
    this.data = data;
    observe(data)
    el.innerHTML = data[key]
}
var ele = document.querySelector('#name');
let htmlnvzhubo = new mvvm(yyNvzhuo,ele,'msg')
//这样我们就把我们要关注 女主播的 msg 打印在了  html 

别急,在只是实现了一个初次的绑定 我们还需要在 msg 发生改变的时候 更新 html,说道这里大家应该很清楚了observe 这个函数用来监听函数的变化并且把消息及时的通知出去(发布消息的功能,发布者)

set: function (newVal) {
 console.log(val,'女主播的信息改变了')
 // 添加一个方法 来实现   el.innerHTML = newVal //显然直接写在这里是简单粗暴的,我们(一)里面讲的设计模式就白讲了
    val = newVal;    
}

中间件

那么现在我们把思绪拉到 (-)里面 我们 要采用 订阅发布 这种 面向对象的编程思想 需要订阅 、 发布、 和一个中间件 这么一个流程 显然 defineProperty 中的 set 和 get 就是订阅 和 发布 ,为了方便大家对vue源码的理解,这里我的方法和vue的保持一致,同样来实现一个简化版的功能

//中间件
var Dep = function(){
//存消息用
    this.subs = []
}
//订阅 get 用来存储订阅者的需求
Dep.prototype.addSub = function(sub){
    this.subs.push(sub) // 这里的 fn 就是 dep.targer 它是什么呢是 一个 new 的watcher
}
// 发布 set 执行 我们页面的更新逻辑用来执行 触发订阅者 想要做的事情
Dep.prototype.notify = function(){
    this.subs.forEach(fn=>{
        fn.updata() // 执行 watcher 里面的 updata
    })
}
Dep.targer = null
看到这里大家是不是觉得和我们第一节里面的内容很类似,虽然函数变了,但是思想没有变,毕竟 设计模式 是思想而不是固定的方程式。

中间件源码,这里很类似,我们就不做过多的介绍了

  var uid = 0;
  /**
   * A dep is an observable that can have multiple
   * directives subscribing to it.
   */
  var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

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

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, 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();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

  // The current target watcher being evaluated.
  // This is globally unique because only one watcher
  // can be evaluated at a time.
  
  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }

有了中间件,接下来就是 一个接收消息的人啦:

订阅者(观察者)

这个函数主要作用就是 接收消息并作出事件的反馈那么我们继续实现功能代码

//我们想下这个函数主要用来做的事情
/*
*观察女主播这个对象,当其变化时候做出反应
*1.获取女主播这个对象属性vm
*2.选择一个属性 exp
*3.事件fn
*/
var  Watcher = function(vm,exp,fn){
    this.vm = vm;
    this.exp = exp;
    this.fn = fn;
    this.value = this.get();  // 将自己添加到订阅器的操作 同时返回 一个当前属性的值
}
Watcher.prototype.update: function () {
    this.run(); // 如果监听的属性更新之后触发 run
}
Watcher.prototype.run: function () {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) { // 是否有数据的变更
        this.value = value;
        this.fn.call(this.vm, value, oldVal); 
    }
}
Watcher.prototype.get: function () {
    Dep.target = this;  // 缓存自己
    var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数  也可以或是监听哪个值
    // 为了把 它 
    Dep.target = null;  // 释放自己
    return value;
}

我们同样看下源码,他的代码实现同上面的原理是类似的

 /**
   * A watcher parses an expression, collects dependencies,
   * and fires callback when the expression value changes.
   * This is used for both the $watch() api and directives.
   */
  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm; // 把我们观察的对象函数 存到 vm里面
    if (isRenderWatcher) { // 判断源
      vm._watcher = this;
    }
    vm._watchers.push(this); // 对观察的 对象进行一个存储
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
   // 用来触发 defineProperty 的get 
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

  /**
   * Add a dependency to this directive.
   */
  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); //  中间的存储 this 
      }
    }
  };

  /**
   * Clean up for dependency collection.
   */
  Watcher.prototype.cleanupDeps = function cleanupDeps () {
    var i = this.deps.length;
    while (i--) {
      var dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    var tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  };

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  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);
        }
      }
    }
  };
  // 下面是vue更细节的考虑,我们不在这里太多的叙述
  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };

  /**
   * Depend on all deps collected by this watcher.
   */
  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };

  /**
   * Remove self from all dependencies' subscriber list.
   */
  Watcher.prototype.teardown = function teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      var i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  };

最后我们把这些链接起来

function mvvm(data, el, exp) {
    this.data = data;
    observe(data); // 添加了这个对象的监听 但是并没有和 属性绑定在
    el.innerHTML = this.data[exp];  //  这个方法 触发了 属性监听的get 方法 并把内容注入到节点里面

    /// 实现功能 对指定的内容进行监听,把更新的内容更换出来
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    //把 this 对象返回 this对象 包含 了 data 
    return this;
}
 var ele = document.querySelector('#name');

var selfVue = new SelfVue(yyNvzhuo, ele, 'msg');

通过这3大块我们实现了双向绑定,和这3个部分的源码分析,双向绑定我们仅仅实现了一个简单 对象 监控,还有其他的内容,且待下回再看 喜欢的点赞吧。