关于前端:Vue关于响应式一依赖收集原理分析

35次阅读

共计 5931 个字符,预计需要花费 15 分钟才能阅读完成。

一、什么是响应式?

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

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…

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

什么是依赖收集?

所谓依赖收集,就是把一个数据用到的中央收集起来,在这个数据产生扭转的时候,对立去告诉各个中央做对应的操作。

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

思考到一个变量的批改可能会引起多处变动,因而须要将依赖这个变量的所有中央都收集起来,等到变量更新时再进行批量操作。

Vue 对于响应式原理的介绍官网曾经说的很分明,这里贴出链接不再赘述:

https://cn.vuejs.org/v2/guide…

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

以下面的代码为例,咱们须要通过 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;
}

既然曾经监听到 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 函数}
  })
}

能够看到 y 的打印后果进去了,但少了 x.value 初始为 1 时的后果,咱们还须要在初始的时候调用一次 active

// 执行 onXChange 时就调用一次回调函数
let onXChange = function(cb) {
  active = cb;
  active();
  active = null; // 销毁 active,防止批改 x.value 时反复增加依赖
}

四、联合 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));
})

// ......

能够看到所有依赖 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/Vu…

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

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

Vue—对于响应式(三、Diff Patch 原理剖析)

Vue—对于响应式(四、深刻学习 Vue 响应式源码)

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

正文完
 0