双向绑定流程:
- new Vue() 首先执行初始化,对 data 执行响应化处理,这个过程发生在 Observer 中
- 同时对模板执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 Compile 中
- 同时定义一个更新函数和 Watcher ,将来对应数据变化时 Watcher 会调用更新函数
- 由于 data 的某个 key 在一个视图中可能出现多次,所以每个 key 都需要一个管家 Dep 来管理多个 Watcher
- 将来 data 中数据一旦发生变化,会首先找到对应的 Dep ,通知所有 Watcher 执行更新函数
vue 采用数据劫持结合发布者 - 订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter , getter ,在数据变动时发布消息给订阅者,触发相应的监听回调。
原理图:
observer 用来实现对每个 vue 中的 data 中定义的属性循环用 Object.defineProperty() 实现数据劫持,以便利用其中的 setter 和 getter ,然后通知订阅者,订阅者会触发它的 update 方法,对视图进行更新。
执行初始化,对 data 执行响应化处理:
class Vue{
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对 data 选项做响应式处理
observe(this.$data);
// 代理 data 到 vm 上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对 data 选项执行响应化具体操作:
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
})
}
}
编译Compile:对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数。
observer 实现,主要是给每个 vue 的属性用 Object.defineProperty() 。
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function(newVal) {
if (newVal === val) return;
val = newVal;
console.log(val);
// 作为发布者发出通知
dep.notify(); // 通知后 dep 会循环调用各自的 update 方法更新视图
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
简单说一下 Vue2.x 响应式数据原理
Vue 在初始化数据时,会使用 Object.defineProperty 重新定义 data 中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的 watcher),如果属性发生变化会通知相关依赖进行更新操作。
Vue3.x 响应式数据原理
Vue3.x 改用 Proxy 替代 Object.defineProperty 。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
监测数组的时候可能触发多次 get/set ,那么如何防止触发多次呢?
我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger。