【Vue2源码】响应式原理

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

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

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

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

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 ,添加依赖收集和通知更新的逻辑处理:

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

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

相关推荐

  1. Vue2响应原理

    2024-03-18 11:34:02       23 阅读
  2. vue响应原理

    2024-03-18 11:34:02       36 阅读
  3. vue2vue 3 的响应原理

    2024-03-18 11:34:02       6 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-18 11:34:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-18 11:34:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-18 11:34:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-18 11:34:02       20 阅读

热门阅读

  1. HBase常用命令

    2024-03-18 11:34:02       19 阅读
  2. 安装vscode及插件

    2024-03-18 11:34:02       21 阅读
  3. SpringBoot整合ElasticSearch应用

    2024-03-18 11:34:02       18 阅读
  4. CSS学习

    2024-03-18 11:34:02       17 阅读
  5. lua gc垃圾回收知识记录

    2024-03-18 11:34:02       20 阅读
  6. IOS面试题object-c 131-135

    2024-03-18 11:34:02       17 阅读
  7. 生成动态指定条件的拼接SQL

    2024-03-18 11:34:02       17 阅读
  8. Photoshop_00000

    2024-03-18 11:34:02       20 阅读
  9. RUST egui部署到github

    2024-03-18 11:34:02       21 阅读
  10. Trustzone和Tee的基本概念区分

    2024-03-18 11:34:02       17 阅读
  11. Ubuntu系统OpenCV推理服务器配置记录

    2024-03-18 11:34:02       19 阅读