通过首次渲染组件中将数据处理成响应式数据的逻辑来理解vue2中的响应式数据,主要是使用了Object.defineProperty()数据劫持,Dep类进行依赖收集与订阅通知,Watcher类进行视图更新与依赖更新等。根据自己的认知对关键步骤做简要描述:
1.数据劫持
- 首先是执行initData(vm)方法,入参是vue组件实例对象,获取到要处理的data对象,并将data对象传入observe函数中进行响应式观测处理
function initData(vm) {
// 获取data函数中的对象
data = vm.$options.data.call(vm,vm)
var ob = observe(data);
ob && ob.vmCount++;
}
function observe(value) {
// 防止已被响应式观测的数据反复观测
if (
value &&
value.hasOwnProperty("__ob__") &&
value.__ob__ instanceof Observer
) {
return value.__ob__;
}
// 是对象或数组
if (Object(value) === value) {
return new Observer(value);
}
}
- 执行new Observer(data),先给data数据添加__ob__属性,再对该数据中的每个属性执行defineReactive方法,这个方法是让数据变为响应式数据的核心
function def(obj, key, val, enumerable) {
Object.defineProperty(obj,key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
})
}
class Observer {
constructor(value) {
this.vmCount = 0;
this.value = value
// 我理解的,这个地方是数组以及数组内子元素用来收集依赖的dep对象
this.dep = new Dep()
def(value, "__ob__", this); // 添加__ob__属性
if (Array.isArray(value)) {
value.__proto__ = arrayMethods // 重写数组原型方法
this.observeArray(value)
} else {
Object.keys(value).forEach(key => defineReactive(value,key))
}
}
observeArray(value) {
value.forEach(v => {
observe(v)
})
}
}
// 数组的处理
let arrayProto = Array.prototype;
let arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
let original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
let result = original.apply(this, args); // 原型方法的结果
let ob = this.__ob__;
let inserted; // 判断是否有新增操作
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted); // 添加观测
ob.dep.notify(); // 订阅通知,数组用来订阅通知的触发地方
return result;
});
});
- 使用Object.defineProperty()方法给每个属性添加get与set方法。get方法中用于处理依赖收集,当有watcher观察者在执行自身的get方法时读取了某一响应式数据,就会触发该响应式数据的get方法。而set方法中用于处理订阅通知,当响应式数据的值被修改时就会触发set方法。
每一个响应式数据都会有一个dep对象,用来存储所收集的依赖与发布订阅通知。
function defineReactive(obj, key) {
let dep = new Dep(); // 创建一个dep对象
let val = obj[key];
// 递归处理
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 此处进行依赖收集逻辑处理
// DepTarget是当前正在读取该响应式数据的watcher观察者
if (DepTarget) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(val)) {
dependArray(val) // 触发数组中的子元素收集依赖
}
}
}
return val;
},
set(newVal) {
val = newVal;
// 此处进行订阅通知逻辑处理
childOb = observe(newVal);
dep.notify();
},
});
return dep;
}
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let e = value[i];
if (e && e.__ob__) {
e.__ob__.dep.depend();
}
if (Array.isArray(e)) {
dependArray(e);
}
}
}
2.Dep类
let uid$1 = 0;
class Dep {
constructor() {
this.id = uid$1++;
this.subs = []; // 存储所收集的依赖
}
depend() { // 将自身存储在对应的watcher观察者中
if (DepTarget) {
// Watcher观察者中添加此dep对象
DepTarget.addDep(this);
}
}
addSub(sub) { // 收集watcher观察者
this.subs.push(sub);
}
notify() { // 订阅通知
let subs = this.subs.filter((v) => v);
subs.forEach((watcherFn) => watcherFn.update());
}
removeSub(sub) { // 移除某一依赖
this.subs = this.subs.filter(v => v.id !== sub.id)
}
}
3.Watcher类
- 在挂载DOM元素前,会先创建一个Watcher观察者实例,观察当前vue组件实例对象
function mountComponent(vm,el) {
// 挂载DOM,读取响应式数据
let updateComponent = function () {
vm._update(vm._render(), hydrating);
};
new Watcher(vm,updateComponent) // 观察者Watcher实例
}
- 会主动触发updateComponent方法的执行,在执行中会读取template模板中使用到的响应式数据
let uid$2 = 0;
let DepTarget = null;
let targetStack = [];
function pushTarget(target) {
targetStack.push(target);
DepTarget = target;
}
function popTarget() {
targetStack.pop();
DepTarget = targetStack[targetStack.length - 1];
}
class Watcher {
constructor(vm, expOrFn) {
this.vm = vm;
vm._watcher = this;
this.id = ++uid$2; // 赋值id
this.deps = []; // dep对象队列
this.newDeps = []; // 新的队列
this.depIds = new Set();
this.newDepIds = new Set();
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 赋值DepTarget,这里赋值是为了在读取响应式数据时收集此依赖
pushTarget(this);
let vm = this.vm;
value = this.getter.call(vm, vm); // 响应式数据读取
popTarget(); // 重置DepTarget
this.cleanupDeps(); // 更新依赖
return value;
}
addDep(dep) {
let id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
update() { // 接收订阅通知,进入异步队列进行统一处理
queueWatcher(this);
}
run() { // 视图更新
this.value = this.get();
}
cleanupDeps() {
this.deps.forEach((v) => {
if (!this.newDepIds.has(dep.id)) {
v.removeSub(this); // 移除不再需要的依赖
}
});
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
}
4.响应式数据的值修改
- 当响应式数据的值被修改时,会通过set方法触发订阅通知,执行watcher.update()方法,再触发queueWatcher(watcher)方法
let has = {}; // 防止多次塞入同一个watcher观察者
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
let id = watcher.id;
if (!has[id]) return;
has[id] = true;
queueWatcher.push(watcher);
if (!waiting) { // 频繁的push,触发一次异步处理即可
waiting = true;
nextTick(flushSchedulerQueue); // nextTick以前介绍过,不再讲述
}
}
- 订阅通知完成后,触发flushSchedulerQueue方法
function flushSchedulerQueue() {
// 排序,确保组件从父组件更新到子组件
queue.sort(function (a, b) {
return a.id - b.id;
});
let watcher;
for (let i = 0; i < queue.length; i++) {
watcher = queue[i];
watcher.run(); // 视图更新
}
resetSchedulerState(); // 重置数据
}
function resetSchedulerState() {
queue.length = 0;
has = {};
waiting = false;
}