【Vue2源码】响应式原理

文章目录

  • 1、基本原理
  • [2、数据劫持 Object.defineProperty](#2、数据劫持 Object.defineProperty)
    • [2.1 对象响应式](#2.1 对象响应式)
    • [2.2 数组响应式处理](#2.2 数组响应式处理)
  • [3、观察者 Watcher](#3、观察者 Watcher)
  • [4、依赖 Dep](#4、依赖 Dep)
  • 5、依赖收集、派发更新

1、基本原理

vue2 中响应式原理主要就是通过数据劫持,依赖收集,派发更新的方式来实现的

  1. 数据劫持: Vue 2使用Object.defineProperty函数对组件的data对象的属性进行劫持(或称为拦截)。当读取 data 中的属性时触发 get,当修改 data 中的属性时触发 set

  2. 依赖收集:当模板或者计算属性等引用了data 中的响应式数据时,Vue将这些消费者(观察者)收集起来,建立起数据与消费者之间的关联

  3. 派发更新:当响应式数据变化时,通过 dep 来执行 watcher 的 notify 方法进行通知更新

2、数据劫持 Object.defineProperty

Vue 2使用 Object.defineProperty 函数对组件的 data 对象的属性进行劫持

局限性:Object.defineProperty 只能劫持对象的属性,因此Vue 2无法自动侦测到对象属性的添加或删除,以及直接通过索引修改数组项的情况。Vue解决这个问题的方式是提供了全局方法如 Vue.set 和 Vue.delete,以及修改数组时应该使用的一系列方法(如push、splice等)

2.1 对象响应式

源码位置:src/core/observer/index.ts

ts 复制代码
import { def } from "core/util/lang";
import { hasChanged, isArray, isPlainObject } from "src/shared/util";
import { arrayMethods } from "./array";

class Observer {
  constructor(value: any) {
    def(value, "__ob__", this);

    if (isArray(value)) {
      // 数组,需要特殊处理,进行劫持数组方法
      (value as any).__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      // 对象
      const keys = Object.keys(value);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        defineReactive(value, key);
      }
    }
  }

  /**
   * 将数组的每一项进行响应式处理
   * @param value
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i]);
    }
  }
}

// 数据响应式的入口函数
export function observe(value: any) {
  if (isPlainObject(value) || isArray(value)) {
    // 当值为对象或数组时,进行响应式处理
    return new Observer(value);
  }
}

// 核心:定义对象的响应式属性
function defineReactive(obj: any, key: string) {
  let value = obj[key];

  // 深度代理
  observe(value);

  Object.defineProperty(obj, key, {
    get() {
      console.log("获取", key);
	  
	  // 这里进行依赖收集 dep.depend()
		
      return value;
    },
    set(newVal) {
      console.log("设置");
      if (!hasChanged(value, newVal)) {
        return;
      }
      // 新值进行响应式
      observe(newVal);

      value = newVal;
      
      // 这里进行通知更新 dep.notify()
      
    },
  });
}

2.2 数组响应式处理

问题:因为 JavaScript 的限制使得 Vue 不能直接检测到数组索引和长度的变化

解决:通过劫持数组的方法,包括:

push

pop

shift

unshift

splice

sort

reverse

当你使用这些方法时,Vue 内部的实现会首先调用原生的数组方法来更新数组,然后执行额外的逻辑来通知变化,从而触发视图的更新。

源码位置:src/core/observer/array.ts

ts 复制代码
import { def } from "core/util/lang";

const arrayProto = Array.prototype;

// 创建一个新对象,该新对象的原型指向 Array 的原型
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach((method) => {
  // 缓存原始方法
  const original = arrayProto[method];

  // 定义新方法
  def(arrayMethods, method, function (...args) {
    console.log("劫持数组", args);
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }

    if (inserted) {
      ob.observeArray(inserted);
    }

    // 通知更新

    return result;
  });
});

3、观察者 Watcher

Watcher(观察者)是一个关键的部分,它用于在数据变化时执行更新的操作。其主要作用是在依赖性收集阶段将自己添加到每个相关数据的Dependent(Dep)对象中,并在数据变化时接收到通知,从而触发回调函数。

主要职责:

(1)依赖收集: Watcher在初始化时会调用自己的get方法去读取数据,这会触发数据的getter函数从而进行依赖性收集。在getter函数中,当前Watcher实例会被添加到数据对应的Dep实例中。

(2)执行更新: 当数据发生变化,Dep实例调用notify方法时,Watcher实例会接收到通知,然后调用自己的update方法以触发回调

源码位置:src/core/observer/watcher.ts

ts 复制代码
import { isFunction, noop } from "src/shared/util";
import { Component } from "src/types/component";
import Dep, { popTarget, pushTarget } from "./dep";

let uid = 0;

export default class Watcher {
  vm: Component;
  cb: Function;
  getter: Function;
  id: number;

  constructor(vm: Component, expOrFn: Function, cb: Function) {
    this.cb = cb;
    this.vm = vm;
    this.id = ++uid;
    if (isFunction(expOrFn)) {
      this.getter = expOrFn;
    } else {
      this.getter = noop;
    }

    this.get();
  }

  // 初次渲染
  get() {
    pushTarget(this); // 给 dep 添加 Watcher

    this.getter(); // 执行 render 进行渲染页面,(src/core/instance/lifecycle.ts)
    
    popTarget(); // 给 dep 取消 Watcher
  }

  /**
   * 添加依赖项
   * @param dep 
   */
  addDep(dep: Dep) {
    dep.addSub(this);
  }

  /**
   * 更新方法
   */
  update() {
    console.log("更新");
    this.get();
  }
}

说明:

调用:在 src/core/instance/lifecycle.ts 中执行 mountComponent 时

4、依赖 Dep

Dep(Dependency)是一个核心的类,它负责建立数据和观察者(Watcher)之间的关联(依赖),并提供接口触发它们的更新

主要职责:

(1)存储观察者: Dep实例内部维护了一个观察者(Watcher)对象的数组。在依赖收集阶段,观察者对象会被添加到Dep实例的数组中,而在派发更新阶段,Dep类则会遍历这个数组,通知所有的观察者。

(2)依赖收集: Dep类提供了addSub方法,用于在依赖收集阶段添加新的观察者。当数据的getter函数被调用时,Dep会把当前正在评估的观察者添加到自身的观察者列表中。

(3)派发更新: Dep类提供了notify方法,用于在数据发生变更时通知所有的观察者。当数据的setter函数被调用时,Dep会遍历自己的观察者列表,并调用它们的update方法

源码位置:src/core/observer/dep.ts

ts 复制代码
interface DepTarget {
  id: number;
  addDep(dep: Dep): void;
  update(): void;
}

export default class Dep {
  static target?: DepTarget | null;
  subs: Array<DepTarget | null>; // 观察者(Watcher)对象的数组

  constructor() {
    this.subs = [];
  }

  addSub(sub: DepTarget) {
    // sub 是 Watcher 对象
    this.subs.push(sub);
  }

  // 收集 Watcher
  depend() {
    if (Dep.target) {
      // 这里是 Watcher 中的 addDep 方法
      Dep.target.addDep(this);
    }
  }

  // 通知更新
  notify() {
    const subs = this.subs.filter((s) => s) as DepTarget[];
    for (let i = 0; i < subs.length; i++) {
      const sub = subs[i];
      // 这里就是 Watcher 中的 update 方法
      sub.update();
    }
  }
}

Dep.target = null;
export function pushTarget(target: DepTarget) {
  Dep.target = target;
}

export function popTarget() {
  Dep.target = null;
}

5、依赖收集、派发更新

修改 src/core/observer/index.ts ,添加依赖收集和通知更新的逻辑处理:

ts 复制代码
import { def } from "core/util/lang";
import { hasChanged, isArray, isPlainObject } from "src/shared/util";
import { arrayMethods } from "./array";
import Dep, { pushTarget } from "./dep";

class Observer {
  dep: Dep;

  constructor(value: any) {
    this.dep = new Dep();

    def(value, "__ob__", this);

    if (isArray(value)) {
      // 数组,需要特殊处理,进行劫持数组方法
      (value as any).__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      // 对象
      const keys = Object.keys(value);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        defineReactive(value, key);
      }
    }
  }

  /**
   * 将数组的每一项进行响应式处理
   * @param value
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i]);
    }
  }
}

// 数据响应式的入口函数
export function observe(value: any) {
  if (isPlainObject(value) || isArray(value)) {
    // 当值为对象或数组时,进行响应式处理
    return new Observer(value);
  }
}

// 核心:定义对象的响应式属性
function defineReactive(obj: any, key: string) {
  let value = obj[key];

  let dep = new Dep();

  // 深度代理
  let childOb = observe(value);

  Object.defineProperty(obj, key, {
    get() {
      console.log("获取", key);

      // 这里进行依赖收集 dep.depend()
      if (Dep.target) {
        dep.depend();
        if (childOb) {
		  // value 是对象或者数组,childOb 才会有值
		  // 但是这里是针对数组的依赖收集
          childOb.dep.depend();
        }
      }

      return value;
    },
    set(newVal) {
      console.log("设置");
      if (!hasChanged(value, newVal)) {
        return;
      }
      
      // 新值进行响应式
      observe(newVal);
      value = newVal;

      // 这里进行通知更新 dep.notify()
      dep.notify();
    },
  });
}

数组:src/core/observer/array.ts

ts 复制代码
import { def } from "core/util/lang";

const arrayProto = Array.prototype;

// 创建一个新对象,该新对象的原型指向 Array 的原型
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach((method) => {
  // 缓存原始方法
  const original = arrayProto[method];

  // 定义新方法
  def(arrayMethods, method, function (...args) {
    console.log("劫持数组", args);
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }

    if (inserted) {
      ob.observeArray(inserted);
    }

    // 通知更新
    ob.dep.notify();

    return result;
  });
});
相关推荐
清灵xmf几秒前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据7 分钟前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_3901617716 分钟前
防抖函数--应用场景及示例
前端·javascript
3345543244 分钟前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript