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,三大类型暂无,所以基本会走狼蚁网站SEO优化的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(),循环该数据订阅器中的观察者,进行更新操作。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持狼蚁SEO。