乐趣区

vue响应式系统–observe、watcher、dep

Vue 的响应式系统
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身,而不用手动处理数据到视图的渲染,避免了繁琐的 DOM 操作,提高了开发效率。
vue 的响应式系统依赖于三个重要的类:Dep 类、Watcher 类、Observer 类,然后使用发布订阅模式的思想将他们揉合在一起(不了解发布订阅模式的可以看我之前的文章发布订阅模式与观察者模式)。

Observer
Observe 扮演的角色是发布者,他的主要作用是调用 defineReactive 函数,在 defineReactive 函数中使用 Object.defineProperty 方法对对象的每一个子属性进行数据劫持 / 监听。
部分代码展示
defineReactive 函数,Observe 的核心,劫持数据,在 setter 中向 Dep(调度中心)添加观察者,在 getter 中通知观察者更新。
function defineReactive(obj, key, val, customSetter, shallow){
// 监听属性 key
// 关键点:在闭包中声明一个 Dep 实例,用于保存 watcher 实例
var dep = new Dep();

var getter = property && property.get;
var setter = property && property.set;

if(!getter && arguments.length === 2) {
val = obj[key];
}
// 执行 observe,监听属性 key 所代表的值 val 的子属性
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 获取值
var value = getter ? getter.call(obj) : val;
// 依赖收集:如果当前有活动的 Dep.target(观察者 –watcher 实例)
if(Dep.target) {
// 将 dep 放进当前观察者的 deps 中,同时,将该观察者放入 dep 中,等待变更通知
dep.depend();
if(childOb) {
// 为子属性进行依赖收集
// 其实就是将同一个 watcher 观察者实例放进了两个 dep 中
// 一个是正在本身闭包中的 dep,另一个是子属性的 dep
childOb.dep.depend();
}
}
return value
},
set: function reactiveSetter(newVal) {
// 获取 value
var value = getter ? getter.call(obj) : val;
if(newVal === value || (newVal !== newVal && value !== value)) {
return
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 新的值需要重新进行 observe,保证数据响应式
childOb = observe(newVal);
// 关键点:遍历 dep.subs,通知所有的观察者
dep.notify();
}
});
}
Dep
Dep 扮演的角色是调度中心 / 订阅器,主要的作用就是收集观察者 Watcher 和通知观察者目标更新。每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的 watch,让订阅者执行自己的 update 逻辑。
部分代码展示
Dep 的设计比较简单,就是收集依赖,通知观察者
//Dep 构造函数
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
// 向 dep 的观察者列表 subs 添加观察者
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
// 从 dep 的观察者列表 subs 移除观察者
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend() {
// 依赖收集:如果当前有观察者,将该 dep 放进当前观察者的 deps 中
// 同时,将当前观察者放入观察者列表 subs 中
if(Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify() {
// 循环处理,运行每个观察者的 update 接口
var subs = this.subs.slice();
for(var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};

//Dep.target 是观察者,这是全局唯一的,因为在任何时候只有一个观察者被处理。
Dep.target = null;
// 待处理的观察者队列
var targetStack = [];

function pushTarget(_target) {
// 如果当前有正在处理的观察者,将他压入待处理队列
if(Dep.target) {
targetStack.push(Dep.target);
}
// 将 Dep.target 指向需要处理的观察者
Dep.target = _target;
}

function popTarget() {
// 将 Dep.target 指向栈顶的观察者,并将他移除队列
Dep.target = targetStack.pop();
}
Watcher
Watcher 扮演的角色是订阅者 / 观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性 computed,vue 会把该属性所依赖数据的 dep 添加到自身的 deps 中),当被观察的值发生变化时,会接收到来自 dep 的通知,从而触发回调函数。,
部分代码展示
Watcher 类的实现比较复杂,因为他的实例分为渲染 watcher(render-watcher)、计算属性 watcher(computed-watcher)、侦听器 watcher(normal-watcher)三种,这三个实例分别是在三个函数中构建的:mountComponent、initComputed 和 Vue.prototype.$watch。
normal-watcher:我们在组件钩子函数 watch 中定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数,这类 watch 的 expression 是我们写的回调函数的字符串形式。
computed-watcher:我们在组件钩子函数 computed 中定义的,都属于这种类型,每一个 computed 属性,最后都会生成一个对应的 watcher 对象,但是这类 watcher 有个特点:当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。这类 watch 的 expression 是计算属性中的属性名。
render-watcher:每一个组件都会有一个 render-watcher, 当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图。这类 watch 的 expression 是 function () {vm._update(vm._render(), hydrating);}。
除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:computed-render -> normal-watcher -> render-watcher。
这样安排是有原因的,这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。
这里我们只看其中一部分代码
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if(isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if(options) {
this.deep = !!options.deep; // 是否启用深度监听
this.user = !!options.user; // 主要用于错误处理,侦听器 watcher 的 user 为 true,其他基本为 false
this.lazy = !!options.lazy; // 惰性求职,当属于计算属性 watcher 时为 true
this.sync = !!options.sync; // 标记为同步计算,三大类型暂无
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
// 初始化各种属性和 option

// 观察者的回调
// 除了侦听器 watcher 外,其他大多为空函数
this.cb = cb;
this.id = ++uid$1; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// 解析 expOrFn,赋值给 this.getter
// 当是渲染 watcher 时,expOrFn 是 updateComponent,即重新渲染执行 render(_update)
// 当是计算 watcher 时,expOrFn 是计算属性的计算方法
// 当是侦听器 watcher 时,expOrFn 是 watch 属性的名字,this.cb 就是 watch 的 handler 属性

// 对于渲染 watcher 和计算 watcher 来说,expOrFn 的值是一个函数,可以直接设置 getter
// 对于侦听器 watcher 来说,expOrFn 是 watch 属性的名字,会使用 parsePath 函数解析路径,获取组件上该属性的值(运行 getter)

// 依赖(订阅目标)更新,执行 update,会进行取值操作,运行 watcher.getter,也就是 expOrFn 函数
if(typeof expOrFn === ‘function’) {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy ? undefined : this.get();
};
// 取值操作
Watcher.prototype.get = function get() {
//Dep.target 设置为该观察者
pushTarget(this);
var vm = this.vm;
// 取值
var value = this.getter.call(vm, vm);
// 移除该观察者
popTarget();
return value
};
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if(!this.newDepIds.has(id)) {
// 为观察者的 deps 添加依赖 dep
this.newDepIds.add(id);
this.newDeps.push(dep);
if(!this.depIds.has(id)) {
// 为 dep 添加该观察者
dep.addSub(this);
}
}
};
// 当一个依赖改变的时候,通知它 update
Watcher.prototype.update = function update() {
// 三种 watcher,只有计算属性 watcher 的 lazy 设置了 true,表示启用惰性求值
if(this.lazy) {
this.dirty = true;
} else if(this.sync) {
// 标记为同步计算的直接运行 run,三大类型暂无,所以基本会走下面的 queueWatcher
this.run();
} else {
// 将 watcher 推入观察者队列中,下一个 tick 时调用。
// 也就是数据变化不是立即就去更新的,而是异步批量去更新的
queueWatcher(this);
}
};

//update 执行后,运行回调 cb
Watcher.prototype.run = function run() {
if(this.active) {
var value = this.get();
if(
value !== this.value ||
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
// 运行 cb 函数,这个函数就是之前传入的 watch 中的 handler 回调函数
if(this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch(e) {
handleError(e, this.vm, (“callback for watcher \”” + (this.expression) + “\””));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

// 对于计算属性,当取值计算属性时,发现计算属性的 watcher 的 dirty 是 true
// 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};

// 收集依赖
Watcher.prototype.depend = function depend() {
var this$1 = this;

var i = this.deps.length;
while(i–) {
this$1.deps[i].depend();
}
};
总结
Observe 是对数据进行监听,Dep 是一个订阅器,每一个被监听的数据都有一个 Dep 实例,Dep 实例里面存放了 N 多个订阅者(观察者)对象 watcher。
被监听的数据进行取值操作时(getter),如果存在 Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器 subs 里面,留待后面数据变更时通知(会先通过观察者 id 判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器 dep 添加到自身 deps 中,方便其他地方使用。
被监听的数据进行赋值操作时(setter)时,就会触发 dep.notify(),循环该数据订阅器中的观察者,进行更新操作。

退出移动版