vue响应式原理

149 阅读3分钟

vue2响应式原理

mvvm模式

defineProperty(响应式实现的基础)

实现:四大核心模块监听器 Observer、订阅器 Dep、订阅者 Watcher、解析器 Compile

监听器 Observer 核心是通过defineReactive实现数据的监听与劫持,在监听与劫持阶段配合Dep与Watcher实现响应式

// 循环遍历数据对象的每个属性
function observable(obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj);
    keys.forEach((key) => {
        defineReactive(obj, key, obj[key])
    })
    return obj;
}
 // 将对象的属性用 Object.defineProperty() 进行设置
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`${key}属性被读取了...`);
            return val;
        },
        set(newVal) {
            console.log(`${key}属性被修改了...`);
            val = newVal;
        }
    })
}

订阅器 Dep:依赖收集容器,用来容纳所有的“订阅者”,当数据变化的时候后执行对应订阅者的更新函数

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null; //对外暴露Dep.target在后面添加依赖时使用

// 有了Dep之后对defineReactive进行完善设计,get作为收集依赖的入口,set劫持到数据变化时通过dep.notify()通知依赖进行更新
defineReactive: function(data, key, val) {
	var dep = new Dep();
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get: function getter () {
			if (Dep.target) {
				dep.addSub(Dep.target);
			}
			return val;
		},
		set: function setter (newVal) {
			if (newVal === val) {
				return;
			}
			val = newVal;
			dep.notify();
		}
	});
}

订阅者 Watcher:两个核心功能——将自己添加到dep中,确定依赖更新的行为 订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作,其中Dep.target 用于缓存下订阅者,添加成功后再将其去掉就可以了

function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};

解析器 Compile:负责初始化Watcher 解析模板指令,并替换模板数据,初始化视图 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

compileText: function(node, exp) {
	var self = this;
	var initText = this.vm[exp]; // 获取属性值
	this.updateText(node, initText); // dom 更新节点文本值
    // 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图
	new Watcher(this.vm, exp, function (value) { 
		self.updateText(node, value);
	});
}

总结: 总体流程:Compile初始化了Watcher并传入了依赖更新的函数,Watcher中将元素添加进了dep中并向外暴露了更新方法,dep与Observer进行绑定,在set阶段进行依赖的更新(get则是依赖收集对外提供的接口)

vue2响应式存在的问题(需要通过原理解释这两个存在的问题、修改和增删)

对象:对象的响应式只针对已经存在的属性,新添加的属性需要使用 this.$set或者Vue.set方法才可以 数组:数组的响应式一般是通过 7 个数组操作方法来实现响应式,像是arr[0] = 1、arr.length = 0这种是没办法实现响应式的 1.Object.defineProperty是可以检测到数组索引的变化的 2.尤大认为性能消耗太大,于是在性能和用户体验之间做了取舍 3.性能消耗体现在无用的数组索引监听 4.解决方案+原理:Vue.set(vm.items, indexOfItem, newValue);核心还是Object.defineProperty。只是尽可能的避免了无用的数组索引监听 但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是 关于无法进行数组添加元素问题,

vue3响应式原理

vue3响应式原理由proxy实现,其重要意义就是解决了vue2响应式在执行上的缺陷