资讯专栏INFORMATION COLUMN

从发布-订阅模式到Vue响应系统

zacklee / 1137人阅读

摘要:典型实现例子售楼处的例子一步步实现发布订阅模式首先指定好谁充当发布者售楼处然后给发布者添加一个缓存列表,用语存放回调函数,以便通知订阅者售楼处花名册。最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者的回调函数。

概念

发布-订阅模式又称为观察者模式,它定义的是一种一对多的依赖关系,当一个状态发生改变的时候,所有以来这个状态的对象都会得到通知。

生活中的发布-订阅模式

上面事发布-订阅模式的一个比较正式的解释,可能这个解释不大好理解。所以我们通过实际生活中的例子来理解。

比如看中了一套房子,等到去了售楼处的说以后才被告知房子已经售罄了。但是售楼小姐告知,将来会有尾盘推出。具体什么时候推出,目前没人知道。

但是买家又不想频繁的跑,于是就把自己的电话号码登记在售楼处,在登记的花名册上有很多类似的买家。售楼小姐答应买家,新的房源一出来就一一通知买家。

所以上面就是一个发布订阅模式的简单例子。购房者(订阅者)订阅房源信息,售楼处(发布者)发布新房源消息给购房者(订阅者),购房者(订阅者)接收到消息后作出相应的反应。

适用性

发布订阅模式可以广泛的应用于异步编程中。

发布订阅模式可以取代对象之间的硬编码通知机制。

典型实现例子

</>复制代码

  1. 1、售楼处的例子

一步步实现发布订阅模式:

首先指定好谁充当发布者(售楼处)

然后给发布者添加一个缓存列表,用语存放回调函数,以便通知订阅者(售楼处花名册)。

最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者的回调函数。

</>复制代码

  1. let salesOffices = {} // 售楼处
  2. salesOffices.books = [] // 缓存列表,存放订阅者的回调函数。
  3. // 增加订阅者
  4. salesOffices.listen = function(fn) {
  5. this.books.push(fn) // 订阅的消息添加近缓存列表里面
  6. }
  7. salesOffices.trigger = function() {
  8. // 发布消息
  9. for (let i = 0, fn; (fn = salesOffices.books[i++]); ) {
  10. fn.apply(this, arguments) // arguments 是发布消息的时候带上的参数
  11. }
  12. }
  13. salesOffices.listen(function(price, squareMeter) {
  14. // 购买者a
  15. console.log(`价格是:${price}`)
  16. console.log(`面积大小:${squareMeter}`)
  17. })
  18. salesOffices.listen(function(price, squareMeter) {
  19. // 购买者b
  20. console.log(`价格是:${price}`)
  21. console.log(`面积大小:${squareMeter}`)
  22. })
  23. salesOffices.trigger(2000000, 88)
  24. salesOffices.trigger(3000000, 128)

上面实现了一个最简单的发布订阅模式。肯定还有很多问题的,例如订阅者只订阅了某一个消息,但是上面会把所有消息发给每一个订阅者。所以还得通过其他的方式让订阅者只订阅自己感兴趣的消息。

</>复制代码

  1. 2、vue 对发布订阅模式的使用

我们都知道 Vue 有个最显著的特性,便是侵入性不是很强的响应式系统。这个特性就是对发布订阅模式非常好的应用。我们接下来就来看看这个特性是怎么应用的。

vue 的数据初始化:

</>复制代码

  1. var v = new Vue({
  2. data() {
  3. return {
  4. a: "hello"
  5. }
  6. }
  7. })

这个初始化的代码的背后包含着发布订阅模式的思想,接下来看看官网的一个图

接下来就是网友的一个图:@xuqiang521

1. 数据劫持

从上图可以看到,数据劫持的核心方法就是使用Object.defineProperty把属性转化成getter/setter。(因为这个是 ES5 中的方法,所以这也是 Vue 不支持 ie8 及以下浏览器的原因之一。)在数据传递变更的时候,会进入到我们封装的DepWatcher中进行处理。

1.1 遍历劫持

数据不紧紧是基本类型的数据,也有可能是对象或者数组。基本类型的数据和对象的处理起来比较简单。

</>复制代码

  1. walk(obj) {
  2. const keys = Object.keys(obj)
  3. for (let i = 0; i < keys.length; ++i) {
  4. defineReactive(obj, keys[i], obj[keys[i]])
  5. }
  6. }

核心的劫持相关函数以及属性的订阅和发布

</>复制代码

  1. /**
  2. * Define a reactive property on an Object.
  3. */
  4. export function defineReactive(
  5. obj: Object,
  6. key: string,
  7. val: any,
  8. customSetter?: Function
  9. ) {
  10. /*在闭包中定义一个dep对象*/
  11. const dep = new Dep()
  12. const property = Object.getOwnPropertyDescriptor(obj, key)
  13. if (property && property.configurable === false) {
  14. return
  15. }
  16. /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
  17. // cater for pre-defined getter/setters
  18. const getter = property && property.get
  19. const setter = property && property.set
  20. /*对象的子对象递归进行observe并返回子节点的Observer对象*/
  21. let childOb = observe(val)
  22. Object.defineProperty(obj, key, {
  23. enumerable: true,
  24. configurable: true,
  25. get: function reactiveGetter() {
  26. /*如果原本对象拥有getter方法则执行*/
  27. const value = getter ? getter.call(obj) : val
  28. if (Dep.target) {
  29. /*进行依赖收集*/
  30. dep.depend()
  31. if (childOb) {
  32. /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
  33. childOb.dep.depend()
  34. }
  35. if (Array.isArray(value)) {
  36. /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
  37. dependArray(value)
  38. }
  39. }
  40. return value
  41. },
  42. set: function reactiveSetter(newVal) {
  43. /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
  44. const value = getter ? getter.call(obj) : val
  45. /* eslint-disable no-self-compare */
  46. if (newVal === value || (newVal !== newVal && value !== value)) {
  47. return
  48. }
  49. /* eslint-enable no-self-compare */
  50. if (process.env.NODE_ENV !== "production" && customSetter) {
  51. customSetter()
  52. }
  53. if (setter) {
  54. /*如果原本对象拥有setter方法则执行setter*/
  55. setter.call(obj, newVal)
  56. } else {
  57. val = newVal
  58. }
  59. /*新的值需要重新进行observe,保证数据响应式*/
  60. childOb = observe(newVal)
  61. /*dep对象通知所有的观察者*/
  62. dep.notify()
  63. }
  64. })
  65. }

最开始在初始化的时候是对 data 里面的数据就开始劫持监听了。初始化的时候就调用了observe方法

</>复制代码

  1. /**
  2. * Attempt to create an observer instance for a value,
  3. * returns the new observer if successfully observed,
  4. * or the existing observer if the value already has one.
  5. */
  6. /*
  7. 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。
  8. */
  9. export function observe(value: any, asRootData: ?boolean): Observer | void {
  10. /*判断是否是一个对象*/
  11. if (!isObject(value)) {
  12. return
  13. }
  14. let ob: Observer | void
  15. /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/
  16. if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
  17. ob = value.__ob__
  18. } else if (
  19. /*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/
  20. observerState.shouldConvert &&
  21. !isServerRendering() &&
  22. (Array.isArray(value) || isPlainObject(value)) &&
  23. Object.isExtensible(value) &&
  24. !value._isVue
  25. ) {
  26. ob = new Observer(value)
  27. }
  28. if (asRootData && ob) {
  29. /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
  30. ob.vmCount++
  31. }
  32. return ob
  33. }
1.2 返回值

上面的数据observe之后返回的就是一个 Observer 的实例

</>复制代码

  1. ob = new Observer(value)
  2. return ob
2."中转站"

在第一步数据劫持的时候,数据的获取或者修改的时候,都会做出对应的操作。这些操作的目的很简单,就是“通知”到“中转站”。这个“中转站”主要就是对数据的变更起通知作用以及存放依赖这些数据的“地方”。

这个"中转站"就是由"Dep"和“Watcher” 类构成的。每个被劫持的数据都会产生一个这样的“中转站”

2.1 Dep

Dep,全名 Dependency,从名字我们也能大概看出 Dep 类是用来做依赖收集的,但是也有通知对应的订阅者的作用 ,让它执行自己的操作,具体怎么收集呢?

</>复制代码

  1. /**
  2. * A dep is an observable that can have multiple
  3. * directives subscribing to it.
  4. */
  5. export default class Dep {
  6. static target: ?Watcher
  7. id: number
  8. subs: Array
  9. constructor() {
  10. this.id = uid++
  11. this.subs = []
  12. }
  13. /*添加一个观察者对象*/
  14. addSub(sub: Watcher) {
  15. this.subs.push(sub)
  16. }
  17. /*移除一个观察者对象*/
  18. removeSub(sub: Watcher) {
  19. remove(this.subs, sub)
  20. }
  21. /*依赖收集,当存在Dep.target的时候添加观察者对象*/
  22. depend() {
  23. if (Dep.target) {
  24. Dep.target.addDep(this)
  25. }
  26. }
  27. /*通知所有订阅者*/
  28. notify() {
  29. // stabilize the subscriber list first
  30. const subs = this.subs.slice()
  31. for (let i = 0, l = subs.length; i < l; i++) {
  32. subs[i].update()
  33. }
  34. }
  35. }
  36. // the current target watcher being evaluated.
  37. // this is globally unique because there could be only one
  38. // watcher being evaluated at any time.
  39. Dep.target = null
  40. /*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
  41. const targetStack = []
  42. export function pushTarget(_target: Watcher) {
  43. if (Dep.target) targetStack.push(Dep.target)
  44. // 改变目标指向
  45. Dep.target = _target
  46. }
  47. export function popTarget() {
  48. // 删除当前目标,重算指向
  49. Dep.target = targetStack.pop()
  50. }

代码很简短,但它做的事情却很重要

定义 subs 数组,用来收集订阅者 Watcher

当劫持到数据变更的时候,通知订阅者 Watcher 进行 update 操作

2.2 Watcher

Watcher 就是订阅者(观察者)。 主要的作用就是就是订阅 Dep(每个属性都会有一个 dep),当 Dep 发出消息传递(notify)的时候,所以订阅着 Dep 的 Watchers 会进行自己的 update 操作。

</>复制代码

  1. export default class Watcher {
  2. vm: Component
  3. expression: string
  4. cb: Function
  5. id: number
  6. deep: boolean
  7. user: boolean
  8. lazy: boolean
  9. sync: boolean
  10. dirty: boolean
  11. active: boolean
  12. deps: Array
  13. newDeps: Array
  14. depIds: ISet
  15. newDepIds: ISet
  16. getter: Function
  17. value: any
  18. constructor(
  19. vm: Component,
  20. expOrFn: string | Function,
  21. cb: Function,
  22. options?: Object
  23. ) {
  24. this.vm = vm
  25. /*_watchers存放订阅者实例*/
  26. vm._watchers.push(this)
  27. // options
  28. if (options) {
  29. this.deep = !!options.deep
  30. this.user = !!options.user
  31. this.lazy = !!options.lazy
  32. this.sync = !!options.sync
  33. } else {
  34. this.deep = this.user = this.lazy = this.sync = false
  35. }
  36. this.cb = cb
  37. this.id = ++uid // uid for batching
  38. this.active = true
  39. this.dirty = this.lazy // for lazy watchers
  40. this.deps = []
  41. this.newDeps = []
  42. this.depIds = new Set()
  43. this.newDepIds = new Set()
  44. this.expression =
  45. process.env.NODE_ENV !== "production" ? expOrFn.toString() : ""
  46. // parse expression for getter
  47. /*把表达式expOrFn解析成getter*/
  48. if (typeof expOrFn === "function") {
  49. this.getter = expOrFn
  50. } else {
  51. this.getter = parsePath(expOrFn)
  52. if (!this.getter) {
  53. this.getter = function() {}
  54. process.env.NODE_ENV !== "production" &&
  55. warn(
  56. `Failed watching path: "${expOrFn}" ` +
  57. "Watcher only accepts simple dot-delimited paths. " +
  58. "For full control, use a function instead.",
  59. vm
  60. )
  61. }
  62. }
  63. this.value = this.lazy ? undefined : this.get()
  64. }
  65. /**
  66. * Evaluate the getter, and re-collect dependencies.
  67. */
  68. /*获得getter的值并且重新进行依赖收集*/
  69. get() {
  70. /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
  71. pushTarget(this)
  72. let value
  73. const vm = this.vm
  74. /*
  75. 执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
  76. 在将Dep.target设置为自生观察者实例以后,执行getter操作。
  77. 譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
  78. 那么在执行getter的时候就会触发a跟c两个数据的getter函数,
  79. getter函数中即可判断Dep.target是否存在然后完成依赖收集,
  80. 将该观察者对象放入闭包中的Dep的subs中去。
  81. */
  82. if (this.user) {
  83. try {
  84. value = this.getter.call(vm, vm)
  85. } catch (e) {
  86. handleError(e, vm, `getter for watcher "${this.expression}"`)
  87. }
  88. } else {
  89. value = this.getter.call(vm, vm)
  90. }
  91. // "touch" every property so they are all tracked as
  92. // dependencies for deep watching
  93. /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
  94. if (this.deep) {
  95. /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
  96. traverse(value)
  97. }
  98. /*将观察者实例从target栈中取出并设置给Dep.target*/
  99. popTarget()
  100. this.cleanupDeps()
  101. return value
  102. }
  103. /**
  104. * Add a dependency to this directive.
  105. */
  106. /*添加一个依赖关系到Deps集合中*/
  107. addDep(dep: Dep) {
  108. const id = dep.id
  109. if (!this.newDepIds.has(id)) {
  110. this.newDepIds.add(id)
  111. this.newDeps.push(dep)
  112. if (!this.depIds.has(id)) {
  113. dep.addSub(this)
  114. }
  115. }
  116. }
  117. /**
  118. * Clean up for dependency collection.
  119. */
  120. /*清理依赖收集*/
  121. cleanupDeps() {
  122. /*移除所有观察者对象*/
  123. let i = this.deps.length
  124. while (i--) {
  125. const dep = this.deps[i]
  126. if (!this.newDepIds.has(dep.id)) {
  127. dep.removeSub(this)
  128. }
  129. }
  130. let tmp = this.depIds
  131. this.depIds = this.newDepIds
  132. this.newDepIds = tmp
  133. this.newDepIds.clear()
  134. tmp = this.deps
  135. this.deps = this.newDeps
  136. this.newDeps = tmp
  137. this.newDeps.length = 0
  138. }
  139. /**
  140. * Subscriber interface.
  141. * Will be called when a dependency changes.
  142. */
  143. /*
  144. 调度者接口,当依赖发生改变的时候进行回调。
  145. */
  146. update() {
  147. /* istanbul ignore else */
  148. if (this.lazy) {
  149. this.dirty = true
  150. } else if (this.sync) {
  151. /*同步则执行run直接渲染视图*/
  152. this.run()
  153. } else {
  154. /*异步推送到观察者队列中,由调度者调用。*/
  155. queueWatcher(this)
  156. }
  157. }
  158. /**
  159. * Scheduler job interface.
  160. * Will be called by the scheduler.
  161. */
  162. /*
  163. 调度者工作接口,将被调度者回调。
  164. */
  165. run() {
  166. if (this.active) {
  167. const value = this.get()
  168. if (
  169. value !== this.value ||
  170. // Deep watchers and watchers on Object/Arrays should fire even
  171. // when the value is the same, because the value may
  172. // have mutated.
  173. /*
  174. 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
  175. */
  176. isObject(value) ||
  177. this.deep
  178. ) {
  179. // set new value
  180. const oldValue = this.value
  181. /*设置新的值*/
  182. this.value = value
  183. /*触发回调渲染视图*/
  184. if (this.user) {
  185. try {
  186. this.cb.call(this.vm, value, oldValue)
  187. } catch (e) {
  188. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  189. }
  190. } else {
  191. this.cb.call(this.vm, value, oldValue)
  192. }
  193. }
  194. }
  195. }
  196. /**
  197. * Evaluate the value of the watcher.
  198. * This only gets called for lazy watchers.
  199. */
  200. /*获取观察者的值*/
  201. evaluate() {
  202. this.value = this.get()
  203. this.dirty = false
  204. }
  205. /**
  206. * Depend on all deps collected by this watcher.
  207. */
  208. /*收集该watcher的所有deps依赖*/
  209. depend() {
  210. let i = this.deps.length
  211. while (i--) {
  212. this.deps[i].depend()
  213. }
  214. }
  215. /**
  216. * Remove self from all dependencies" subscriber list.
  217. */
  218. /*将自身从所有依赖收集订阅列表删除*/
  219. teardown() {
  220. if (this.active) {
  221. // remove self from vm"s watcher list
  222. // this is a somewhat expensive operation so we skip it
  223. // if the vm is being destroyed.
  224. /*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源,所以如果vm实例正在被销毁则跳过该步骤。*/
  225. if (!this.vm._isBeingDestroyed) {
  226. remove(this.vm._watchers, this)
  227. }
  228. let i = this.deps.length
  229. while (i--) {
  230. this.deps[i].removeSub(this)
  231. }
  232. this.active = false
  233. }
  234. }
  235. }

通过上面对 vue 的响应系统的 学习,就可以了解到这个发布订阅模式就是这样的:

Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。

Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。

</>复制代码

  1. 3、vue 中更多的应用

vue 中还有个组件之间的时间传递也是用到了发布订阅模式。
$emit 负责发布消息, $on 负责消费消息(执行 cbs 里面的事件)

</>复制代码

  1. Vue.prototype.$on = function(
  2. event: string | Array,
  3. fn: Function
  4. ): Component {
  5. const vm: Component = this
  6. if (Array.isArray(event)) {
  7. for (let i = 0, l = event.length; i < l; i++) {
  8. this.$on(event[i], fn)
  9. }
  10. } else {
  11. ;(vm._events[event] || (vm._events[event] = [])).push(fn)
  12. }
  13. return vm
  14. }
  15. Vue.prototype.$emit = function(event: string): Component {
  16. const vm: Component = this
  17. let cbs = vm._events[event]
  18. if (cbs) {
  19. cbs = cbs.length > 1 ? toArray(cbs) : cbs
  20. const args = toArray(arguments, 1)
  21. for (let i = 0, l = cbs.length; i < l; i++) {
  22. cbs[i].apply(vm, args)
  23. }
  24. }
  25. return vm
  26. }
总结

本文通过对 vue 相关源码的学习,了解了发布订阅模式(观察者模式)的概念和应用。还了解了该模式的 一些优缺点:

时间上的解耦,对象之间的解耦。

创建订阅者本身会消耗一定的时间和内存,并且订阅者订阅一个消息后,该消息一直不发生的话,那么该订阅者 会一直存在在内存中

感谢

从源码角度再看数据绑定

《javascript 设计模式与开发实践》

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107602.html

相关文章

  • 源码解析 —— Vue响应式数据流

    摘要:下面我们会向大家解释清楚为什么这个这么重要,以及它和的响应式数据流有什么关系。源码前面铺垫这么多就是希望大家能理解接下来要讲的响应式数据流。总结讲到这里大家应该都能够明白的响应式数据流是如何实现的。 Vue、React介绍 目前前端社区比较推崇的框架有Vue 和 React,公司内部许多端都自发的将原有的老技术方案(widget + jQuery)迁移到 Vue / React上了。我...

    LuDongWei 评论0 收藏0
  • vue.js响应式原理解析与实现

    摘要:今天,就我们就来一步步解析响应式的原理,并且来实现一个简单的。当然,这个也只是一个简单的,来说明响应式的原理,真实的源码会更加复杂,因为加了很多其他逻辑。接下来我可能会将其与联系起来,实现和语法。 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染。之后,再接触了vue.js,当时也一度很好奇vue.js是如何监...

    Shihira 评论0 收藏0
  • 订阅发布模式和观察者模式的区别

    摘要:或许以前认为订阅发布模式是观察者模式的一种别称,但是发展至今,概念已经有了不少区别。参考文章订阅发布模式和观察者模式真的不一样 首选我们需要先了解两者的定义和实现的方式,才能更好的区分两者的不同点。 或许以前认为订阅发布模式是观察者模式的一种别称,但是发展至今,概念已经有了不少区别。 订阅发布模式 在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特...

    ysl_unh 评论0 收藏0

发表评论

0条评论

zacklee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<