Vue响应式原理
如何追踪变化
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,每一个属性都有一个自己对应的Dep订阅器。
在对’#app’里面的子元素进行编译时,会对每一个需要获取key的位置创建一个订阅者watcher
(1)构造时将这个watcher赋值给Dep.newSub,然后读取key,触发get(),在get函数中触发当前key对应的订阅器dep的addSub事件,将这个watcher加入到key的订阅者数组中------订阅key
(2)每次key改变,就会触发set,从而触发dep的notify事件,通知该key的订阅者数组subs所有成员更新最新数据并把新值通过回调函数传回,更新视图,达到追踪变化的效果------发布消息
检测变化的注意事项
受现代 JavaScript 的限制,Vue 无法检测到对象属性的添加或删除。由于 Vue 只在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。
声明响应式属性
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值
/* 监听器 */
class Observer {
constructor(data) {
this.observer(data);
}
/* 遍历data设置监听 */
observer(obj) {
if (obj && typeof obj == 'object') {
/* 获取obj所有子属性 */
let keys = Object.keys(obj);
/* 遍历子属性,全部设置监听 */
keys.forEach(key => {
this.defineRactive(obj, key, obj[key]);
})
}
}
/* 监听 */
defineRactive(obj, key, val) {
/* 若子对象则继续遍历 */
this.observer(val);
/* 给每个值设置一个订阅器 */
let dep = new Dep();
/* 监听改变函数 */
Object.defineProperty(obj, key, {
get() {
/* 判断是否有新订阅者 */
Dep.newSub && dep.addSub();
return val;
},
set: (newVal) => {
/* 相等则没必要更新 */
if (val !== newVal) {
/* 更新val已有的属性的数据,对新加的属性进行监听 */
if (typeof val == 'object') {/* 对象或数组 */
/* 更新内部内容 */
this.updateObj(val, newVal);
} else {
val = newVal;
}
/* 发布 */
dep.notify();
}
}
})
}
/* 对象更新设置子数据更新 */
updateObj(obj1, obj2) {
if (obj1 instanceof Object || obj1 instanceof Array) {
for (let key in obj1) {
if (obj1[key] instanceof Object || obj1[key] instanceof Array) {
this.updateObj(obj1[key], obj2[key]);
} else {
obj1[key] = obj2[key];
}
}
} else {
obj1 = obj2;
}
}
}
/* 订阅器 */
class Dep {
constructor() {
/* 初始化订阅者数组 */
this.subs = [];
}
/* 增加订阅者 */
addSub() {this.subs.push(Dep.newSub);}
/* 发布 */
notify() {
/* 通知所有订阅者更新数据 */
this.subs.forEach(sub => {
sub.update();
})
}
}
/* 订阅者 */
class Watcher {
constructor(vm, prop, cb) {
this.vm = vm;
this.prop = prop;
this.cb = cb;
/* 加入订阅者数组 */
this.join();
}
/* 加入订阅者数组 */
join() {
/* 将自己设置为订阅器的新订阅者 */
Dep.newSub = this;
/* 读取一次数据触发get,从而间接触发addSub */
this.oldVal = CompileUtil.getVal(this.vm, this.prop);
/* 重置,否则会重复添加 */
Dep.newSub = null;
}
/* 更新 */
update() {
/* 对比新旧值决定是否更新 */
let newVal = CompileUtil.getVal(this.vm, this.prop);
if (this.oldVal !== newVal) {
this.oldVal = newVal;
/* 运行回调函数 */
this.cb(newVal);
}
}
}
异步更新队列
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})