Vue源碼學習 -- 響應式原理之觀察者模式

1. 觀察者模式

  • 概念

觀察者模式又被稱爲 發佈-訂閱 模式,這種模式定義了對象間的一種一對多的依賴關係。當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並完成自動更新。

  • 優點
  1. 觀察者與被觀察者是抽象耦合的
  2. 建立了一套觸發機制
  • 缺點
  1. 被觀察者有很多的觀察者時,通知更新這一過程會花費很多的時間
  2. 觀察者和被觀察者之間存在循環依賴的話,可能導致系統奔潰
  • 現實中的例子
    在這裏插入圖片描述

從上圖可以看到,一個 subject 可以被多個 observer 訂閱/觀察。當 subject 狀態發生變化時,就會通知訂閱者進行更新操作。在這樣的一個模式中,訂閱者的數量以及具體訂閱者都是不確定的(訂閱者列表是動態變化的),但仍能保證整個機制的正常運轉。

2. Vue 的響應式原理

Vue.js 實現響應式的核心是利用了 ES5 中的 Object.defineProperty 方法(這也是爲什麼 Vue.js 不能兼容 IE8 及以下版本瀏覽器的原因)。

Vue 會爲每個組件實例都創建一個 watcher ,它會在組件渲染的過程中把“接觸”過的數據屬性記錄爲依賴(依賴收集)。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件重新渲染(派發更新)。

代碼實現上: 創建 Vue 實例時,會傳入一個 function / object 作爲 data 屬性。Vue 會遍歷 data 屬性所對應的對象的屬性,並藉助 Object.defineProperty 方法,進行數據劫持,將每個數據屬性都轉化爲響應式對象(擁有settergetter)。

  1. 在 getter 方法中,進行依賴收集
  2. 在 setter 方法中,進行派發更新

3. 簡要分析源碼中的觀察者模式部分

說明: 下文分析的是 vue.js 2 的部分源碼。

  1. 數據劫持,將數據屬性轉化爲響應式對象
    核心爲 defineReactive 方法
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && 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
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 派發更新
      dep.notify()
    }
  })
}
  1. 依賴收集
    依賴收集從 getter 中的 dep.depend() 方法說起,下面是 Dep 類中的 depend 方法的實現:
depend () {
    if (Dep.target) {
     // 訂閱
      Dep.target.addDep(this)
    }
  }

其中調用了 Watcher 實例中的 addDep 方法:

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)) {
      	// 添加當前 watcher 到依賴列表 subs 中
        dep.addSub(this)
      }
    }
  }
  1. 派發更新
    派發更新從 setter 中的 dep.notify() 說起,下面是 Dep 類中的 notify 方法的實現:
notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍歷依賴列表,通知訂閱者更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

其中調用了 watcher 實例中的 update 方法:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 將更新 watcher 放入更新隊列中,在下一個 tick 調用
      queueWatcher(this)
    }
  }

4. 簡易實現

源碼中考慮的情況比較多,看起來比較複雜。爲了便於理解觀察者模式使用的核心思想,可以暫時忽略分支邏輯,只查看主要的邏輯。下面的代碼,就是忽略各種分支情況後,最最簡單的一個實現:

// 觀察者模式
class Watcher {
	constructor(vm, expr, callback) {
		this.vm = vm;
		this.expr = expr;
		this.callback = callback;
		this.oldValue = this.get();
	}
	get() {
		Dep.target = this;
		let value = CompileUtil.getValue(this.vm, this.expr);
		Dep.target = null;
		return value;
	}
	update() {
		let newValue = CompileUtil.getValue(this.vm, this.expr)
		if (this.oldValue !== newValue) {
			this.callback(newValue)
		}
	}
}
// 發佈/訂閱
class Dep {
	constructor() {
		// 存儲所有的觀察者
		this.subs = [];
	}
	// 訂閱
	addSub(watcher) {
		this.subs.push(watcher);
	}
	// 發佈
	notify() {
		this.subs.forEach((watcher) => {
			watcher.update()
		})
	}
}

// 實現數據劫持
// 觀察類 將傳入的數據的定義都改爲 defineProperty 的方式
class Observer {
	constructor(data) {
		this.observe(data);
	}
	observe(data) {
		if (data && typeof data === 'object') {
			for (let key in data) {
				this.defineReactive(data, key, data[key]);
				this.observe(data[key]);
			}
		}
	}
	defineReactive(obj, key, value) {
		this.observe(value);
		let dep = new Dep();
		Object.defineProperty(obj, key, {
			get() {
				// 添加觀察者
				Dep.target && dep.addSub(Dep.target);
				return value;
			},
			set: (newValue) => {
				if (newValue !== value) {
					// 將新值轉爲 get set 形式後再進行賦值
					this.observe(newValue);
					value = newValue;
					// 通知觀察者
					dep.notify();
				}
			}
		})
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章