• 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

  • 相关阅读:
    福州3中集训day5
    福州三中集训day4
    福州三中集训day3
    福州三中基训day2
    福州三中集训day1
    Python3 字符串
    Python3 数字
    Python3 运算符
    Python3 基础数据类型
    Codeforces Round 253 (Div. 2)
  • 原文地址:https://www.cnblogs.com/dreamsqin/p/14986941.html
Copyright © 2020-2023  润新知