扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
观察者模式
在上党等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供成都网站设计、网站制作 网站设计制作定制网站,公司网站建设,企业网站建设,品牌网站设计,成都全网营销,成都外贸网站建设,上党网站建设费用合理。
首先话题下来,我们得反问一下自己,什么是观察者模式?
概念
观察者模式(Observer):通常又被称作为发布-订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
讲个故事
上面对于观察者模式的概念可能会比较官方化,所以我们讲个故事来理解它。
A:是共产党派往国民党密探,代号 001(发布者)
B:是共产党的通信人员,负责与 A 进行秘密交接(订阅者)
适用性
以下任一场景都可以使用观察者模式
vue 对于观察者模式的使用
vue 使用到观察者模式的地方有很多,这里我们主要谈谈对于数据初始化这一块的。
var vm = new Vue({ data () { return { a: 'hello vue' } } })
1、实现数据劫持
上图我们可以看到,vue 是利用的是 Object.defineProperty() 对数据进行劫持。 并在数据传递变更的时候封装了一层中转站,即我们看到的 Dep 和 Watcher 两个类。
这一小节,我们只看如何通过观察者模式对数据进行劫持。
1.1、递归遍历
我们都知道,vue 对于 data 里面的数据都做了劫持的,那只能对对象进行遍历从而完成每个属性的劫持,源码具体如下
walk (obj: Object) { const keys = Object.keys(obj) // 遍历将其变成 vue 的访问器属性 for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } }
1.2、发布/订阅
从上面对象的遍历我们看到了 defineReactive ,那么劫持最关键的点也在于这个函数,该函数里面封装了 getter 和 setter 函数,使用观察者模式,互相监听
// 设置为访问器属性,并在其 getter 和 setter 函数中,使用发布/订阅模式,互相监听。 export function defineReactive ( obj: Object, key: string, val: any ) { // 这里用到了观察者(发布/订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。 // 实例化一个主题对象,对象中有空的观察者列表 const dep = new Dep() // 获取属性描述符对象(更多的为了 computed 里面的自定义 get 和 set 进行的设计) const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } const getter = property && property.get const setter = property && property.set let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // 收集依赖,建立一对多的的关系,让多个观察者监听当前主题对象 get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() // 这里是对数组进行劫持 if (Array.isArray(value)) { dependArray(value) } } } return value }, // 劫持到数据变更,并发布消息进行通知 set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = observe(newVal) dep.notify() } }) }
1.3、返回 Observer 实例
上面我们看到了observe 函数,核心就是返回一个 Observer 实例
return new Observer(value)
2、消息封装,实现 "中转站"
首先我们要理解,为什么要做一层消息传递的封装?
我们在讲解观察者模式的时候有提到它的 适用性 。这里也同理,我们在劫持到数据变更的时候,并进行数据变更通知的时候,如果不做一个"中转站"的话,我们根本不知道到底谁订阅了消息,具体有多少对象订阅了消息。
这就好比上文中我提到的故事中的密探 A(发布者) 和共产党 B(订阅者)。密探 A 与 共产党 B 进行信息传递,两人都知道对方这么一个人的存在,但密探 A 不知道具体 B 是谁以及到底有多少共产党(订阅者)订阅着自己,可能很多共产党都订阅着密探 A 的信息,so 密探 A(发布者) 需要通过暗号 收集到所有订阅着其消息的共产党们(订阅者),这里对于订阅者的收集其实就是一层封装。然后密探 A 只需将消息发布出去,而订阅者们接受到通知,只管进行自己的 update 操作即可。
简单一点,即收集完订阅者们的密探 A 只管发布消息,共产党 B 以及更多的共产党只管订阅消息并进行对应的 update 操作,每个模块确保其独立性,实现高内聚低耦合这两大原则。
废话不多说,我们接下来直接开始讲 vue 是如何做的消息封装的
2.1、Dep
Dep,全名 Dependency,从名字我们也能大概看出 Dep 类是用来做依赖收集的,具体怎么收集呢。我们直接看源码
let uid = 0 export default class Dep { static target: ?Watcher; id: number; subs: Array; constructor () { // 用来给每个订阅者 Watcher 做唯一标识符,防止重复收集 this.id = uid++ // 定义subs数组,用来做依赖收集(收集所有的订阅者 Watcher) this.subs = [] } // 收集订阅者 addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null
代码很简短,但它做的事情却很重要
源码中,还抛出了两个方法用来操作 Dep.target ,具体如下
// 定义收集目标栈 const targetStack = [] export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) // 改变目标指向 Dep.target = _target } export function popTarget () { // 删除当前目标,重算指向 Dep.target = targetStack.pop() }
2.2、 Watcher
Watcher 意为观察者,它负责做的事情就是订阅 Dep ,当Dep 发出消息传递(notify)的时候,所以订阅着 Dep 的 Watchers 会进行自己的 update 操作。废话不多说,直接看源码就知道了。
export default class Watcher { vm: Component; expression: string; cb: Function; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) this.cb = cb // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 解析表达式 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} } } this.value = this.get() } get () { // 将目标收集到目标栈 pushTarget(this) const vm = this.vm let value = this.getter.call(vm, vm) // 删除目标 popTarget() return value } // 订阅 Dep,同时让 Dep 知道自己订阅着它 addDep (dep: Dep) { const 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 () { this.run() } run () { const value = this.get() const oldValue = this.value this.value = value this.cb.call(this.vm, value, oldValue) } }
上述代码中,我删除了一些与目前探讨无关的代码,如果需要进行详细研究的,可以自行查阅 vue2.5.3 版本的源码。
现在再去看 Dep 和 Watcher,我们需要知道两个点
两者看似相互依赖,实则却保证了其独立性,保证了模块的单一性。
更多的应用
vue 还有一些地方用到了"万能"的观察者模式,比如我们熟知的组件之间的事件传递,$on 以及 $emit 的设计。
$emit 负责发布消息,并对订阅者 $on 做统一消费,即执行 cbs 里面所有的事件。
Vue.prototype.$on = function (event: string | Array, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) } return vm } Vue.prototype.$emit = function (event: string): Component { const vm: Component = this let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) for (let i = 0, l = cbs.length; i < l; i++) { cbs[i].apply(vm, args) } } return vm }
总结
本文探讨了观察者模式的基本概念、适用场景,以及在 vue 源码中的具体应用。这一节将总结一下观察者模式的一些优缺点
OK,本文到这就差不多了,更多的源码设计思路细节将在同系列的其它文章中进行一一解读。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持创新互联。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流