美文网首页
Vue—关于响应式(一、依赖收集原理分析)

Vue—关于响应式(一、依赖收集原理分析)

作者: Mr丶Sunny | 来源:发表于2022-10-25 08:39 被阅读0次

    一、什么是响应式?

    在了解什么是响应式之前我们现来看一段代码演示

    let x;
    let y;
    let f = n => n * 100
    
    x = 1;
    y = f(x);
    console.log(y); // 100
    
    x = 2;
    y = f(x);
    console.log(y); // 200
    
    x = 3;
    y = f(x);
    console.log(y); // 300
    

    代码示例中,变量y依赖变量x进行求值,但是我们会发现每一次变量x重新赋值时都要手动对y进行求值,存在大量的重复模板,因此,指导我们进行程序设计的DRY原则就发挥价值了

    DRY 全称:Don't Repeat Yourself (摘自wikipedia),是指编程过程中不写重复代码,将能够公共的部分抽象出来,封装成工具类或者用“abstraction”类来抽象公有的东西,降低代码的耦合性,这样不仅提高代码的灵活性、健壮性以及可读性,也方便后期的维护或者修改。

    那么我们需要有一个方法,实现自动监听x的变化并且自动对y进行求值,以减少重复代码

    假设我们有一个onXChange函数,使得每次x重新赋值时都会触发onXChange中的回调函数,你的代码看起来应该像下面这样:

    let x;
    let y;
    let onXChange = function(cb) {
      // ...
    }
    
    onXChange(() => {
      y = f(x);
      console.log(y);
    })
    
    x = 1; // 100
    x = 2; // 200
    x = 3; // 300
    

    如果将y换成dom模板,根据x的变化自动渲染不同的模板也是同理。

    现在我们可以来解释什么是响应式了(其实都不用我解释,看到这你自己也有答案了),响应式只是一种编程方式,它的目的是为了简化编程,特点是自动对变化进行响应。

    以下是摘自wikipedia的解释:

    响应式(Reactive Programming)

    是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便的表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

    二、Vue中的响应式分析

    我们来看看Vue中是怎么实现响应式的

    首先第一步需要监听数据变化,知道变量什么时候进行了修改,JS提供的能够监听数据变化的API有Object.defineProperty以及ES6新增的Proxy,本节我们只探讨Object.defineProperty

    关于Object.defineProperty的使用如果你还不了解的话请阅读如下文档:

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

    Vue2.0中使用了Object.defineProperty来遍历data中的数据,在getter中将使用到这个数据的上下文进行收集,这个过程称之为【依赖收集】,在setter中修改这个数据时则会触发【通知依赖更新】的操作,如下图所示:

    1628664097.png

    什么是依赖收集?

    所谓依赖收集,就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作。

    为什么这里需要依赖收集?

    考虑到一个变量的修改可能会引起多处变化,因此需要将依赖这个变量的所有地方都收集起来,等到变量更新时再进行批量操作。

    Vue关于响应式原理的介绍官网已经说的很清楚,这里贴出链接不再赘述:

    https://cn.vuejs.org/v2/guide/reactivity.html#ad

    三、实现一个简单的数据响应

    以上面的代码为例,我们需要通过onXChange函数来监听x的修改,由于上面代码中x的值是基础类型,我们需要将x更改为引用类型才可以使用Object.defineProperty,因此我们可以创建一个函数来做这件事情,假设这个函数为ref

    ref函数接收一个初始值,函数内闭包一个value变量赋值为传入的初始值,通过Object.defineProperty返回一个带有value属性的对象,在get中返回value,并在set中将value赋值,这样在我们修改了x.value之后会自动触发set来更新闭包的value变量

    let ref = initValue => {
      let value = initValue;
    
      return Object.defineProperty({}, 'value', {
        get() {
          return value;
        },
        set(newValue) {
          value = newValue;
        }
      })
    }
    

    对应的代码也需要修改一下:

    • 修改x为ref函数调用的返回值
    • 将对应的x赋值更改为x.value赋值
    let x;
    let y;
    let f = n => n * 100
    
    let onXChange = function(cb) {
      // ...
    }
    
    let ref = initValue => {
      let value = initValue;
    
      return Object.defineProperty({}, 'value', {
        get() {
          return value;
        },
        set(newValue) {
          value = newValue;
        }
      })
    }
    
    // 创建x对象,初始value传入1
    x = ref(1);
    
    // 监听x
    onXChange(() => {
      y = f(x.value);
      console.log(y);
    })
    
    x.value = 2;
    x.value = 3;
    

    到这一步已经可以自动获取到x.value改变后的值了,我们可以在set方法中打印newValue

    set(newValue) {
      console.log('x: ', newValue)
      value = newValue;
    }
    
    image.png

    既然已经监听到x.value的修改了,接下来我们只需要拿到onXChange中的回调函数,在set方法中调用它就可以同步修改y的值

    怎么拿这个回调函数?

    我们可以创建一个变量将这个回调函数存起来,假设变量名为active,然后在set方法中调用active函数即可

    // 省略部分代码...
    
    // 创建active变量
    let active;
    let onXChange = function(cb) {
      // 将回调赋值给active
      active = cb;
    }
    
    let ref = initValue => {
      let value = initValue;
    
      return Object.defineProperty({}, 'value', {
        get() {
          return value;
        },
        set(newValue) {
          value = newValue;
          active(); // 调用active函数
        }
      })
    }
    
    image.png

    可以看到y的打印结果出来了,但少了x.value初始为1时的结果,我们还需要在初始的时候调用一次active

    // 执行onXChange时就调用一次回调函数
    let onXChange = function(cb) {
      active = cb;
      active();
      active = null; // 销毁active,避免修改x.value时重复添加依赖
    }
    
    image.png

    四、结合Vue源码来看响应式

    到这里一个简单的响应式其实已经完成了,但还不够,如果我们不仅有onXChange,还有onYChange、onZChange呢,这些函数都依赖了x变量怎么办?

    Vue的解决办法是通过一个Dep对象将这些依赖都收集起来,在变量发生改变时进行批量通知更新。

    那么Dep对象至少应该具有一个存储依赖的列表、一个添加依赖的方法和一个通知依赖更新的方法

    我们先来简单实现一下,再结合Vue源码验证

    Dep代码如下:

    class Dep {
      deps = new Set();
    
      // 收集依赖
      depend() {
        if (active) {
          this.deps.add(active);
        }
      }
    
      // 批量更新
      // 将所有的依赖都执行一遍
      notify() {
        this.deps.forEach(dep => dep());
      }
    }
    

    然后我们需要在ref函数中获取dep实例,在get时调用dep.depend()添加依赖,在set时调用dep.notify来通知依赖更新

    let ref = (initValue) => {
      let value = initValue;
      // 获取dep实例
      let dep = new Dep();
    
      return Object.defineProperty({}, "value", {
        get() {
          // 添加依赖
          dep.depend();
          return value;
        },
        set(newValue) {
          value = newValue;
          // 通知依赖更新
          dep.notify();
        },
      });
    };
    

    现在来验证一下,我们添加任意个依赖x变量的函数:

    let onYChange = function(cb) {
      active = cb;
      active();
    }
    
    onYChange(() => {
      console.log('onYChange', f(x.value));
    })
    
    // ......
    
    image.png

    可以看到所有依赖x变量的地方都打印了结果,一切都没有问题。

    那么Vue的源码是不是这么实现的呢?

    以vue2.6.11版本为例:

    defineReactive$$1函数的作用就是通过Object.defineProperty来将普通的数据处理成响应式数据,完整代码如下:

    function defineReactive$$1 (
        obj,
        key,
        val,
        customSetter,
        shallow
      ) {
        var dep = new Dep();
    
        var property = Object.getOwnPropertyDescriptor(obj, key);
        if (property && property.configurable === false) {
          return
        }
    
        // cater for pre-defined getter/setters
        var getter = property && property.get;
        var setter = property && property.set;
        if ((!getter || setter) && arguments.length === 2) {
          val = obj[key];
        }
    
        var childOb = !shallow && observe(val);
        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();
          }
        });
      }
    

    去除源码中影响阅读的代码:

    function defineReactive$$1 (
        obj,
        key,
        val,
      ) {
        var dep = new Dep();
        
        // 省略部分代码...
        
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function reactiveGetter () {
            if (Dep.target) {
              dep.depend();
            }
            return value
          },
          set: function reactiveSetter (newVal) {
            val = newVal;
            dep.notify();
          }
        });
      }
    

    与我们自己实现的ref函数对比一下,你Get到了吗?

    再看源码中的Dep是怎么实现的:

      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();
        }
      };
    

    先不用管别的代码,至少我们在Dep源码中找到了一个存储依赖的列表subs、添加依赖的方法depend、通知依赖更新的方法notify,看到这里我想你对Vue的响应式原理已经有自己的理解了。

    那么我们再来总结一下Vue的响应式原理:

    1. 将data中的数据通过Object.defineProperty处理成响应式数据
    2. 数据被【读】的时候会触发getter,将使用到这个数据的上下文进行依赖收集,存放到Dep类中
    3. 数据被【写】的时候会触发setter,调用Dep.notify方法通知依赖更新

    不对的地方请指正,但不要批评我,不听哈哈哈!以上演示代码已上传github:

    https://github.com/Mr-Jemp/VueStudy/blob/main/vue-reactive-demo/src/assets/js/demo2.js

    后面要学习的内容在这里:

    Vue—关于响应式(二、异步更新队列原理分析)

    Vue—关于响应式(三、Diff Patch原理分析)

    Vue—关于响应式(四、深入学习Vue响应式源码)

    本文由博客一文多发平台 OpenWrite 发布!

    相关文章

      网友评论

          本文标题:Vue—关于响应式(一、依赖收集原理分析)

          本文链接:https://www.haomeiwen.com/subject/hfunzrtx.html