Vue通过 Object.defineProperty 的 getter/setter 对收集的依赖项进行监听,在属性被访问和修改时通知变化,进而更新视图数据。
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;Dep
,用来收集订阅者,对监听器 Observer
和 订阅者 Watcher
进行统一管理;Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;Object.defineProperty
**Object.defineProperty()**
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol
。
descriptor
要定义或修改的属性描述符。
// 响应式函数
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`)
return val
},
set(newValue) {
console.log(`将${key}由->${val}->设置成->${newValue}`)
if (val == newValue) {
return;
}
val = newValue
}
})
}
const data = {
name: 'Tom',
age: 3
}
const observe = function(data) {
for (let key in data) {
defineReactive(data, key, data[key])
};
}
observe(data);
console.log(data.name)
// 访问了name属性
// Tom
data.name = 'Jerry' // 将name由->Tom->设置成->Jerry
console.log(data.name)
// 访问了name属性
// Jerry
const Vue = function(options) {
const self = this;
// 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
this._data = options.data.apply(this);
}
// 挂载函数
this.mount = function() {
new Watcher(self, self.render);
}
// 渲染函数
this.render = function() {
console.log(self._data.text);
}
// 监听this._data
observe(this._data);
}
const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})
vue.mount(); // in get /n hello world
vue._data.text = '123'; // in watcher update /n in get /n 123
const observe = function(data) {
return new Observer(data);
}
const Observer = function(data) {
// 循环修改为每个属性添加get set
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
const defineReactive = function(obj, key, val) {
// 局部变量dep,用于get set内部调用
const dep = new Dep();
Object.defineProperty(obj, key, {
// 设置当前描述属性为可被循环
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify();
}
});
}
Vue 初始化时,进行了数据的 get、set 绑定,并创建了一个 Dep 对象。
对于数据的 get、set 绑定我们并不陌生,但是 Dep 对象什么呢?
Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅,我们一起来剖析一下。
const Dep = function() {
const self = this;
// 收集目标
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
// 当有目标时,绑定Dep与Wathcer的关系
this.depend = function() {
if (Dep.target) {
// 这里其实可以直接写self.addSub(Dep.target),
// 没有这么写因为想还原源码的过程。
Dep.target.addDep(self);
}
}
// 为当前收集器添加Watcher
this.addSub = function(watcher) {
self.subs.push(watcher);
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function() {
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
}
const Watcher = function(vm, fn) {
const self = this;
this.vm = vm;
// 将当前Dep.target指向自己
Dep.target = this;
// 向Dep方法添加当前Wathcer
this.addDep = function(dep) {
dep.addSub(self);
}
// 更新方法,用于触发vm._render
this.update = function() {
console.log('in watcher update');
fn();
}
// 这里会首次调用vm._render,从而触发text的get
// 从而将当前的Wathcer与Dep关联起来
this.value = fn();
// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
// 造成代码死循环
Dep.target = null;
}
Watcher 实现了渲染方法 render
和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染。
我们再拉通串一下整个流程:Vue 通过 defineProperty
完成了 Data 中所有数据的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。
Dep的作用是什么?
用两个例子来看看依赖收集器的作用吧。
例子1,毫无意义的渲染是不是没必要?
const vm = new Vue({
data() {
return {
text: 'hello world',
text2: 'hey',
}
}
})
当vm.text2
的值发生变化时,会再次调用render
,而template
中却没有使用text2
,所以这里处理render
是不是毫无意义?
针对这个例子还记得我们上面模拟实现的没,在Vue
的render
函数中,我们调用了本次渲染相关的值,所以,与渲染无关的值,并不会触发get
,也就不会在依赖收集器中添加到监听(addSub
方法不会触发),即使调用set
赋值,notify
中的subs
也是空的。OK,继续回归demo,来一小波测试去印证下我说的吧。
const vue = new Vue({
data() {
return {
text: 'hello world',
text2: 'hey'
};
}
})
vue.mount(); // in get
vue._data.text = '456'; // in watcher update /n in get
vue._data.text2 = '123'; // nothing
例子2,多个Vue实例引用同一个data时,通知谁?是不是应该俩都通知?
let commonData = {
text: 'hello world'
};
const vm1 = new Vue({
data() {
return commonData;
}
})
const vm2 = new Vue({
data() {
return commonData;
}
})
vm1.mount(); // in get
vm2.mount(); // in get
commonData.text = 'hey' // 输出了两次 in watcher update /n in get
当面试官问你Vue响应式原理,你可以这么回答他
图解 Vue 响应式原理
Object.defineProperty
实现的
vm.items[index] = newValue
vm.items.length = newLength
Object.defineProperty
,实现对象的深度监听,需要一次性递归到底。对于层级比较深的数据来说,计算量比较大。无法监听新增属性/删除属性,这也是为什么 Vue2 中对象新增属性需要使用 Vue.set()
, 删除属性需要使用 Vue.delete()
Proxy
来实现的
注: