理解 Vue 2 的响应式原理:数据劫持与依赖收集的背后

在Vue2中,响应式系统是一切魔法的源头,无论是模板中的数据绑定,还是computed,watch的精准监听,都离不开Vue背后的响应式机制,本文将从源码角度出发,结合实例,深入剖析vue2是如何通过数据劫持(Object.defineProperty)和依赖收集实现响应式的

一.Vue2响应式系统基本原理

vue2中响应式原理主要就是利用object.defineProperty劫持对象属性的读写操作.在读取时进行依赖收集,在写入时进行派发更新 

  1. 数据劫持: Vue2使用Object.defineProperty函数对组件的data对象属性进行劫持,当读取data中的属性时触发get,当修改data中的属性时触发set 
  2. 依赖收集: 当模版或者计算属性等引用了data中响应式数据时,Vue将这些消费者收集起来,建立数据与消费者之间的关联  
  3. 派发更新 当响应式数据变化时,通过dep来执行watcher的notify方法进行通知更新

1.1 数据劫持 Object.defineProperty  

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

局限性: Object.defineProperty只能劫持对象的属性,因此Vue2无法自动侦测到对象属性的添加或是删除,以及直接通过索引修改数组项的情况,Vue解决这个问题的方式是提供了全局方法如Vue.set和Vue.delete, 以及修改数组时应该使用的一系列方法(如push,splice等 )
data() {
  return {
            user: { name: "Alice" },
            list: ['apple','banana']
          };
},
methods: {
  addAge() {
    this.user.age = 25; // ❌ 视图不更新

    this.$set(this.user, "age", 25); // ✅ 添加属性

    //数组
    this.list[1]='grape'; //视图不更新

    this.$set(this.list, 1, 'grape'); // ✅ 视图更新
    
    

   

  },
  deleteName() {
    delete this.user.name; // ❌ 视图不更新
    this.$delete(this.user, "name"); // ✅ 删除属性
  }

二. 响应式的实现关键类: Observer,Dep,Watcher,defineReactive

Vue的响应式系统中主要涉及一下三个核心类: 

  • Observer:  用于对对象进行递归响应式处理; 
  • Dep(依赖): 每个被劫持的属性都会对应一个Dep实例,用于收集依赖并在数据变更时通知更新; 
  • Watcher(观察者): 每个组件或计算属性,侦听器在初始化时会创建一个Watcher,用于响应数据变化 
  • defineReactive; 具体实现对属性的劫持,依赖收集 & 通知更新

他们之间的关系如下 

          data.x
            |
      defineReactive
            |
          getter <------ Dep.target = 当前 Watcher
            |
          Dep.depend()
            |
          Dep ←———— Watcher(视图更新逻辑)
            |
          setter
            |
         Dep.notify() —→ Watcher.update()
 

三. Observer( ): 让一个对象变成响应式

Vue会在初始化数据时调用observe( )方法,每个对象在挂载前,都先会被"响应化

function observe(value){
   //如果传入的不是对象或者null,就不做任何处理,直接返回     
   if(typeof value !== 'object' || value===null) return ;    
   //如果是,对其进行观察处理,并返回一个Observer实例
   return new Observe(value)
}

而Observer的核心逻辑就是对对象的每个属性进行递归处理

//Observer.js 
import { defineReactive } from './defineReactive.js';
import { Dep } from './dep.js';
import { arrayMethods, augmentArray } from './arrayMethods.js';
import { observe } from './observe.js';

class Observer {
    constructor(value) {

        //要被观察的数据对象或数组
        this.value = value;
        //实例化一个依赖收集器Dep,用于在这个对象本身发生变化时通知依赖更新
        this.dep = new Dep();

        //标记这个对象已经被响应式处理过,防止重复处理
        object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,
        });
        //如果是数组
        if (Array.isArray(value)) {
            //重写数组方法实现响应式
            this.augmentArray(value);
            //递归监听数组元素 
            this.observeArray(value);
        } else {
            //如果是普通对象,给对象的所有袁术添加getter.setter 
            this.walk(value);
        }

    }

    //对象属性递归响应式处理
    walk(obj){
        Object.keys(obj).forEach(key=>{
            defineReactive(obj,key,obj[key]);
        })
    }
    //重写数组原型方法 
    augmentArray(arr){
        arr.__proto__=arrayMethods
    }

    //递归观察数组元素
    
    observeAarray(items){
       items.forEach(item => observe(item));
    }
}

 



 四. defineReactive 

defineReactive:  Vue想要追踪某个属性的读取和修改,就必须在这个属性的getter中收集依赖,在setter中通知更新,而defineReactive就是专门用来包裹一个对象的属性,让它具备这些能力 

举个例子

const obj={ }

defineReactive(obj,'msg','hello')


conosle.log(obj.msg); //触发getter, 收集依赖

obj.msg='world'; //触发setter,派发更新

//defineReactive.js 
import { Dep } from './dep.js';
import { observe } from './observe.js';
function defineReactive(obj, key, val) {

    const dep = new Dep();//每个属性都拥有自己的依赖收集器
    observe(val); //如果属性值还是对象,递归处理

    object.defineProperty(obj, key, {
        get() {
            //如果现在处于依赖收集阶段
            if (Dep.target) {
                dep.depend(); //依赖收集
            }
            return val;
        },
       set(newVal) {
            if (newVal === val) return;
            val = newVal;
            //对新值进行递归响应式处理,如果它是对象或数组,并赋值给childOb
            childOb = observe(newVal);
            //通知依赖当前属性的Watcher(计算属性,渲染函数,侦听器等)重新执行 
            dep.notify();
        }
    });
}

五. Dep: 依赖收集与派发更新

Dep是一个依赖收集器,它的主要职责是: 

(1) 存储观察者: Dep实例内部维护了一个观察者(Watcher)对象的数组,在依赖收集阶段,观察者对象会被添加到Dep实例的数字中,而在派发更新阶段,Dep类则会遍历这个数组,通知所有的观察者
(2)依赖收集: Dep类提供了addSub方法,用于在依赖收集阶段添加新的观察者,当数据的getter函数被调用时,Dep会把当前正在评估的观察者添加到自身的观察者列表中
(3)派发更新: Dep类提供了notify方法,用于在数据发生变更时通知所有的观察者,当数据的setter函数被调用时,Dep会遍历自己观察者列表,并调用它们的update方法 
let uid = 0;
class Dep {
    //每个Deo实例代表一个"响应式属性"的依赖容器
    //每一个被defineReactive()包括的属性都会对应一个Dep实例
    //用于存放所有依赖这个属性Watcher(比如组件渲染函数,计算属性,侦听器)


    //用于给每个Dep实例生成唯一ID(调试用,无功能性作用)
    constructor() {
        this.id = uid++;
        //用于存放所有依赖这个属性的Watcher
        this.subs = [];
    }
    //把某个Watcher添加到当前Dep的依赖列表中
    //这个方法一般在Watcher.addDep(dep)中调用
    addSub(sub) {
       this.subs.push(sub);
    }

    //如果Dep.target不为空(代表当前有一个Watcher正在运行),就调用它哦addDep(this)
    //换句话说: 这个Dep告诉当前Watcher:"我被你用到了,你得订阅我"
    //注意: 不是dep.addSub(watcher),而是watcher.addDep(this)
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }
    //数据变化时触发通知,让所有依赖的Watcher执行更新逻辑(update())
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}
  • subs 数组保存所有依赖(Watcher)
  • 依赖收集通过dep.depend( )和全局Dep.target配合完成; 
  • 数据变化时调用notify( ),触发所有watcher更新 

六.观察者 Watcher

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

主要职责: 
          (1) 依赖收集: Watcher在初始化时会调用自己的get方法去读取数据,这会触发数据的getter函数从而进行依赖收集,在getter函数中,当前Watcher实例会被添加到数据对一个的Deo实例中
          (2)执行更新: 当数据发生变化,Dep实例调用notify方法时, Watcher实例会接收到通知,然后调用自己的update方法以触发回调 
let watcherId = 0;

class Watcher {
    constructor(vm, expOrFn, cb) {
        //为每个watcher分配唯一id(用于优化)
        this.id = watcherId++;
        //当前组件实例 
        this.vm = vm;
        this.getter = expOrFn; //表达式函数或渲染函数,比如render或某个计算属性getter 
        this.cb = cb;//数据更新后调用的回调,比如更新DOM 
        this.deps = [];//当前Watcher依赖了哪些Dep 
        this.get(); //初次执行getter,触发依赖收集 
    }
    get() {
        Dep.target = this; //当前正在求值的watcher(静态属性)
        this.getter.call(this.vm); //执行getter,触发data的getter,从而进行依赖收集 
        Dep.target = null; //清空target,避免污染其他依赖收集
    }
    //每个响应式数据(通过defineReactive实现)在getter被触发时,会把Dep.target添加到自己的subs(订阅者列表)中,
    //最终完成Dep记录Watcher,也就是Watcher鼎娱乐Dep 

    //添加依赖 
    addDep(dep) {
        dep.addSub(this); //将当前watcher添加到Dep的订阅者列表中
        this.deps.push(dep); //记录依赖了哪个Dep(用于后续取消依赖)
    }
    update() {
        //这里执行视图更新逻辑,调用回调 
        this.cb();
    }
    //当某个响应式数据发生变化,它的dep.notify()方法会被调用
    //notify() 会遍历所有订阅它的Watcher,执行他们的update()方法

}
  • 创建watcher后会立即执行get( )进行依赖收集
  • 依赖数据的getter被调用时,收集watcher; 
  • 数据变化时调用dep.notify( )时,watcher.update()被触发,更新视图

七. 数组响应式的实现(结合Observer和Dep)

  • Observer会为数组替换原型,绑定重写后的变异方法
  • 这些方法执行时调用dep.notify( ),通知依赖更新; 
  • 数组内部的对象元素也会被递归观察,实现深度响应式

1.arraymethods 是vue2中通过劫持数组原型方法来模拟数组响应式

// arraymethods.js 

//获取原始的Array原型,用于保留原生方法引用
const arrayProto = Array.prototype;

//创建一个新对象,继承自原生数组原型
//我们将在这个对象上"重写"某些变更方法 

const arrayMethods = Object.create(arrayProto);

// 需要被重写的 7 个数组变更方法
const methodsToPatch = [
    'push',    // 尾部插入
    'pop',     // 尾部删除
    'shift',   // 头部删除
    'unshift', // 头部插入
    'splice',  // 插入/删除指定位置
    'sort',    // 排序
    'reverse'  // 反转
];

//遍历每一个方法,进行重写
methodsToPatch.forEach(function (method) {
    //保留原始方法的引用,稍后调用 
    const original = arrayProto[method];

    //在arrayMethods 上定义一个新的同名方法 
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            //执行原始方法,拿到其返回值 
            const result = original.apply(this, args);

            //拿到当前数组的Observer实例
            const ob = this.__ob__;

            //用于存储新插入的元素(如果有)
            let inserted;
            switch (method) {
                case 'push':
                case 'unshift':
                    // 新元素全部在参数中
                    inserted = args;
                    break;
                case 'splice':
                    // splice(start, deleteCount, ...inserted)
                    // 插入的新元素从第三个参数开始
                    inserted = args.slice(2);
                    break;
            }

            //对插入的新元素做响应式处理
            if (inserted) ob.observeArray(inserted);

            //通知依赖更新,触发视图刷新
            ob.dep.notify();
            //返回原始方法的执行结果 
            return result;
        },
        enumerable: false,
        writable: true,
        configurable: true
    });
});

在Observer中使用

if(Array.isArray(value)){

    protoAugment(value,arrayMethods);

    this.observeArray(value)

}

八. 工作流程总结

  1. Vue初始化数据时,调用observe(data); 
  2. 对象每个属性调用defineReactive,加getter/setter;
  3. 组件渲染时,会创建对应的Watcher,并执行渲染函数
  4. 渲染函数访问数据属性,触发getter,Dep.target收集依赖Watcher; 
  5. 数据被修改,setter调用dep.notify( ),触发所有依赖watcher执行update( )
  6. watcher执行视图更新,组件自动刷新
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值