VUE2数据响应式源码实现

Object.defineProperty()方法

Vue2数据响应式的核心是通过Object.defineProperty实现的,该方法可以对指定对象中的一个属性设置getset方法,进行数据劫持。
当属性被访问时会触发set方法,当属性修改时会触发set方法,通过set方法VUE就能知道哪些数据发生变化了。

const obj={    
    name:'数据响应式'
} 

Object.defineProperty(obj, "name", {
        get() {
            console.log('访问了name属性');
        },
        set(newVal) {
            //newVal接收新值
            console.log('修改了name属性');
        }
    })

console.log(obj.name);//触发get方法
obj.name = '响应式原理'; //触发set方法

自定义defineReactive函数

通过自定义defineReactive函数对Object.defineProperty进行封装,并进行导出

import observe from "./observe";
export default function defineReactive(obj, key, val) {
    if (arguments.length === 2) {
        //传的值等于对象本身的值
        val = obj[key];
    }
    observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('访问了' + key + '属性');
            return val;
        },
        set(newVal) {
            console.log('修改了' + key + '属性');
            if (val === newVal) return;
            val = newVal;
            observe(val);
        }
    })
}

observe函数会在下文中进行讲解

递归监听对象全部属性

由于Object.defineProperty()方法一次只能劫持对象中的一个属性,所以需要使用递归遍历所有属性进行数据劫持。

首先,需要定义个Observer类,该类的功能是将需要被侦测Object对象中每一层级的属性都设置为响应式的。一个Observer类代表一个对象。

import { def } from "./utils"; //封装的defineProperty方法
import defineReactive from "./defineReactive";
//将obj对象中的属性都转成响应式发
export default class Observer {
    constructor(value) {
        console.log("Observer构造器: ", value);
        def(value, '__ob__', this, false);
        this.walk(value);
    }
    walk(value) {
        for (let key in value) { // 遍历当前层属性设置数据劫持
            defineReactive(value, key); 
        }
    }
}

然后创建一个Observe函数,对Observer类进行实例化

import Observer from "./Observer";
export default function observe(value) {
    if (typeof value !== 'object') return;
    let ob;
    if (typeof value.__ob__ !== 'undefined')
        ob = value.__ob__;
    else
        ob = new Observer(value);
    return ob;
}

value对象中的__ob__属性用来存储Observer实例。

整体流程是:调用Observe函数,查看对象中有没有__ob__属性,若没有则new Observer,将产生的实例添加到__ob__上,每个Observer实例都会对它的每个属性进行defindeReactive数据劫持
在defindeReactive函数中对属性进行数据劫持的同时,通过调用Observe函数对该属性再次进行上述操作,若该属性是一个对象,则继续对其子元素进行数据劫持,从而形成递归实现。

针对数组的响应式处理

对于数组,通过方法去改变其属性值时,只会被get方法获取到,但不会被set方法劫持

import observe from './responsive/observe';
const obj = {
    a: 1,
    b: 2,
    c: {
        c1: 333
    },
    d: [1, 2, 3, 4, 5]

}

observe(obj);
obj.b++;
obj.c.c1++;
obj.d.push(6); 

可以看到通过push方法添加了一个值,只有get方法获取到了 

Vue针对 这一现象对Array.prototype中的七个方法进行了重写,并在这些方法里进行判断数据是否被更改。
分别是:push、pop、shift、unshift、splice、sort、reverse

现在我们就来实现一下这七个重写的方法

实现思路是:
1. 创建一个arrayMethods对象,令对象以Array.prototype为原型
2. 将需要重写的方法写在该对象中进行重写
3. 将需要被劫持的对象的原型指向arrayMethods
这样被劫持对象就可以使用Array中被重写过以及没重写过的方法了,也不会改变Array中原有的方法

import { def } from "./utils";
const arrayPrototype = Array.prototype;
//以array.prototype为原型创建一个对象
export const arrayMethods = Object.create(arrayPrototype);
//改写的数组
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

methodsNeedChange.forEach((method) => {
    //备份原始方法
    const original = arrayMethods[method];
    //重写,进行数据劫持
    def(arrayMethods, method, function () {
        console.log('重写成功!');
        console.log(arguments);
        //执行原有方法功能
        original.call(this, arguments);
    }, false);
})

在Observer类中对每个属性进行判断,若是数组则将其原型指向arrayMethods


import { def } from "./utils"; //封装的defineProperty方法
import defineReactive from "./defineReactive";
import { arrayMethods } from "./arrary";
//将obj对象中的属性都转成响应式
export default class Observer {
    constructor(value) {
        console.log("Observer构造器: ", value);
        def(value, '__ob__', this, false);
        if (Array.isArray(value)) {
            //改变数组原型
            Object.setPrototypeOf(value, arrayMethods);
            this.observeArray(value);
        } else
            this.walk(value);
    }
    //对象
    walk(value) {
        for (let key in value) { // 遍历当前层属性设置数据劫持
            defineReactive(value, key);
        }
    }
    //数组
    observeArray(value) {
        for (let key of value) {
            //递归遍历
            observe(key);
        }
    }
}

这样最基本的重写功能就写好了。

 需要注意的是,push、shift、splice方法会新增数组元素,所以还需要对新增元素进行数据劫持

    //重写,进行数据劫持
    def(arrayMethods, method, function () {
        let inserted = [];
        let args = [...arguments];
        //执行原有方法功能
        const result = original.apply(this, arguments);
        //处理新增元素
        switch (method) {
            case 'push':
            case 'shift':
                inserted = args;
                break;
            case 'splice':
                inserted = args.slice(2);
                break;
        }
        //对新添加的函数设置数据劫持
        if (inserted) {
            this.__ob__.observeArray(inserted);
        } 
        //判断数组被更改
        console.log('响应');
        //返回结果
        return result;
    }, false);

依赖收集

什么是依赖收集?

这里的依赖指的是需要用到数据的地方,比如在vue2中,用到数据的组件就是依赖

收集就是将这些依赖全部存储到一个集合中,然后当数据发送变化时,通知这个集合中的所有组件,组件最终通过diff算法进行视图更新

我们会在getter中收集依赖,在setter中触发依赖
依赖收集主要通过Dep类Watcher类实现

Dep类和Watcher类

  • 将依赖收集的代码封装到Dep类中,该类是专门管理依赖的,每个Observer实例中都会有一个Dep实例
  • Watcher类是一个中介,数据发生改变时通过Watcher中转,通知组件

​ 深入响应式原理 — Vue.js (vuejs.org)​

借助VUE官网中的流程图进行分析:

Touch指的是使用到了某个被劫持的数据,然后就会在getter中收集依赖(需要用到Dep类中的depend方法进行收集依赖),当该数据被修改时,会在setter中触发依赖(需要用到Dep类中的notify方法进行触发依赖),通过Watcher通知视图更新,然后组件进行最终的更新。

Dep类成员

成员属性:
  •  subs:用来存储Watcher的数组。当数据更新时,Dep会通知所有存储的Watcher进行视图更新。
  • target(静态属性):指向当前Watcher实例,保证在同一时刻只有一个Watcher。
成员函数:
  •  depend:添加依赖。使dep与当前watcher建立依赖关系
  • notify:通知更新。当依赖属性发生变化时(即set触发),通过该函数遍历存储在subs中的所有Watcher进行更新视图。
  • addSub:添加watcher至subs中。
  • removeSub:删除subs中的watcher。
let uid = 0;
export default class Dep {
    constructor() {
        this.id = uid++;
        //当前属性的依赖集合
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    //收集
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    //通知更新
    notify() {
        //浅拷贝
        const subs = this.subs.slice();
        for (let i = 0; i < subs.length; i++) {
            console.log("依赖通知");
            subs[i].update();
        }
    }
}

说明:Dep类的作用就是用来管理所有Watcher,确保数据变化时能够更新数组。通过subs数组存储Watcher,通过notify函数通知所有Watcher更新,因为Dep类是唯一的,所以其target属性确保了同一时刻全局只有一个Watcher

Dep类使用场合:
  • Observer类中:每个Observer实例中都有一个有一个Dep实例
    import Dep from './Dep';
    export default class Observer {
        constructor(value) {
            console.log("Observer构造器: ", value);
            //对象及其子对象都添加一个dep实例
            this.dep = new Dep();
           //......
        }
      }
  • defineReactive函数中:当属性被访问时,进行依赖收集,当属性被修改时,通知组件更新
    
    import observe from "./observe";
    import Dep from './Dep';
    export default function defineReactive(obj, key, val) {
        const dep = new Dep();
        if (arguments.length === 2) {
            //传的是对象或空
            val = obj[key];
        }
        const childOb = observe(val);
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                console.log('访问了' + key + '属性');
                //收集依赖
                if (Dep.target) {
                    dep.depend();
                    if(childOb) 
                        childOb.dep.depend();
                }
                return val;
            },
            set(newVal) {
                console.log('修改了' + key + '属性');
                if (val === newVal) return;
                val = newVal;
                //通知依赖:当数据发生改变时,通知组件更新
                dep.notify();
                //当设置新值时也进行数据劫持
                observe(val);
            }
        })
    }
  • Watcher类中:将当前Watcher实例指向Dep.target

Watcher类

成员属性:

  • target:被数据劫持的对象
  • getter:深度优先方法,返回指定的最内层属性值,

  • callback:其他操作

  • value:获取的值

成员函数:

  • update:进行一系列更新操作
  • get:返回最新的属性值,会在初始化实例和视图更新时被调用


import Dep from './Dep';

let uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        this.id = uid++;
        this.target = target;
        this.getter = parsePath(expression);
        this.callback = callback;
        this.value = this.get();
    }
    //当属性首次被使用和视图更新时触发
    get() {
        //进入依赖收集
        Dep.target = this;
        const obj = this.target;
        let value;
        try {
            value = this.getter(obj); //获取
        } finally {
            //当前Watcher退出依赖收集,让下个Watcher进入
            obj.target = null;
        }
        return value;  //获取最内层属性
    }
    update() {
        this.run();
    }
    run() {
        this.getAndInvoke(this.callback);
    }
    getAndInvoke(cb) {
        const value = this.get();
        const oldValue = this.value;
        if (value !== this.value || typeof value === 'object') {
            //获取最新更新
            this.value = value;
            cb.call(this.target, value, oldValue);
        }
    }
}
//深度优先 返回一个查找最内层的属性的方法
function parsePath(exp) {
    const segments = exp.split('.')
    return (obj) => {
        for (const i of segments) {
            obj = obj[i];
        }
        return obj;
    }
}
说明:

为方便理解,可以认为每个组件实例都对应一个Watcher实例,它会监听当前组件中被数据劫持的数据,当数据发送变化时,会触发dep.notify方法,该方法会调用所有依赖了该数据的Watcher实例的update方法实现更新。

视图更新总结:

每个组件都对应一个Watcher实例,当数据被某一组件实例使用时,该实例会在getter中被dep进行依赖收集至subs数组中。当数据发生变化时,会被setter劫持,触发subs数组中的所有Watcher实例的update方法完成更新。

源码地址:jyq1650940731/vue-reactive: vue数据响应式源码 (github.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值