從vue源碼中看觀察者模式

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

代碼很簡短,但它做的事情卻很重要

  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, 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. 一些意外的更新:因為一個觀察者它自己並不知道其它觀察者的存在,它可能對改變目標的最終代價一無所知。如果觀察者直接在目標上做操作的話,可能會引起一系列對觀察者以及依賴於這些觀察者的那些對象的更新,所以一般我們會把一些操作放在目標內部,防止出現上述的問題。

對前端的技術,架構技術感興趣的同學關注我的頭條號,並在後臺私信發送關鍵字:“前端”即可獲取免費的架構師學習資料

知識體系已整理好,歡迎免費領取。還有面試視頻分享可以免費獲取。關注我,可以獲得沒有 的架構經驗哦!!


分享到:


相關文章: