浅析从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 相关文章推荐
使用Modello编写JavaScript类
Dec 22 Javascript
JavaScript与DropDownList 区别分析
Jan 01 Javascript
2则自己编写的jQuery特效分享
Feb 26 Javascript
JavaScript中函数(Function)的apply与call理解
Jul 08 Javascript
jQuery实现的简洁下拉菜单导航效果代码
Aug 26 Javascript
javascript中checkbox使用方法实例演示
Nov 19 Javascript
动态创建按钮的JavaScript代码
Jan 29 Javascript
js验证框架之RealyEasy验证详解
Jun 08 Javascript
JavaScript实现横线提示输入验证码随输入验证码输入消失的方法
Sep 24 Javascript
推荐VSCode 上特别好用的 Vue 插件之vetur
Sep 14 Javascript
Vue+iview+webpack ie浏览器兼容简单处理
Sep 20 Javascript
解决layer 关闭当前弹窗 关闭遮罩层 input值获取不到的问题
Sep 25 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如何将log信息写入服务器中的log文件
2015/07/29 PHP
php封装的表单验证类完整实例
2016/10/19 PHP
源码分析 Laravel 重复执行同一个队列任务的原因
2017/12/25 PHP
Javascript document.referrer判断访客来源网址
2020/05/15 Javascript
一个简单的瀑布流效果(主体形式自写)
2013/05/27 Javascript
Jquery响应回车键直接提交表单操作代码
2014/07/25 Javascript
JavaScript设计模式之工厂方法模式介绍
2014/12/28 Javascript
举例详解JavaScript中Promise的使用
2015/06/24 Javascript
jquery.validate 自定义验证方法及validate相关参数
2016/01/18 Javascript
深入浅析JS的数组遍历方法(推荐)
2016/06/15 Javascript
AngularJS基础 ng-dblclick 指令用法
2016/08/01 Javascript
Bootstrap中的fileinput 多图片上传及编辑功能
2016/09/05 Javascript
AngularJS ng-repeat指令中使用track by子语句解决重复数据遍历错误问题
2017/01/21 Javascript
Angular中ng-bind和ng-model的区别实例详解
2017/04/10 Javascript
微信小程序 引入es6 promise
2017/04/12 Javascript
Angular项目中$scope.$apply()方法的使用详解
2017/07/26 Javascript
[13:21]DOTA2国际邀请赛采访专栏:RSnake战队国士无双,Fnatic.Fly
2013/08/06 DOTA
[47:04]EG vs RNG 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/18 DOTA
解决Python2.7读写文件中的中文乱码问题
2018/04/12 Python
python 根据网易云歌曲的ID 直接下载歌曲的实例
2019/08/24 Python
django正续或者倒序查库实例
2020/05/19 Python
keras 获取某层输出 获取复用层的多次输出实例
2020/05/23 Python
Python如何创建装饰器时保留函数元信息
2020/08/07 Python
canvas之万花筒效果的简单实现(推荐)
2016/08/16 HTML / CSS
软件测试面试题
2014/01/05 面试题
大学同学聚会邀请函
2014/01/29 职场文书
前处理班长职位说明书
2014/03/01 职场文书
教师对学生的评语
2014/04/28 职场文书
师范生见习报告范文
2014/11/03 职场文书
红色影片观后感
2015/06/18 职场文书
高中班主任培训心得体会
2016/01/07 职场文书
公司晚会主持词
2019/04/17 职场文书
《学会生存》读后感3篇
2019/12/09 职场文书
mysql外连接与内连接查询的不同之处
2021/06/03 MySQL
分析mysql中一条SQL查询语句是如何执行的
2021/06/21 MySQL
GO语言异常处理分析 err接口及defer延迟
2022/04/14 Golang