Vue底层学习3——手撸发布订阅模式

全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/14986941.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇完成MVue框架搭建,实现数据劫持并重写settergetter,本篇就来手撸发布订阅模式~

依赖收集与追踪

上一篇的结尾我们通过console.log(`${key}属性更新了:${val}`);预留了视图更新的代码位置,也就是当数据发生变更时,我们需要做数据对应的视图更新,那么到底更新哪些视图,就是依赖收集的意义。首先看一个日常的例子:

new Vue({
    template:
       `<div>
            <span>{{name1}}</span>
            <span>{{name2}}</span>
            <span>{{name1}}</span>
        </div>`,
    data: {
    	name1: 'name1',
        name2: 'name2',
        name3: 'name3'
    },
    created() {
    	this.name1 = 'change name1';
        this.name3 = 'change name3';
    }
});

根据页面绑定的data我们可以整理出以下逻辑:

  • name1被修改,视图更新2处;
  • name2被修改,视图更新1处;
  • name3被修改,视图无需更新;

所以我们需要做的事是扫描视图收集依赖,得知视图中哪里对数据有依赖后,对应数据变更时就可以得到通知,接下来可以对照第一篇《Vue底层学习1——原理解析》中的简化版原理图实现,DepWatcher遵从发布订阅模式,也是本篇的重点,建议后续代码结合着原理图看,思路会更清晰哦~

依赖对象

首先需要实现的是依赖对象Dep,主要用于依赖收集,管理Watcher,它与数据属性一一对应。其中需要提供2个方法:添加观察者、通知观察者。

/*** MVue.js ***/
// 依赖收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依赖(Watcher)
    this.deps = [];
  }

  // 在deps中添加一个观察者对象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的观察者去更新视图
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

观察者对象

接下来实现观察者对象Watcher,主要用于视图更新。其中需要提供更新视图的方法,构造函数中有一个看似奇怪的操作,后续会详细说明。

/*** MVue.js ***/
class Watcher {
  constructor() {
    // 将当前Watcher的实例指定到Dep静态属性target
    Dep.target = this;
  }

  update() {
    // 预留视图更新
    console.log('数据更新了,需要我们更新视图');
  }
}

自建框架整合

根据上面的实现,我们把代码跟之前的做一下初步整合:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 数据缓存
    this.$options = options;
    this.$data = options.data;

    // 数据遍历
    this.observe(this.$data);
  }

  observe(data) {
    // 确定data存在并且为对象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍历data对象
    Object.keys(data).forEach(key => {
        // 重写对象属性的getter和setter,实现数据的响应化
        this.defineReactive(data, key, data[key]);
    })
  }

  defineReactive(obj, key, val) {
    // 解决数据嵌套,递归实现深度遍历
    this.observe(val);

    Object.defineProperty(obj, key, {
      get: function() {
        return val;
      },
      set: function(newVal) {
        // 判断属性值是否发生变化
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 预留视图更新
        console.log(`${key}属性更新了:${val}`);
      }
    })
  }
}

// 依赖收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依赖
    this.deps = [];
  }

  // 在deps中添加一个观察者对象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的观察者去更新视图
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

class Watcher {
  constructor() {
    // 将当前Watcher的实例指定到Dep静态属性target
    Dep.target = this;
  }

  update() {
    // 预留视图更新
    console.log('数据更新了,需要我们更新视图');
  }
}

接下来需要将原本预留在defineReactive中的视图更新替换成发布订阅模式:

  • 第1步:我们需要在defineReactive中定义Dep,这样在将来的某个时刻就能把收集的依赖(Watcher)放进去;
  • 第2步:我们需要替换掉Line42中预留的部分,修改为dep.notify(),实现通知Watcher的功能,最终由Watcher完成视图更新。那么Watcher怎么来?交给第三步;
  • 第3步:在MVue构造函数中先模拟一下Watcher的创建过程,即new Watcher(),接下来就发生了神奇的现象,也就是前面提到的Watcher构造函数中那行奇怪的操作,Watcher当前的实例会被指定到Deptarget静态属性,这样做的目的就是为了将Watcher添加到之前创建的Dep中;
  • 第4步:在属性的getter中添加依赖收集,即dep.addDep(Dep.target),当然需要先判断target是否存在;
  • 第5步:要想依赖可以成功收集,那么我们需要触发getter,也就是读取一下属性,同样在MVue构造函数中模拟;

修改后代码如下:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 数据缓存
    this.$options = options;
    this.$data = options.data;

    // 数据遍历
    this.observe(this.$data);

    // 模拟Watcher的创建过程——第3步
    new Watcher();
    // 模拟属性读取,激活getter,实现依赖收集——第5步
    this.$data.name;
    
    // 模拟Watcher的创建过程——第2步
    new Watcher();
     // 模拟属性读取,激活getter,实现依赖收集——第5步
    this.$data.infoObj.location;
  }

  observe(data) {
    // 确定data存在并且为对象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍历data对象
    Object.keys(data).forEach(key => {
        // 重写对象属性的getter和setter,实现数据的响应化
        this.defineReactive(data, key, data[key]);
    })
  }

  defineReactive(obj, key, val) {
    // 解决数据嵌套,递归实现深度遍历
    this.observe(val);

    // 初始化Dep——第1步
    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get: function() {
      	// 依赖收集,将当前属性对应的Watcher添加至Dep中——第4步
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set: function(newVal) {
        // 判断属性值是否发生变化
        if (newVal === val) {
          return;
        }
        val = newVal;

        // 通知观察者更新视图——第2步
        dep.notify();
      }
    })
  }
}

// 依赖收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依赖
    this.deps = [];
  }

  // 在deps中添加一个观察者对象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的观察者去更新视图
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

class Watcher {
  constructor() {
    // 将当前Watcher的实例指定到Dep静态属性target
    Dep.target = this;
  }

  update() {
    // 预留视图更新
    console.log('数据更新了,需要我们更新视图');
  }
}

自建框架测试demo1

运行上一篇《Vue底层学习2——手撸数据响应化》中的demo1案例,执行结果如下,说明我们的发布订阅模式成功替换:

总结

本篇实现发布订阅模式的整体过程可以归纳如下:新增一个Dep类的实例来做依赖收集。读取数据时,会触发getter把当前的Watcher(存放在Dep.target中)收集到Dep实例中。写入数据时,会触发setter通知Dep类调用notify方法,以此触发所有Watcherupdate方法来更新对应的视图。

最后提个小tips——每个Dep针对单个属性,有多少个数据属性就有多少Dep,但是一个Dep中可能有多个Watcher,因为一个属性可能在视图中出现多次。

参考资料

1、Vue源码:https://github.com/vuejs/vue

posted @ 2021-07-08 16:59  Dreamsqin  阅读(415)  评论(0编辑  收藏  举报