首頁 > 軟體

VUE響應式原理的實現詳解

2022-03-28 16:00:35

前言

相信vue學習者都會發現,vue使用起來上手非常方便,例如雙向繫結機制,讓我們實現檢視、資料層的快速同步,但雙向繫結機制實現的核心資料響應的原理是怎麼樣的呢,接下來讓我們開始介紹:

function observer(value) {

	//給所有傳入進來的data 設定一個__ob__物件 一旦value有__ob__ 說明該value已經做了響應式處理
	Object.defineProperty(value, '__ob__', {
		value: this, //當前範例 也就是new observer
		enumerable: false, //不可列舉  即不可for in
		writable: true, // 可用賦值運運算元改寫__ob__
		configurable: true //可改寫可刪除
	})

	//這裡是判斷是物件 陣列的話需要改造陣列原型上的方法
	if (Object.prototype.toString.call(value) === "[object Array]") {
		//陣列的話需要改造陣列原型上的方法 下面會講解arrayMethods
		value.__proto__ = arrayMethods;
		//對陣列進行響應式處理
		observeArray(value);
	} else {
		//如果是物件 遍歷物件屬性進行響應式處理
		iterate(value)
	}


}

// 遍歷物件屬性進行響應式處理
function iterate(data) {
	const keys = Object.keys(data);
	keys.forEach((key) => {
		defineReactive(data, key, data[key])
	})
}

//響應式處理 這裡是核心
function defineReactive(data, key, value){
	//遞迴物件 這裡是因為物件裡面仍可能巢狀物件
	observe(value)

	//寫道這裡 Object.defineProperty 我們主角出場了
 	// 這裡實現了讀寫都能捕捉到,響應式的底層原理
	Object.defineProperty(data, key, {
		get() { 
			console.log('我被成功存取啦!');
			return value
		},
		set(newValue) { 
			if (newValue === value) return
			console.log("我被變更啦")
			value = newValue
		}
	})
}

function  observeArray(data) { 
	data.forEach(item => { 
		observe(item)
	})
}

function observe(value) {
    // 如果傳進來的是物件或者陣列,則進行響應式處理
    if (Object.prototype.toString.call(value) === '[object Object]' || Object.prototype.toString.call(value) === "[object Array]") {
        return new Observer(value)
    }
}

上面程式碼簡單的實現了vue2.0中響應式的原理,相信註釋也非常的清晰,總結一下三個主要的方法:

名稱作用
observer觀察者物件,對陣列、物件進行響應式處理
defineReactive攔截物件中的key中的set、get方法
observe響應式處理的入口

從上面的大致實現方法中,我們不難看出幾個問題:

1.使用defineProperty,我們無法實現物件刪除的監聽、以及新增物件屬性的時候,set方法沒有被呼叫,下圖是實驗結果

2.陣列修改只能通過改寫的方法,無法通過arr[index] = xxx 進行修改,也無法通過length屬性進行修改,下圖是輸出結果:

解決方案

針對上面的問題,vue提出了自己的解決方案:

$set(obj, key, value),原理相信大家不難猜出,通過hack的方式,物件的處理方法是重新為物件賦值,而陣列是通過splice來轉換為響應式

function set (target, key, val) {
  //isValidArrayIndex 用來檢測是否合法索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
  //... 
  defineReactive$$1(ob.value, key, val);
  ob.dep.notify();
  return val
}

陣列的特殊處理

相信大家還發現,陣列做了特殊處理,上面的程式碼也寫到沒有使用遍歷使用defineProperty去監聽資料,修改陣列原型上的部分方法,來實現修改陣列觸發響應式,也就是上面程式碼的arrayMethods,我們接著來看這個的具體實現思路:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]


methodsToPatch.forEach(method =>{
	// 快取原來的方法
	def(arrayMethods, method)
})

function def(obj, key) {
	Object.defineProperties(obj, key, {
		enumerable: true,
		configurable: true,
		value: function (...args) {

			//獲取陣列原生方法
			let original = arrayProto[key];

			//改變this指向 
			const result = original.apply(this, args)

			console.log('我被更新了');
			
			//result就是上文的arrayMethods
			return result;
		}
	})
}

這裡大概分為三個思路

1.獲取陣列原型上的方法

2.使用defineProperties對陣列原型上的方法進行劫持

3.把需要被改造的 Array 原型方法指向改造後原型。

這樣做的好處

沒有直接修改 Array.prototype,而是直接把 arrayMenthods 賦值給 value 的 proto 。因為這樣不會汙染全域性的Array, arrayMenthods 只對 data中的Array 生效。

題外話

關於陣列為什麼不使用defineProperties進行劫持,網上大部分說法都是覺得開銷太大,因為在我們業務場景中一般的物件不會有太多屬性,但列表中幾千、上萬條資料確是很正常,這一點也可以講通。

總結

感謝你的閱讀,在vue3.0中使用proxy進行資料劫持後,都說解決了2.0存在的問題以及提升了效率,後面我也會完善3.0響應式的實現原理。

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!   


IT145.com E-mail:sddin#qq.com