天天看點

從vue源碼看觀察者模式

觀察者模式

首先話題下來,我們得反問一下自己,什麼是觀察者模式?

概念

觀察者模式(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'
    }
  }
})
           

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,本文到這就差不多了,更多的源碼設計思路細節将在同系列的其它文章中進行一一解讀。

本文作者:qiangdada

本文釋出時間:2018/01/28

本文來自雲栖社群合作夥伴

開源中國

,了解相關資訊可以關注oschina.net網站。

繼續閱讀