Vue2源码理解-Observer、Dep、Watcher

1.前言

Vue依靠数据驱动视图,本篇主要描述vue是如何侦测数据变化的。在Vue2中数据侦测主要是基于 Object.defineProperty() 实现的,数组的变化侦测主要依靠改写数组方法实现。

下面主要是一些自己理解源码和尝试还原的一些过程,及遇到的问题。实现的不全面,可以作为参考(欢迎各位大佬留评)。
主要涉及源码位置
src/core/observer/index.js
src/core/observer/dep.js
src/core/observer/watcher.js

2.数据侦测实现

  • Object.defineProperty()

    • Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
    • @param obj 要定义属性的对象
    • @param prop 要定义或修改的属性的名称可为Symbol
    • @param descriptor {{configurable?: boolean, enumerable?: boolean, value?: any, writable?: boolean, get?: Function, set?: Function}} 要定义或修改的属性描述符,及对象属性的描述对象字段
      • -configurable:
      • -作用:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
      • -默认值:false
      • -enumerable:
      • -作用:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
      • -默认值:false
      • -value:
      • -作用:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
      • -默认值:undefined
      • -writable:
      • -作用:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (=, *=等)改变。
      • -默认值:false
      • -get:
      • -作用:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
      • -默认值:undefined
      • -set:
      • -作用:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
      • -默认值:undefined
    • 此方法详情可见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  • 实现数据的可侦测

    我们先定义一个对象

    var obj = {
      name: 'obj',
      value: 'obj_value',
      child: {
        name: 'child',
        value: 'child_value'
      }
    }
    

    这个对象能被我们读取到,但是当对其中的属性做修改时,我们无法监测到,我们要想能监测到就需要用到上面提到的 Object.defineProperty(),我们再看下面一段代码

    var obj = {};
    var val = 12;
    Object.defineProperty(obj, 'value', {
      enumerable: true,
      configurable: true,
      get: function () {
        console.log('value属性被获取了');
        return val;
      },
      set: function (newValue) {
        console.log('value属性被修改了');
        val = newValue
      }
    });
    var v = obj.value;
    // value属性被获取了
    obj.value = 24;
    // value属性被修改了
    

    上面代码实现了一个属性可侦测获取及修改,下面将上面代码进行下改造,使传入一个定义好的对象使其变的可观测

    /**
     * 循环对象的key使每个属性转变为可观测对象
     * @param obj
     */
    function walk (obj) {
      var keys = Object.keys(obj);
      for (var i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i]);
      }
    }
    
    /**
     * 判断是否Object,是否需要转变为可观测对象
     * @param obj
     */
    function observe (obj) {
      // 本篇只针对对Object的观测,数组不做实现
      if (Object.prototype.toString.call(val) === '[object Object]') {
        walk(val);
      }
    }  
    /**
     * 单个属性可侦测
     * @param obj
     * @param key
     * @param val
     */
    function defineReactive (obj, key, val) {
      // 这里判断下是否传入了val值, 没传入则使用obj[key];
      if (arguments.length === 2) {
        val = obj[key]
      }
      // 如果是Object做递归处理
      observe(val);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
          console.log(['获取', '"', key, '"', '的值'].join(''));
          return val;
        },
        set: function (v) {
          // 判断新值是否和原值一样,一样不做操作
          if (val === v) {
            return;
          }
          console.log(['修改', '"', key, '"', '的值'].join(''));
          val = v;
          // 如果新值为Object,转变为可观测对象
          observe(v);
        }
      })
    }
    
    var obj = {
      name: 'obj',
      value: 'obj_value',
      child: {
        name: 'child',
        value: 'child_value'
      }
    }
    // 调用
    walk(obj);
    

    参照Vue源码再次改造实现Observer类

    function observe (obj) {
      // 本篇只针对对Object的监测,数组不做实现
      if (Object.prototype.toString.call(val) === '[object Object]') {
        new Observer(obj);
      }
    }
    function Observer(value) {
      this.value = value;
      if (Array.isArray(value)) {
        // 数组不做处理
      } else {
        this.walk(value);
      }
    }
    Observer.prototype.walk = function (obj) {
      var keys = Object.keys(obj);
      for (var i = 0; i < keys.length; i++) {
        this.defineReactive(obj, keys[i]);
      }
    }
    Observer.prototype.defineReactive = function (obj, key, val) {
      if (arguments.length === 2) {
        val = obj[key]
      }
      observe(val);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
          console.log(['获取', '"', key, '"', '的值'].join(''));
          return val;
        },
        set: function (v) {
          // 判断新值是否和原值一样,一样不做操作
          if (val === v) {
            return;
          }
          console.log(['修改', '"', key, '"', '的值'].join(''));
          val = v;
          observe(v);
        }
      })
    }
    

    到这里我们通过get和set方法实现了将传入的json数据变为可侦测数据,但是这远远不够,虽然数据可观测了,但是没有任何意义,数据变更后,我们只是console一串文字,仍然不知道要做什么,我们还需要记录谁观测了这些数据(谁使用了这些数据),在数据发生变更时通知观测者,做出相应的动作。
    在Vue中主要通过Dep和Watcher两个类实现,Watcher用来收集具体谁观测了(使用了)这些数据,Dep用来存取每条数据对应了多少个Watcher。这样在数据变更后触发Dep通知Watcher,Watcher回调具体使用数据的方法,使其做出相对应的操作。

  • Dep 和 Watcher

    这两个类放在一起说,是因为他们之间有相互依赖的关系,共同实现数据变化的传递。
    实现一个简单的Dep和Watcher

    function Dep () {
      this.sups = []; // 用于存取Watcher实例
    }
    Dep.prototype.addSub = function (sub) {
      this.subs.push(sub); // 向sups中添加Watcher实例
    }
    Dep.prototype.removeSub = function (sub) {
      // 从sups中删除Watcher实例
      const index = this.subs.indexOf(sub);
      if (index > -1) {
        this.subs.splice(index, 1);
      }
    }
    Dep.prototype.depend = function () {
      // window.target 是暂存全局的Watcher实例,在此处将Dep和Watcher关联,在vue中不是放于全局的,为方便理解,放在全局更好理解
      if (window.target) {
        // 调用
        this.addSub(window.target);
      }
    }
    Dep.prototype.notify = function () {
      // 数据发生变化后调用,遍历当前更新数据对应的所有的Watcher对象,调用update方法,通知更新
      const subs = this.subs.slice()
      for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
    
    /**
     * @param vm 观测对象
     * @param expOrFn {String | Function} 观测的具体字段,如: 'child.name'、function () { return this.name + ':' + this.value };
     * @param cb {Function} 数据变化后的回调函数
     */
    function Watcher (vm, expOrFn, cb) {
      this.vm = vm;
      this.cb = cb;
      if (typeof expOrFn === "function") {
        this.getter = expOrFn;
      } else {
        this.getter = parsePath(expOrFn);
      }
      // 这里调用一遍get方法,触发获取数据的getter,向Dep添加一个实例
      this.value = this.get();
    }
    
    Watcher.prototype.get = function () {
      // 这里将this赋值给window.target,上方Dep的depend方法被调用,就会向subs插入实例
      window.target = this;
      var vm = this.vm;
      var value = this.getter.call(vm, vm); // call改变this指针,防止内部this指针错误
      window.target = null;
      return value;
    }
    Watcher.prototype.update = function () {
      var oldValue = this.value
      this.value = this.get()
      // 触发回调
      this.cb.call(this.vm, this.value, oldValue)
    }
    
    /**
     * Parse simple path.
     * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
     * 例如:
     * data = {a:{b:{c:2}}}
     * parsePath('a.b.c')(data)  // 2
     */
    var bailRE = /[^\w.$]/;
    function parsePath (path) {
      if (bailRE.test(path)) {
        return
      }
      const segments = path.split('.')
      return function (obj) {
        for (let i = 0; i < segments.length; i++) {
          if (!obj) return
          obj = obj[segments[i]]
        }
        return obj
      }
    }
    

    这样简单的Dep和Watcher就实现了,下面将Dep 和 Watcher 用到 Observer中, 改写下 Observer.prototype.defineReactive 方法

    Observer.prototype.defineReactive = function (obj, key, val) {
      if (arguments.length === 2) {
        val = obj[key]
      }
      observe(val);
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
          if (window.target) {
            dep.depend();
          }
          return val;
        },
        set: function (v) {
          // 判断新值是否和原值一样,一样不做操作
          if (val === v) {
            return;
          }
          val = v;
          dep.notify();
          observe(v);
        }
      })
    }
    // 调用
    var obj = {
      name: 'obj',
      value: 'obj_value',
      child: {
        name: 'child',
        value: 'child_value'
      }
    }
    new Observer(obj);
    new Watcher(obj, 'name', function (val, oldVal) {
      // ...
      console.log(val, oldVal);
    })
    new Watcher(obj, 'child.name', function (val, oldVal) {
      // ...
      console.log(val, oldVal);
    })
    obj.name = 'test';
    obj.child.name = 'child_test';
    

    上面的赋值操作,会触发dep.notify,dep.notify触发实例Watcher的update方法触发传入的回调函数,在控制台就能看到打印信息,有兴趣的可以试下。
    这里面感觉比较难理解的就是,Watcher和Dep是如何关联的,关联方式我梳理了下,不知到能不能说明白,先这么看着吧:
    1、new Watcher()
    2、触发 Watcher的get方法
    3、window.target = this 向window.target赋值
    4、this.getter.call(vm, vm) 获取对应的数据值,触发数据值的get方法
    5、数据值的get方法执行存在window.target
    6、调用Dep的depend方法,将Watcher实例传递给Dep实例,添加从观测成功
    7、window.target = null 清除赋值,阻止后续别的值调用依赖关系错误

    到这里已经实现一大部分了,但是当你运行这段代码的时候会发现Watcher的回调函次数会更着你数据修改的数据的次数增加,就是回调被重复绑定了,修改一次多绑定一次,这里解决方式就是绑定使用一次后销毁重新绑定,下面改写了下Dep和Watcher的代码对此处做了修正:

    
    function Dep () {
      this.subs = [];
    }
    Dep.prototype.addSub = function (sub) {
      this.subs.push(sub);
    }
    Dep.prototype.removeSub = function (sub) {
      const index = this.subs.indexOf(sub);
      if (index > -1) {
        this.subs.splice(index, 1);
      }
    }
    Dep.prototype.depend = function () {
      if (window.target) {
        window.target.addDep(this);
      }
    }
    Dep.prototype.notify = function () {
      const subs = this.subs.slice()
      for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
    
    function Watcher (vm, expOrFn, cb) {
      this.vm = vm;
      this.cb = cb;
      if (typeof expOrFn === "function") {
        this.getter = expOrFn;
      } else {
        this.getter = parsePath(expOrFn);
      }
      this.value = this.get();
      this.dep = null;
    }
    
    Watcher.prototype.get = function () {
      window.target = this;
      var vm = this.vm;
      var value = this.getter.call(vm, vm);
      window.target = null;
      return value;
    }
    Watcher.prototype.update = function () {
      var oldValue = this.value
      this.value = this.get()
      this.cb.call(this.vm, this.value, oldValue)
    }
    Watcher.prototype.addDep = function (dep) {
      dep.removeSub(this); // 这里移除后再新增
      this.dep = dep;
      dep.addSub(this);
    }
    

    这就是改造后的代码,参照Vue和自己理解写的,没有源码复杂,简化了很多,也修正了数据修改回调次数增多的问题。

上面就是本篇全部了,就是抽空看下,现在只看了源码这些部分,数组的更新没有去做实现,大致看了下主要依靠改写Array原型链方法实现。

posted on 2022-04-02 15:49  一颗、白菜  阅读(294)  评论(0)    收藏  举报

导航