Vue2依赖收集

Vue2


响应式数据

Vue2的对象数据中数据响应式是通过Object.defineProperty将对象上的数据属性替换成访问器属性

Object.defineProperty

Object.defineProperty(obj, key, desc);
  • obj: 需要定义属性的当前对象
  • key: 定义的属性名
  • desc: 属性描述符

示例

const girl = {};

Object.defineProperty(girl, 'sex', {
    value: '女' 
})

console.log(girl); // { sex: '女' }

这样就在girl这个对象上添加了sex属性,这样写与girl.sex = '女'无异,定义的都是数据属性

let girl = {}, val = '女';

Object.defineProperty(girl, 'sex', {
    get() {
        return val;
    },
    set(newVal) {
        val = newVal;
    },
})

console.log(girl);

通过gettersetter方法在该对象上定义了一个访问器属性,当获取该属性时就会执行getter方法,修改该属性时就会执行setter方法

在浏览器的DevTools中会这样显示{ sex: (...) },点击...就会去访问getter方法,然后得到该属性的值

依赖收集流程

image dep

  1. Observer实例通过Object.defineProperty()方法劫持data上的属性,重新定义为访问器属性,同时并创建一个Dep实例,与data上的属性一一对应
  2. Watcher实例访问data上的属性时会触发对应属性的getter方法,getter方法会再调用dep.depend()方法将该Watcher实例添加到与该属性对应的Dep实例的subs数组中,来收集依赖
  3. 当数据被修改,触发setter方法,setter方法会再调用dep.notify()来通知被修改的属性对应的Depsubs数组中所有的Watcher进行更新操作

Observer类

方法


  • constructor
  1. 创建一个 Dep 对象,该对象用于数组的收集依赖和通知更新。
  2. 通过 Object.defineProperty 在数据对象上添加一个不可枚举的 __ob__ 属性,其值为当前 Observer 实例,防止重复观察同一数据对象。
  3. 判断是否是数组类型,如果是数组则调用 observeArray() 方法,否则调用 walk() 方法
constructor(data) {
    // 因为Observer劫持的data一定是一个对象,每个对象身上又挂载了__ob__(该Observer实例)
    // 如此就可以通过__ob__访问到该Dep对象
    // 是为了当向数组或对象中添加数据时候能够依赖收集
    this.dep = new Dep(); 

    Object.defineProperty(data, '__ob__', {
        value: this,
        enumerable: false
    })

    if (Array.isArray(data)) {
        Object.setPrototypeOf(data, arrayMethods)
        this.observeArray(data);
    } else {
        this.walk(data);
    }
}
  • walk()
    遍历data上的属性并调用defineReactive()将属性定义为访问器属性
walk(data) { 
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    })
}
  • observeArray
    遍历数组,劫持该数组
// Observer constructor():
// if (Array.isArray(data)) {
//     Object.setPrototypeOf(data, arrayMethods)
//     this.observeArray(data);
// }

const arrayProto = Array.prototype; // 获取Array构造函数的原型
export let arrayMethods = Object.create(arrayProto); 拷贝一份Array的原型对象

const methodsToPatch = [ // 将会修改数组的方法存入一个数组内
    'push',
    'pop',
    'shift',
    'unshift',
    'spilce',
    'sort',
    'reverse'
]

methodsToPatch.forEach(method => {
    arrayMethods[method] = function(...args) {
        const result = arrayProto[method].call(this, ...args);
        let inserted, ob = this.__ob__;

        switch(method) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                inserted = args.slice(2);
                break;
        }

        inserted && ob.observeArray(inserted); // 劫持新添加的数组

        ob.dep.notify(); // 调用Observer实例上的Dep实例的notify方法通知更新

        return result;
    }

})

observeArray(data) {
    data.forEach(item => observe(item));
}
  • ...
defineReavtive

  1. 创建一个 Dep 对象,该对象在gettersetter中调用形成了一个闭包,每一个数据都对应一个Dep对象
  2. 定义一个getter方法,在getter中调用dep.depend()方法将当前Watcher实例添加到与该属性对应的Dep实例的subs数组中,来收集依赖
  3. 定义一个setter方法,在setter中调用dep.notify()来通知被修改的属性对应的Depsubs数组中所有的Watcher进行更新操作
export function defineReactive(obj, key, val) {

    const dep = new Dep(); // 在getter和setter中访问dep变量形成了一个闭包

    Object.defineProperty(obj, key, {
        get() {
            dep.depend(); // 收集依赖,将Watcher放入subs数组中
            return val; // 返回值
        },

        set(newVal) {
            observe(newVal); // 继续劫持新值
            val = newVal; // 设置新值
            dep.notify(); // 派发更新
        }
    })
}
observe

劫持一个对象

export function observe(value) {
    // 如果不是对象则直接返回
    if (typeof value !== 'object' || value == null) return;

    // 如果已经有Observer实例,则直接返回
    if (value && value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
        return value.__ob__;
    }

    // 创建实例观察数据
    return new Observer(value);

}

Observer实例上创建的Dep实例是为了收集对象(数组)的依赖,而在defineReactive上创建Dep实例是为了收集数据的依赖

Dep类

方法


  • constructor
constructor() {
    this.subs = [];
}
  • addSub
    将一个Watcher实例添加到subs数组中
addSub(sub) {
    this.subs.push(sub);
}
  • removeSub
    subs数组中移除一个Watcher实例
removeSub(sub) {
    this.subs[this.subs.indexOf(sub)] = null;
}
  • depend
depend() {
    if (Dep.target) {
        this.addSub(Dep.target); // Dep.target是Dep构造函数上的一个属性
    }
}
  • notify
const subs = this.subs.slice(); // 复制一份
subs.forEach(sub => sub.update()); // 通知更新
  • ...

Watcher类

方法


  • constructor
constructor(vm, expr, cb, options) {
    this.vm = vm // Vue实例
    this.expr = expr; // 监听的表达式或方法 例: user.name
    this.cb = cb; // 回调函数
    this.getter = parsePath(expr); // 把路径解析成对象
    this.options = options; // 是否是一个渲染Watcher
}
  • get
get() {
    Dep.target = this; // 将this指向当前的Watcher实例
                
    const vm = this.vm;

    let val;

    try {
        val = this.getter.call(vm, vm); // 执行parsePath(expr)

        // export default function parsePath(path) {

        //     const segments = path.split('.');

        //     return function(obj) {

        //         if (!obj) return;

        //         for (let i = 0; i < segments.length; i++) {    
        //             obj = obj[segments[i]];
        //         }

        //         return obj;
        //     }

        // }

        // parsePath是传入一个路径,返回一个函数,这个函数接收一个对象作为参数,然后通过这个路径从对象中取出值,并返回这个值
        // parsePath('user.name')({user: {name: 'jack'}}) // jack
        
    }  finally {
        Dep.target = null; // 将Dep.target重置为null
    }

    return val;
}
  • ...

总结


!. 出于对性能的考虑,Vue 没有对数组类型的数据使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持,直接通过数组索引来设置元素时,Vue 2不能直接检测到变化。例如,arr[index] = value 这样的操作不会触发视图更新。为了解决这个问题,Vue 2 提供了一组特殊的数组方法,如 $set$delete,用于对数组进行修改并触发视图更新。

依赖收集的目的是建立从数据到依赖它的 Watcher 实例之间的关系,以便在数据发生变化时,通知相关的 Watcher 执行更新操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值