浅析从vue源码看观察者模式


Posted in Javascript onJanuary 29, 2018

观察者模式

首先话题下来,我们得反问一下自己,什么是观察者模式?

概念

观察者模式(Observer):通常又被称作为发布-订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。

讲个故事

上面对于观察者模式的概念可能会比较官方化,所以我们讲个故事来理解它。

A:是共产党派往国民党密探,代号 001(发布者)
B:是共产党的通信人员,负责与 A 进行秘密交接(订阅者)

  1. A 日常工作就是在明面采集国民党的一些情报
  2. B 则负责暗中观察着 A
  3. 一旦 A 传递出一些有关国民党的消息(更多时候需要对消息进行封装传递,后面根据源码具体分析)
  4. B 会立马订阅到该消息,然后做一些相对应的变更,比如说通知共产党们做一些事情应对国民党的一些动作。

适用性

以下任一场景都可以使用观察者模式

  1. 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中可以让它们可以各自独立的改变和复用
  2. 当一个对象的改变的时候,需要同时改变其它对象,但是却不知道具体多少对象有待改变
  3. 当一个对象必须通知其它对象,但是却不知道具体对象到底是谁。换句话说,你不希望这些对象是紧密耦合的。

vue 对于观察者模式的使用

vue 使用到观察者模式的地方有很多,这里我们主要谈谈对于数据初始化这一块的。

var vm = new Vue({
 data () {
  return {
   a: 'hello vue'
  }
 }
})

浅析从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<Watcher>;

 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

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

  1. 定义subs数组,用来收集订阅者Watcher
  2. 当劫持到数据变更的时候,通知订阅者Watcher进行update操作

源码中,还抛出了两个方法用来操作 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,我们需要知道两个点

  1. Dep 负责收集所有的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要通过 target 指向的计算去收集订阅其消息的 Watcher 即可,然后只需要做好消息发布 notify 即可。
  2. Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。

两者看似相互依赖,实则却保证了其独立性,保证了模块的单一性。

更多的应用

vue 还有一些地方用到了"万能"的观察者模式,比如我们熟知的组件之间的事件传递,$on 以及 $emit 的设计。

$emit 负责发布消息,并对订阅者 $on 做统一消费,即执行 cbs 里面所有的事件。

Vue.prototype.$on = function (event: string | Array<string>, 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 源码中的具体应用。这一节将总结一下观察者模式的一些优缺点

  1. 目标和观察者间的抽象耦合:一个目标只知道他有一系列的观察者(目标进行依赖收集),却不知道其中任意一个观察者属于哪一个具体的类,这样目标与观察者之间的耦合是抽象的和最小的。
  2. 支持广播通信:观察者里面的通信,不像其它通常的一些请求需要指定它的接受者。通知将会自动广播给所有已订阅该目标对象的相关对象,即上文中的 dep.notify() 。当然,目标对象并不关心到底有多少对象对自己感兴趣,它唯一的职责就是通知它的各位观察者,处理还是忽略一个通知取决于观察者本身。
  3. 一些意外的更新:因为一个观察者它自己并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。如果观察者直接在目标上做操作的话,可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新,所以一般我们会把一些操作放在目标内部,防止出现上述的问题。

OK,本文到这就差不多了,更多的源码设计思路细节将在同系列的其它文章中进行一一解读。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery图片滚动图片的效果(另类实现)
Jun 02 Javascript
js控制页面的全屏展示和退出全屏显示的方法
Mar 10 Javascript
纯javascript制作日历控件
Jul 17 Javascript
20分钟轻松创建自己的Bootstrap站点
May 12 Javascript
jQuery中ajax错误调试分析
Dec 01 Javascript
js获取ip和地区
Mar 10 Javascript
各种选择框jQuery的选中方法(实例讲解)
Jun 27 jQuery
jQuery接受后台传递的List的实例详解
Aug 02 jQuery
gulp安装以及打包合并的方法教程
Nov 19 Javascript
vue-cli下的vuex的简单Demo图解(实现加1减1操作)
Feb 26 Javascript
利用Angular2的Observables实现交互控制的方法
Dec 27 Javascript
angula中使用iframe点击后不执行变更检测的问题
May 10 Javascript
实例学习JavaScript读取和写入cookie
Jan 29 #Javascript
微信小程序之多文件下载的简单封装示例
Jan 29 #Javascript
JS中Object对象的原型概念基础
Jan 29 #Javascript
vue.js实现只弹一次弹框
Jan 29 #Javascript
javascript trie前缀树的示例
Jan 29 #Javascript
Vue官网todoMVC示例代码
Jan 29 #Javascript
jquery动态添加以及遍历option并获取特定样式名称的option方法
Jan 29 #jQuery
You might like
可定制的PHP缩略图生成程式(需要GD库支持)
2007/03/06 PHP
phpExcel中文帮助手册之常用功能指南
2014/08/18 PHP
PHPStrom中实用的功能和快捷键大全
2015/09/23 PHP
php检查页面是否被百度收录
2015/10/28 PHP
JQuery 动画卷页 返回顶部 动画特效(兼容Chrome)
2010/02/15 Javascript
jQuery创建自己的插件(自定义插件)的方法
2010/06/10 Javascript
原生js和jquery实现图片轮播特效
2015/04/23 Javascript
JavaScript中判断两个字符串是否相等的方法
2015/07/07 Javascript
在WordPress中加入Google搜索功能的简单步骤讲解
2016/01/04 Javascript
JS实现的几个常用算法
2016/11/12 Javascript
学习使用bootstrap的modal和carousel
2016/12/09 Javascript
开源免费天气预报接口API及全国所有地区代码(国家气象局提供)
2016/12/26 Javascript
vue中子组件传递数据给父组件的讲解
2019/01/27 Javascript
nodejs nedb 封装库与使用方法示例
2020/02/06 NodeJs
JS实现简易贪吃蛇游戏
2020/08/24 Javascript
vue 实现click同时传入事件对象和自定义参数
2021/01/29 Vue.js
[31:55]完美世界DOTA2联赛循环赛 IO vs GXR BO2第一场 11.04
2020/11/05 DOTA
Python通过websocket与js客户端通信示例分析
2014/06/25 Python
python实现将pvr格式转换成pvr.ccz的方法
2015/04/28 Python
Python实现打砖块小游戏代码实例
2019/05/18 Python
python采集百度搜索结果带有特定URL的链接代码实例
2019/08/30 Python
使用tensorflow实现VGG网络,训练mnist数据集方式
2020/05/26 Python
python中upper是做什么用的
2020/07/20 Python
美国休闲服装品牌:J.Crew Factory
2017/03/04 全球购物
三星新西兰官网:Samsung新西兰
2019/03/05 全球购物
New Balance法国官方网站:购买鞋子和服装
2019/09/01 全球购物
介绍一下Cookie和Session及他们之间的区别
2012/11/20 面试题
私有程序集与共享程序集有什么区别
2013/04/05 面试题
乡镇干部十八大感言
2014/02/17 职场文书
物业保安岗位职责
2014/07/02 职场文书
意外伤害赔偿协议书范文
2014/09/23 职场文书
乡镇党的群众路线教育实践活动剖析材料
2014/10/09 职场文书
2014年连锁店圣诞节活动方案
2014/12/09 职场文书
西游降魔篇观后感
2015/06/15 职场文书
小学英语课教学反思
2016/02/15 职场文书
php去除deprecated的实例方法
2021/11/17 PHP