前言 在上篇分析 Vue 的观察者模式(上) 中,简单实现了“手动”版观察者模式,而在本篇中我们继续沿着该思路往下探索,看如何实现“自动”版的观察者。
完善的观察者 紧接着上文的思路,因为我们只是实现了 Dep 依赖(管家),却还没有对平台的观察处理方式优化,因此我们需要将此步骤给完善起来。
首先,要实现发布者更新内容后自动触发订阅事件,那么我们需要的是对发布者内容改变的监听,ES5 中恰好有这么一种方法能满足我们的需求,那就是 Object.defineProperty(...)
。其语法很简单,我们此处仅仅需要拦截 get 和 set 方法,大致写法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const data = { name: 'test' }; Object .defineProperty(data, name, { get () { return data.name; }, set (newVal) { } })
既然有了能够观测发布者的方法,那么对于对象变更后的监听就能自动化处理了,那么先前手动触发 dep.depend()
和 dep.notify()
函数的操作就能整合入 Object.defineProperty
内了。
延续先前的逻辑,dep.notify()
方法执行都得在对象值变动之后,因此我们可以将此方法放入 set 函数内;而 dep.depend()
方法主要是将订阅者事件存储入自身的事件列表中,因此该方法执行一次就行,之后每次更新都触发 notify
来依次执行注册的函数即可。因此,在考虑只是传入简单对象的情况时,这阶段的代码思路大致是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Dep { constructor () { this .subscriberList = []; } depend() { } notify() { this .subscriberList.forEach(sub => { }); } } function observer (data = {} ) { Object .keys(data).forEach(key => { let val = data[key]; const dep = new Dep(); Object .defineProperty(data, key, { get () { if (target) { dep.depend(); } return val; }, set (newVal) { if (newVal === val) { return ; } val = newVal; dep.notify(); } }) }); }
那么现在问题又有了。目前代码实现至此我们仅仅只是将发布者与其 Dep 依赖进行了关联,那么观察者又该如何将其获订阅者的事件与 Dep 存储的事件列表进行关联呢?从订阅者角度分析一下:
订阅者决定好要订阅的内容(订阅者的函数);
订阅者在对应的观察者平台上注册事件,绑定发布者信息;
观察者先联系到 Dep 管家,将订阅事件存放至管家处;
待管家拿到了发布者新情报,则响应所有存储好的订阅者的订阅事件了。
思路逐渐清晰,那么这么个流程我们可以简化为如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 target = null ; class Dep { constructor () { this .sub = []; } depend() { if (target && !this .sub.includes(target)) { this .sub.push(target); } } notify() { this .sub.forEach(cb => cb()); } } function watcher (callback ) { target = callback; target = null ; } watcher(() => { })
这样我们就讲观察者的订阅方法和 Dep 依赖给绑定上了。考虑到 watcher 的通用性,这里写了一个加强版的 demo,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 let target = null ;class Dep { constructor () { this .subscriberList = []; } addSub(watcher) { this .subscriberList.push(watcher) } depend() { if (target) { target.addDep(this ); } } notify() { this .subscriberList.forEach(sub => { sub.update(); }); } } class Watcher { constructor (data = {}, key = '' , cb = () => {}) { this .cb = cb; this ._data = data; this .key = key; target = this ; this .value = data[key]; target = null ; } addDep(dep) { dep.addSub(this ); } update() { const newVal = this ._data[this .key]; this .value = newVal; this .cb(newVal); } } function observer (data = {} ) { Object .keys(data).forEach(key => { let val = data[key]; const dep = new Dep(); Object .defineProperty(data, key, { get () { if (target) { dep.depend(); } return val; }, set (newVal) { if (newVal === val) { return ; } val = newVal; dep.notify(); } }) }); } const publisher = { bookName: 'book' , bookContent: 'hello world' } observer(publisher); new Watcher(publisher, 'bookName' , name => { console .log(`new book name is ${name} ` ); }); new Watcher(publisher, 'bookContent' , content => { console .log(`new book content is ${content} ` ); }); publisher.bookName = 'new book' ; publisher.bookContent = 'new content' ;
Vue 观察者的实现 在看源码之前,首先让我们看看 Vue 的实现原理图:
先仅考虑 data 部分的观察者模式,我们可以看到大致步骤为:
为发布者 data 做数据监听处理;
为劫持方法添加 Dep 依赖收集,get
存储 target 目标,set 设置 target 目标;
依赖收集触发后,触发 Watch 观察者,通知订阅者改变。
因为流程图中只是详细展示了响应式部分的变动,为了便于大家更清楚的理解这部分的流程,所以这里先解释一下 Vue 中完整的流程:
页面开始渲染,对 data 进行依赖收集;
依赖收集完毕,开始监测订阅者的方法,并存储原值方便变更时的对比;
因为依赖收集完毕,要存储原值则触发了 get 事件,此时添加 dep 依赖,将对应生成的 watcher 存储入依赖列表内
页面渲染完成,用户触发页面某些点击事件(比如按钮);
点击事件绑定了某个 methods 方法,该 methods 方法使得 data 部分内容更新了;
因为已经对 data 进行了依赖收集,触发了对应的 set 方法;
set 方法对比新旧值发现值更新了,此时重新收集依赖新的值(特别是对象的情况),然后通知它的私有管家 Dep 进行更新;
Dep 依赖开始遍历其所存储的数组,为该发布者的订阅者们发送消息,触发订阅事件。
各订阅者事件触发 update 更新,此时便流转到 render 渲染,之后页面就更新啦!
为了方便大家理解源码,这里手动实现了一个简单的 observer 模式,基本是按着源码的思路一点点撸出来的,注释很全,方便大家理解!
源码地址 所有的 demo 都写入仓库了,仓库地址为:传送门