【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher

文章目录

  • 1. 定义 defineReactive 函数
    • 1.1 Why (临时变量)
    • 1.2 How (闭包)
  • 2. 对象的响应式处理——递归侦测对象全部属性 object
    • 2.1 Why (嵌套)
    • 2.2 How(递归)
      • observe.js
      • Observer.js
      • def.js
      • defineReactive.js
    • 2.3 测试
      • index.js
  • 3. 数组的响应式处理
    • 3.1 前置知识
      • `Object.setPrototypeOf()` 修改对象原型
      • `Object.create()` 创建一个新对象
    • 3.2 实现
      • array.js
      • Observer.js
  • 4. 收集依赖
    • 4.1 Why 为什么要收集依赖
    • 4.2 How 怎么收集依赖
      • 依赖收集到哪里? Dep
      • 依赖是什么?Watcher
    • 4.3 实现
      • Dep.js
      • Watcher.js
      • defineReactive.js
      • array.js
  • 5. 测试侦测情况

我们在使用Vue时,只需要修改数据,视图就会自动更新,这就是数据响应
今天来学习Vue实现数据响应式的原理~

源码链接 https://gitee.com/ykang2020/vue_learn

前置知识 Object.defineProperty() 可以参考之前的博文
【JS】JavaScript对象属性-属性类型-数据属性-访问器属性-Object.defineProperty()方法-get方法-set方法

gettersetter方法可以对数据进行监听,访问和设置都会被监听捕获
读取数据的时候会触发getter,而修改数据的时候会触发setter

1. 定义 defineReactive 函数

1.1 Why (临时变量)

我们要进行数据劫持,先想到的就是Object.defineProperty()中给属性添加gettersetter方法,但是这么做有点问题~

defineProperty() 方法需要临时的全局变量周转gettersetter

我们来看下面这个例子

let obj = {
     };
let temp;

Object.defineProperty(obj, "a", {
     
  get() {
     
    console.log("getter试图访问obj的a属性");
    return temp;
  },
  set(newValue) {
     
    console.log("setter试图改变obj的a属性", newValue);
    temp = newValue;
  },
});

console.log(obj.a); 
obj.a = 5
console.log(obj.a);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第1张图片

1.2 How (闭包)

所以我们就自己定义一个函数,对defineProperty进行封装,来实现数据劫持

使用defineReactive 函数不需要设置临时变量了,而是用闭包

function defineReactive(data, key, value) {
     
  Object.defineProperty(data, key, {
     
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
     
      console.log(`getter试图访问obj的${
       key}属性`);
      return value;
    },
    // setter
    set(newValue) {
     
      console.log(`setter试图改变obj的${
       key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
    },
  });
}

let obj = {
     };
// 初始化
defineReactive(obj, "a", 10);
console.log(obj.a);

obj.a = 5;
console.log(obj.a);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第2张图片

2. 对象的响应式处理——递归侦测对象全部属性 object

2.1 Why (嵌套)

上面定义的defineProperty()函数,不能监听到对象嵌套的形式

也就是对象嵌套对象

function defineReactive(data, key, value) {
     
  if (arguments.length === 2) {
     
    value = data[key];
  }
  Object.defineProperty(data, key, {
     
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
     
      console.log(`getter试图访问obj的${
       key}属性`);
      return value;
    },
    // setter
    set(newValue) {
     
      console.log(`setter试图改变obj的${
       key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
    },
  });
}

let obj = {
     
  b: {
     
    c: {
     
      d: 4,
    },
  },
};
// 初始化
defineReactive(obj, "b");
console.log(obj.b.c.d);

这里显示没有监听到内部(obj.b.c.d)
在这里插入图片描述

2.2 How(递归)

所以我们要创建一个Observer类 ——> 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

遍历对象

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第3张图片

observe.js

监听 value
尝试创建Observer实例,如果value已经是响应式数据,就不需要再创建Observer实例,直接返回已经创建的Observer实例即可,避免重复侦测value变化的问题

import Observer from "./Observer";
/**
 * 监听 value
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
     
  // 如果value不是对象,就什么都不做
  if (typeof value != "object") return;
  let ob;
  if (typeof value.__ob__ !== "undefined") {
     
    ob = value.__ob__;
  } else {
     
    ob = new Observer(value);
  }
  return ob;
}

Observer.js

将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

Observer 类会附加到每一个被侦测的object上
一旦被附加,Observer会将object所有属性转换成getter/setter的形式

__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例

import def from "./def";
import defineReactive from "./defineReactive";
/**
 * 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
 */
export default class Observer {
     
  // 构造器
  constructor(value) {
     
    // 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
    def(value, "__ob__", this, false);
    
    console.log("Observer构造器", value);
    
    // 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
    this.walk(value);
  }
  // 遍历value的每一个key
  walk(value) {
     
    for (let key in value) {
     
      defineReactive(value, key);
    }
  }
}

def.js

工具方法 定义一个对象属性

/**
 * 定义一个对象属性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
     
  Object.defineProperty(obj, key, {
     
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}

defineReactive.js

给对象data的属性key定义监听

import observe from "./observe";

/**
 * 给对象data的属性key定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive(data, key, value) {
     
  console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
     
    value = data[key];
  }
  
  // 子元素要进行observe,形成递归
  let childOb = observe(value)
  
  Object.defineProperty(data, key, {
     
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
     
      console.log(`getter试图访问${
       key}属性`);
      return value;
    },
    // setter
    set(newValue) {
     
      console.log(`setter试图改变${
       key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
      
      // 当设置了新值,新值也要被observe
      childOb = observe(newValue)
    },
  });
}

文件之间的依赖结构
【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第4张图片
三个函数互相调用形成了递归
【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第5张图片

处理完对象,如果嵌套的是数组怎么办,下面我们来看看数组是怎么处理的

2.3 测试

index.js

import observe from "./observe";

let obj = {
     
  a: 1,
  b: {
     
    c: {
     
      d: 4,
    },
  },
};

observe(obj);
obj.a = 5;
obj.b.c.d = 10;

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第6张图片

3. 数组的响应式处理

正因为我们可以通过Array原型上的方法来改变数组的内容,所以ojbect那种通过getter/setter的实现方式就行不通了。

ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法。

Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理

这些方法分别是:pushpopshiftunshiftsplicesortreverse
【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第7张图片

这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码

思路就是 以Array.prototype为原型,创建一个新对象arrayMthods
然后在新对象arrayMthods上定义(改写)这些方法
定义 数组 的原型指向 arrayMthods

这就相当于用一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。在拦截器中使用原生Array的原型方法去操作数组。

3.1 前置知识

Object.setPrototypeOf() 修改对象原型

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null

Object.setPrototypeOf(obj, prototype)

obj 要设置其原型的对象
prototype 该对象的新原型(一个对象 或 null)

Object.create() 创建一个新对象

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

Object.create(proto,[propertiesObject])

proto 新创建对象的原型对象。
propertiesObject 可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

返回 一个新对象,带着指定的原型对象和属性。

3.2 实现

array.js

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
     
  // 备份原来的方法
  const original = arrayPrototype[methodName];

  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
     
      console.log("array数据已经被劫持");

      // 恢复原来的功能(数组方法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取Observer的实例
      const ob = this.__ob__;

      // 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
      let inserted = [];
      switch (methodName) {
     
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项inserted,有的话就劫持
      if (inserted) {
     
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});

Observer.js

__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了
而且可以通过value.__ob__来访问Observer的实例

import def from "./def";
import defineReactive from "./defineReactive";
import observe from "./observe";
import {
     arrayMethods} from './array'
/**
 * 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
 * Observer 类会附加到每一个被侦测的object上
 * 一旦被附加,Observer会将object所有属性转换成getter/setter的形式
 * 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
 */
export default class Observer {
     
  // 构造器
  constructor(value) {
     
  
    // 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
    def(value, "__ob__", this, false);
    // __ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例
    
    // console.log("Observer构造器", value);
    // 判断是数组还是对象
    if (Array.isArray(value)) {
     
      // 是数组,就将这个数组的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // 早期实现是这样
      // value.__proto__ = arrayMethods;
      
      // observe数组
      this.observeArray(value);
    } else {
     
      this.walk(value);
    }
  }
  // 对象的遍历方式 遍历value的每一个key
  walk(value) {
     
    for (let key in value) {
     
      defineReactive(value, key);
    }
  }
  // 数组的遍历方式
  observeArray(arr) {
     
    for (let i = 0, l = arr.length; i < l; i++) {
     
      // 逐项进行observe
      observe(arr[i]);
    }
  }
}

4. 收集依赖

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第8张图片

4.1 Why 为什么要收集依赖

之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。

先收集依赖,把用到数据的地方收集起来,等属性改变,在之前收集好的依赖中循环触发一遍就好了~

总结起来就是
对象是
在getter中收集依赖,在setter中触发依赖
而数组是
在getter中收集依赖,在拦截器中触发依赖

4.2 How 怎么收集依赖

依赖收集到哪里? Dep

目标明确,我们要在 getter 中收集依赖,那 依赖收集到哪里?

将依赖收集封装成一个类 Dep 这个类帮我们管理依赖
可以收集依赖、删除依赖、向依赖发送通知

依赖是什么?Watcher

需要用到数据的地方,成为依赖,就是Watcher

只有Watcher 触发的 getter 才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher 收集到Dep

Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher 都通知一遍。

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第9张图片

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第10张图片

4.3 实现

代码实现的巧妙之处:Watcher 把自己设置到全局的一个指定位置,然后读取数据。
因为读取了数据,所以会触发这个数据的getter 。在getter 中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep 中。

需要用一个数组来存watcher
watcher实例需要订阅 依赖(数据),也就是获取依赖或者收集依赖
watcher的依赖发生时,触发watcher的回调函数,也就是派发更新

Dep.js

Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher 都通知一遍。

Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等

let uid = 0;
/**
 * Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等
 */
export default class Dep {
     
  constructor() {
     
    console.log("Dep");
    this.id = uid++;
    // 用数组存储自己的订阅者,放的是Watcher的实例
    this.subs = [];
  }

  // 添加订阅
  addSub(sub) {
     
    this.subs.push(sub);
  }

  // 删除订阅
  removeSub(sub) {
     
    remove(this.subs, sub);
  }

  // 添加依赖
  depend() {
     
    // Dep.target 是一个我们指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行
    if (Dep.target) {
     
      this.addSub(Dep.target);
    }
  }

  // 通知更新
  notify() {
     
    console.log("notify");
    // 浅拷贝一份
    const subs = this.subs.slice();
    // 遍历
    for (let i = 0, l = subs.length; i < l; i++) {
     
      subs[i].update();
    }
  }
}

/**
 * 从arr数组中删除元素item
 * @param {*} arr 
 * @param {*} item
 * @returns 
 */
function remove(arr, item) {
     
  if (arr.length) {
     
    const index = arr.indexOf(item);
    if (index > -1) {
     
      return arr.splice(index, 1);
    }
  }
}

Watcher.js

import Dep from "./Dep";

let uid = 0;
/**
 * Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
 */
export default class Watcher {
     
  constructor(target, expression, callback) {
     
    console.log("Watcher");
    this.id = uid++;
    this.target = target;
    // 按点拆分  执行this.getter()就可以读取data.a.b.c的内容
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }

  get() {
     
    // 进入依赖收集阶段。
    // 让全局的Dep.target设置为Watcher本身
    Dep.target = this;
    const obj = this.target;
    var value;
    // 只要能找就一直找
    try {
     
      value = this.getter(obj);
    } finally {
     
      Dep.target = null;
    }
    return value;
  }

  update() {
     
    this.run();
  } 

  run() {
     
    this.getAndInvoke(this.callback);
  }
  getAndInvoke(callback) {
     
    const value = this.get();
    if (value !== this.value || typeof value === "object") {
     
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue);
    }
  }
}

/**
 * 将str用.分割成数组segments,然后循环数组,一层一层去读取数据,最后拿到的obj就是str中想要读的数据
 * @param {*} str
 * @returns
 */
function parsePath(str) {
     
  let segments = str.split(".");
  return function (obj) {
     
    for (let key of segments) {
     
      if (!obj) return;
      obj = obj[key];
    }
    return obj;
  };
}

defineReactive.js

对象在getter中收集依赖,在setter中触发依赖
数组在getter中收集依赖,在拦截器中触发依赖

import Dep from "./Dep";
import observe from "./observe";

/**
 * 给对象data的属性key定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive(data, key, value) {
     
  
  // 每个数据都要维护一个属于自己的数组,用来存放依赖自己的watcher
  const dep = new Dep();

  // console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
     
    value = data[key];
  }

  // 子元素要进行observe,形成递归
  let childOb = observe(value);

  Object.defineProperty(data, key, {
     
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter  收集依赖
    get() {
     
      console.log(`getter试图访问${
       key}属性`);

      // 收集依赖
      if (Dep.target) {
     
        dep.depend();
        // 判断有没有子元素
        if (childOb) {
     
          // 数组收集依赖
          childOb.dep.depend();
        }
      }

      return value;
    },
    // setter 触发依赖
    set(newValue) {
     
      console.log(`setter试图改变${
       key}属性`, newValue);

      if (value === newValue) return;
      value = newValue;

      // 当设置了新值,新值也要被observe
      childOb = observe(newValue);

      // 触发依赖
      // 发布订阅模式,通知dep
      dep.notify();
    },
  });
}

array.js

数组在getter中收集依赖,在拦截器中触发依赖

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
     
  // 备份原来的方法
  const original = arrayPrototype[methodName];

  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
     
      console.log("array数据已经被劫持");

      // 恢复原来的功能(数组方法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取Observer的实例
      const ob = this.__ob__;

      // 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
      let inserted = [];
      switch (methodName) {
     
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项inserted,有的话就劫持
      if (inserted) {
     
        ob.observeArray(inserted);
      }

      // 发布订阅模式,通知dep
      // 向依赖发送消息
      ob.dep.notify();

      return result;
    },
    false
  );
});

5. 测试侦测情况

let obj = {
     
  a: 1,
  b: {
     
    c: {
     
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
console.log(obj);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第11张图片

observe(obj);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第12张图片

observe(obj);
console.log(obj);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第13张图片

observe(obj);
obj.a = 5;

在这里插入图片描述

observe(obj);
obj.a++

在这里插入图片描述

observe(obj);
console.log(obj.b.c)

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第14张图片

observe(obj);
obj.b.c.d = 10;

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第15张图片

observe(obj);
obj.e.push(66, 77, 88);
console.log(obj.e);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第16张图片

observe(obj);
obj.e.splice(2, 1, [13, 14]);
console.log(obj.e);

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第17张图片

observe(obj);
new Watcher(obj, "b.c.d", (val) => {
     
  console.log("watcher监听", val);
});

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher_第18张图片
源码链接 https://gitee.com/ykang2020/vue_learn

你可能感兴趣的:(前端框架Vue,javascript,vue)