前言
為什么前端框架Vue能夠做到響應式?當依賴數據發生變化時,會對頁面進行自動更新,其原理還是在于對響應式數據的獲取和設置進行了監聽,一旦監聽到數據發生變化,依賴該數據的函數就會重新執行,達到更新的效果。那么我們如果想監聽對象中的屬性被設置和獲取的過程,可以怎么做呢?
1.Object.defineProperty
在ES6之前,如果想監聽對象屬性的獲取和設置,可以借助Object.defineProperty方法的存取屬性描述符來實現,具體怎么用呢?我們來看一下。
const obj = {
name: 'curry',
age: 30
}
// 1.拿到obj所有的key
const keys = Object.keys(obj)
// 2.遍歷obj所有的key,并設置存取屬性描述符
keys.forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
console.log(`obj對象的${key}屬性被訪問啦!`)
return value
},
set: function(newValue) {
console.log(`obj對象的${key}屬性被設置啦!`)
value = newValue
}
})
})
// 設置:
obj.name = 'kobe' // obj對象的name屬性被設置啦!
obj.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(obj.name) // obj對象的name屬性被訪問啦!
console.log(obj.age) // obj對象的age屬性被訪問啦!
在Vue2.x中響應式原理實現的核心就是使用的Object.defineProperty,而在Vue3.x中響應式原理的核心被換成了Proxy,為什么要這樣做呢?主要是Object.defineProperty用來監聽對象屬性變化,有以下缺點:
- 首先,Object.defineProperty設計的初衷就不是為了去監聽對象屬性的,因為它的主要使用功能就是用來定義對象屬性的;
- 其次,Object.defineProperty在監聽對象屬性功能上有所缺陷,如果想監聽對象新增屬性、刪除屬性等等,它是無能為力的;
2.Proxy
在ES6中,新增了一個Proxy類,翻譯為代理,它可用于幫助我們創建一個代理對象,之后我們可以在這個代理對象上進行許多的操作。
2.1.Proxy的基本使用
如果希望監聽一個對象的相關操作,當Object.defineProperty不能滿足我們的需求時,那么可以使用Proxy創建一個代理對象,在代理對象上,我們可以監聽對原對象進行了哪些操作。下面將上面的例子用Proxy來實現,看看效果。
基本語法:const p = new Proxy(target, handler)
- target:需要代理的目標對象;
- handler:定義的各種操作代理對象的行為(也稱為捕獲器);
const obj = {
name: 'curry',
age: 30
}
// 創建obj的代理對象
const objProxy = new Proxy(obj, {
// 獲取對象屬性值的捕獲器
get: function(target, key) {
console.log(`obj對象的${key}屬性被訪問啦!`)
return target[key]
},
// 設置對象屬性值的捕獲器
set: function(target, key, newValue) {
console.log(`obj對象的${key}屬性被設置啦!`)
target[key] = newValue
}
})
// 之后的操作都是拿代理對象objProxy
// 設置:
objProxy.name = 'kobe' // obj對象的name屬性被設置啦!
objProxy.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(objProxy.name) // obj對象的name屬性被訪問啦!
console.log(objProxy.age) // obj對象的age屬性被訪問啦!
// 可以發現原對象obj同時發生了改變
console.log(obj) // { name: 'kobe', age: 24 }
2.2.Proxy的set和get捕獲器
在上面的例子中,其實已經使用到了set和get捕獲器,而set和get捕獲器是最為常用的捕獲器,下面具體來看看這兩個捕獲器吧。
(1)set捕獲器
set函數可接收四個參數:
- target:目標對象(被代理對象);
- property:將被設置的屬性key;
- value:設置的新屬性值;
- receiver:調用的代理對象;
(2)get捕獲器
get函數可接收三個參數:
- target:目標對象;
- property:被獲取的屬性key;
- receiver:調用的代理對象;
2.3.Proxy的Apply和construct捕獲器
上面所講的都是對對象屬性的操作進行監聽,其實Proxy提供了更為強大的功能,可以幫助我們監聽函數的調用方式。
- apply:監聽函數是否使用apply方式調用。
- construct:監聽函數是否使用new操作符調用。
function fn(x, y) {
return x + y
}
const fnProxy = new Proxy(fn, {
/*
target: 目標函數(fn)
thisArg: 指定的this對象,也就是被調用時的上下文對象({ name: 'curry' })
argumentsList: 被調用時傳遞的參數列表([1, 2])
*/
apply: function(target, thisArg, argumentsList) {
console.log('fn函數使用apply進行了調用')
return target.apply(thisArg, argumentsList)
},
/*
target: 目標函數(fn)
argumentsList: 被調用時傳遞的參數列表
newTarget: 最初被調用的構造函數(fnProxy)
*/
construct: function(target, argumentsList, newTarget) {
console.log('fn函數使用new進行了調用')
return new target(...argumentsList)
}
})
fnProxy.apply({ name: 'curry' }, [1, 2]) // fn函數使用apply進行了調用
new fnProxy() // fn函數使用new進行了調用
2.4.Proxy所有的捕獲器
除了上面提到的4種捕獲器,Proxy還給我們提供了其它9種捕獲器,一共是13個捕獲器,下面對這13個捕獲器進行簡單總結,下面表格的捕獲器分別對應對象上的一些操作方法。
捕獲器handler |
捕獲對象 |
get() |
屬性讀取操作 |
set() |
屬性設置操作 |
has() |
in操作符 |
deleteProperty() |
delete操作符 |
apply() |
函數調用操作 |
construct() |
new操作符 |
getPrototypeOf() |
Object.getPrototypeOf() |
setPrototypeOf() |
Object.setPrototypeOf() |
isExtensible() |
Object.isExtensible() |
preventExtensions() |
Object.perventExtensions() |
getOwnPropertyDescriptor() |
Object.getOwnPropertyDescriptor() |
defineProperty() |
Object.defineProperty() |
ownKeys() |
Object.getOwnPropertySymbols() |
Proxy捕獲器具體用法可查閱MDN:https://developer.mozilla.org/zh-CN/docs/Web/JAVAScript/Reference/Global_Objects/Proxy
3.Reflect
在ES6中,還新增了一個API為Reflect,翻譯為反射,為一個內置對象,一般用于搭配Proxy進行使用。
3.1.Reflect有什么作用呢?
可能會有人疑惑,為什么在這里提到Reflect,它具體有什么作用呢?怎么搭配Proxy進行使用呢?
- Reflect上提供了很多操作JavaScript對象的方法,類似于Object上操作對象的方法;
- 比如:Reflect.getPrototypeOf()類似于Object.getPrototypeOf(),Reflect.defineProperty()類似于Object.defineProperty();
- 既然Object已經提供了這些方法,為什么還提出Reflect這個API呢?這里涉及到早期ECMA規范問題,Object本是作為一個構造函數用于創建對象,然而卻將這么多方法放到Object上,本就是不合適的;所以,ES6為了讓Object職責單一化,新增了Reflect,將Object上這些操作對象的方法添加到Reflect上,且Reflect不能作為構造函數進行new調用;
3.2.Reflect的基本使用
在上述Proxy中,操作對象的方法都可以換成對應的Reflect上的方法,基本使用如下:
const obj = {
name: 'curry',
age: 30
}
// 創建obj的代理對象
const objProxy = new Proxy(obj, {
// 獲取對象屬性值的捕獲器
get: function(target, key) {
console.log(`obj對象的${key}屬性被訪問啦!`)
return Reflect.get(target, key)
},
// 設置對象屬性值的捕獲器
set: function(target, key, newValue) {
console.log(`obj對象的${key}屬性被設置啦!`)
Reflect.set(target, key, newValue)
},
// 刪除對象屬性的捕獲器
deleteProperty: function(target, key) {
console.log(`obj對象的${key}屬性被刪除啦!`)
Reflect.deleteProperty(target, key)
}
})
// 設置:
objProxy.name = 'kobe' // obj對象的name屬性被設置啦!
objProxy.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(objProxy.name) // obj對象的name屬性被訪問啦!
console.log(objProxy.age) // obj對象的age屬性被訪問啦!
// 刪除:
delete objProxy.name // obj對象的name屬性被刪除啦!
3.3.Reflect上常見的方法
對比Object,我們來看一下Reflect上常見的操作對象的方法(靜態方法):
Reflect方法 |
類似于 |
get(target, propertyKey [, receiver]) |
獲取對象某個屬性值,target[name] |
set(target, propertyKey, value [, receiver]) |
將值分配給屬性的函數,返回一個boolean |
has(target, propertyKey) |
判斷一個對象是否存在某個屬性,和in運算符功能相同 |
deleteProperty(target, propertyKey) |
delete操作符,相當于執行delete target[name] |
apply(target, thisArgument, argumentsList) |
對一個函數進行調用操作,可以傳入一個數組作為調用參數,Function.prototype.apply() |
construct(target, argumentsList [, newTarget]) |
對構造函數進行new操作,new target(...args) |
getPrototypeOf(target) |
Object.getPrototype() |
setPrototypeOf(target, prototype) |
設置對象原型的函數,返回一個boolean |
isExtensible(target) |
Object.isExtensible() |
preventExtensions(target) |
Object.preventExtensions(),返回一個boolean |
getOwnPropertyDescriptor(target, propertyKey) |
|
defineProperty(target, propertyKey, attributes) |
Object.defineProperty(),設置成功返回true |
ownKeys(target) |
返回一個包含所有自身屬性(不包含繼承屬性)的數組,類似于Object.keys(),但是不會受enumerable影響 |
具體Reflect和Object對象之間的關系和使用方法,可以參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
3.4.Reflect的construct方法
construct方法有什么作用呢?具體的應用場景是什么?這里提一個需求,就明白construct方法的作用了。
需求:創建Person和Student兩個構造函數,最終的實例對象執行的是Person中的代碼,帶上實例對象的類型是Student。
construct可接收的參數:
- target:被運行的目標構造函數(Person);
- argumentsList:類數組對象,參數列表;
- newTarget:作為新創建對象原型對象的constructor屬性(Student);
function Person(name, age) {
this.name = name
this.age = age
}
function Student() {}
const stu = Reflect.construct(Person, ['curry', 30], Student)
console.log(stu)
console.log(stu.__proto__ === Student.prototype)
打印結果:實例對象的類型為Student,并且實例對象原型指向Student構造函數的原型。
Reflect的construct方法就可以用于類繼承的實現,可在babel工具中查看ES6轉ES5后的代碼,就是使用的Reflect的construct方法:
4.receiver的作用
在介紹Proxy的set和get捕獲器的時候,其中有個參數叫receiver,具體什么是調用的代理對象呢?它的作用是什么?
如果原對象(需要被代理的對象)它有自己的getter和setter服務器屬性時,那么就可以通過receiver來改變里面的this。
// 假設obj的age為私有屬性,需要通過getter和setter來訪問和設置
const obj = {
name: 'curry',
_age: 30,
get age() {
return this._age
},
set age(newValue) {
this._age = newValue
}
}
const objProxy = new Proxy(obj, {
get: function(target, key, reveiver) {
console.log(`obj對象的${key}屬性被訪問啦!`)
return Reflect.get(target, key)
},
set: function(target, key, newValue, reveiver) {
console.log(`obj對象的${key}屬性被設置啦!`)
Reflect.set(target, key, newValue)
}
})
// 設置:
objProxy.name = 'kobe'
objProxy.age = 24
// 訪問:
console.log(objProxy.name)
console.log(objProxy.age)
在沒有使用receiver的情況下的打印結果為:name和age屬性都被訪問一次和設置一次。
但是由于原對象obj中對age進行了攔截操作,我們看一下age具體的訪問步驟:
- 首先,打印objProxy.age會被代理對象objProxy中的get捕獲器所捕獲;
- 緊接著Reflect.get(target, key)對obj中的age進行了訪問,又會被obj中的get訪問器所攔截,返回this._age;
- 很顯然在執行this._age的時候_age在這里是被訪問了的,而這里的this指向的原對象obj;
- 一般地,通過this._age的時候,應該也是要被代理對象的get捕獲器所捕獲的,那么就需要將這里的this修改成objProxy,相當于objProxy._age,在代理對象objProxy中就可以被get捕獲到了;
- receiver的作用就在這里,把原對象中this改成其代理對象,同理age被設置也是一樣的,訪問和設置信息都需要被打印兩次;
// 假設obj的age為私有屬性,需要通過getter和setter來訪問和設置
const obj = {
name: 'curry',
_age: 30,
get age() {
return this._age
},
set age(newValue) {
this._age = newValue
}
}
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`obj對象的${key}屬性被訪問啦!`)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
console.log(`obj對象的${key}屬性被設置啦!`)
Reflect.set(target, key, newValue, receiver)
}
})
// 設置:
objProxy.name = 'kobe'
objProxy.age = 24
// 訪問:
console.log(objProxy.name)
console.log(objProxy.age)
再來看一下打印結果:
也可以打印receiver,在瀏覽器中進行查看,其實就是這里的objProxy:
5.響應式原理的實現
5.1.什么是響應式呢?
當某個變量值發生變化時,會自動去執行某一些代碼。如下代碼,當變量num發生變化時,對num有所依賴的代碼可以自動執行。
let num = 30
console.log(num) // 當num方式變化時,這段代碼能自動執行
console.log(num * 30) // 當num方式變化時,這段代碼能自動執行
num = 1
- 像上面這一種自動響應數據變化的代碼機制,就稱之為響應式;
- 在開發中,一般都是監聽某一個對象中屬性的變化,然后自動去執行某一些代碼塊,而這些代碼塊一般都存放在一個函數中,因為函數可以方便我們再次執行這些代碼,只需再次調用函數即可;
5.2.收集響應式函數的實現
在響應式中,需要執行的代碼可能不止一行,而且也不可能一行行去執行,所以可以將這些代碼放到一個函數中,當數據發生變化,自動去執行某一個函數。但是在開發中有那么多函數,怎么判斷哪些函數需要響應式?哪些又不需要呢?
- 封裝一個watchFn的函數,將需要響應式的函數傳入;
- watchFn的主要職責就是將這些需要響應式的函數收集起來,存放到一個數組reactiveFns中;
const obj = {
name: 'curry',
age: 30
}
// 定義一個存放響應式函數的數組
const reactiveFns = []
// 封裝一個用于收集響應式函數的函數
function watchFn(fn) {
reactiveFns.push(fn)
}
watchFn(function() {
let newName = obj.name
console.log(newName)
console.log('1:' + obj.name)
})
watchFn(function() {
console.log('2:' + obj.name)
})
obj.name = 'kobe'
// 當obj中的屬性值發送變化時,遍歷執行那些收集的響應式函數
reactiveFns.forEach(fn => {
fn()
})
5.3.收集響應式函數的優化
上面實現的收集響應式函數,目前是存放到一個數組中來保存的,而且只是對name屬性的的依賴進行了收集,如果age屬性也需要收集,不可能都存放到一個數組里面,而且屬性值改變后,還需要通過手動去遍歷調用,顯而易見是很麻煩的,下面做一些優化。
- 封裝一個類,專門用于收集這些響應式函數;
- 類中添加一個notify的方法,用于遍歷調用這些響應式函數;
- 對于不同的屬性,就分別去實例化這個類,那么每個屬性就可以對應一個對象,并且對象中有一個存放它的響應式數組的屬性reactiveFns;
class Depend {
constructor() {
// 用于存放響應式函數
this.reactiveFns = []
}
// 用戶添加響應式函數
addDependFn(fn) {
this.reactiveFns.push(fn)
}
// 用于執行響應式函數
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'curry',
age: 30
}
const dep = new Depend()
// 在watchFn中使用dep的addDependFn來收集
function watchFn(fn) {
dep.addDependFn(fn)
}
watchFn(function() {
let newName = obj.name
console.log(newName)
console.log('1:' + obj.name)
})
watchFn(function() {
console.log('2:' + obj.name)
})
obj.name = 'kobe'
// name屬性發生改變,直接調用notify
dep.notify()
5.4.自動監聽對象的變化
在修改對象屬性值后,還是需要手動去調用其notify函數來通知響應式函數執行,其實可以做到自動監聽對象屬性的變化,來自動調用notify函數,這個想必就很容易了,在前面做了那么多功課,就是為了這里,不管是用Object.defineProperty還是Proxy都可以實現對象的監聽,這里我使用功能更加強大的Proxy,并結合Reflect來實現。
class Depend {
constructor() {
// 用于存放響應式函數
this.reactiveFns = []
}
// 用戶添加響應式函數
addDependFn(fn) {
this.reactiveFns.push(fn)
}
// 用于執行響應式函數
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'curry',
age: 30
}
const dep = new Depend()
// 在watchFn中使用dep的addDependFn來收集
function watchFn(fn) {
dep.addDependFn(fn)
}
// 創建一個Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 當set捕獲器捕獲到屬性變化時,自動去調用notify
dep.notify()
}
})
watchFn(function() {
let newName = objProxy.name
console.log(newName)
console.log('1:' + objProxy.name)
})
watchFn(function() {
console.log('2:' + objProxy.name)
})
objProxy.name = 'kobe'
objProxy.name = 'klay'
objProxy.name = 'james'
注意:后面使用到的obj對象,需都換成代理對象objProxy,這樣儲能監聽到屬性值是否被設置了。
打印結果:name屬性修改了三次,對應依賴函數就執行了三次。
5.5.對象依賴的管理(數據存儲結構設計)
在上面實現響應式過程中,都是基于一個對象的一個屬性,如果有多個對象,這多個對象中有不同或者相同的屬性呢?我們應該這樣去單獨管理不同對象中每個屬性所對應的依賴呢?應該要做到當某一個對象中的某一個屬性發生變化時,只去執行對這個對象中這個屬性有依賴的函數,下面就來講一下怎樣進行數據存儲,能夠達到我們的期望。
在ES16中,給我們新提供了兩個新特性,分別是Map和WeakMap,這兩個類都可以用于存放數據,類似于對象,存放的是鍵值對,但是Map和WeakMap的key可以存放對象,而且WeakMap對對象的引用是弱引用。如果對這兩個類不太熟悉,可以去看看上一篇文章:ES6-ES12簡單知識點總結
- 將不同的對象存放到WeakMap中作為key,其value存放對應的Map;
- Map中存放對應對象的屬性作為key,其value存放對應的依賴對象;
- 依賴對象中存放有該屬性對應響應式函數數組;
如果有以下obj1和obj2兩個對象,來看一下它們大致的存儲形式:
const obj1 = { name: 'curry', age: 30 }
const obj2 = { name: 'kobe', age: 24 }
5.6.對象依賴管理的實現
已經確定了怎么存儲了,下面就來實現一下吧。
- 封裝一個getDepend函數,主要用于根據對象和key,來找到對應的dep;
- 如果沒有找到就先進行創建存儲;
// 1.創建一個WeakMap存儲結構,存放對象
const objWeakMap = new WeakMap()
// 2.封裝一個獲取dep的函數
function getDepend(obj, key) {
// 2.1.根據對象,獲取對應的map
let map = objWeakMap.get(obj)
// 如果是第一次獲取這個map,那么需要先創建一個map
if (!map) {
map = new Map()
// 將map存到objWeakMap中對應key上
objWeakMap.set(obj, map)
}
// 2.2.根據對象的屬性,獲取對應的dep
let dep = map.get(key)
// 如果是第一次獲取這個dep,那么需要先創建一個dep
if (!dep) {
dep = new Depend()
// 將dep存到map中對應的key上
map.set(key, dep)
}
// 2.3最終將dep返回出去
return dep
}
在Proxy的捕獲器中獲取對應的dep:
// 創建一個Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根據當前對象target和設置的key,去獲取對應的dep
const dep = getDepend(target, key)
console.log(dep)
// 當set捕獲器捕獲到屬性變化時,自動去調用notify
dep.notify()
}
})
5.7.對象的依賴收集優化
可以發現上面打印的結果中的響應式函數數組全部為空,是因為在前面收集響應式函數是通過watchFn來收集的,而在getDepend中并沒有去收集對應的響應式函數,所以返回的dep對象里面的數組全部就為空了。如果對響應式函數,還需要通過自己一個個去收集,是不太容易的,所以可以監聽響應式函數中依賴了哪一個對象屬性,讓Proxy的get捕獲器去收集就行了。
- 既然get需要監聽到響應式函數訪問了哪些屬性,那么響應式函數在被添加之前肯定是要執行一次的;
- 如何在Proxy中拿到當前需要被收集的響應式函數呢?可以借助全局變量;
- 下面就來對watchFn進行改造;
// 定義一個全局變量,存放當前需要收集的響應式函數
let currentReactiveFn = null
function watchFn(fn) {
currentReactiveFn = fn
// 先調用一次函數,提醒Proxy的get捕獲器需要收集響應式函數了
fn()
// 收集完成將currentReactiveFn重置
currentReactiveFn = null
}
Proxy中get捕獲器具體需要執行的操作:
// 創建一個Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 拿到全局的currentReactiveFn進行添加
dep.addDependFn(currentReactiveFn)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根據當前對象target和設置的key,去獲取對應的dep
const dep = getDepend(target, key)
console.log(dep)
// 當set捕獲器捕獲到屬性變化時,自動去調用notify
dep.notify()
}
})
下面測試一下看看效果:
watchFn(function() {
console.log('1:我依賴了name屬性')
console.log(objProxy.name)
})
watchFn(function() {
console.log('2:我依賴了name屬性')
console.log(objProxy.name)
})
watchFn(function() {
console.log('1:我依賴了age屬性')
console.log(objProxy.age)
})
watchFn(function() {
console.log('2:我依賴了age屬性')
console.log(objProxy.age)
})
console.log('----------以上為初始化執行,以下為修改后執行-------------')
objProxy.name = 'kobe'
objProxy.age = 24
5.8.Depend類優化
截止到上面,大部分響應式原理已經實現了,但是還存在一些小問題需要優化。
- 優化一:既然currentReactiveFn可以在全局拿到,何不在Depend類中就對它進行收集呢。改造方法addDependFn;
- 優化二:如果一個響應式函數中多次訪問了某個屬性,就都會去到Proxy的get捕獲器,該響應式函數會被重復收集,在調用時就會調用多次。當屬性發生變化后,依賴這個屬性的響應式函數被調用一次就可以了。改造reactiveFns,將數組改成Set,Set可以避免元素重復,注意添加元素使用add。
// 將currentReactiveFn放到Depend之前,方便其拿到
let currentReactiveFn = null
class Depend {
constructor() {
// 用于存放響應式函數
this.reactiveFns = new Set()
}
// 用戶添加響應式函數
addDependFn() {
// 先判斷一下currentReactiveFn是否有值
if (currentReactiveFn) {
this.reactiveFns.add(currentReactiveFn)
}
}
// 用于執行響應式函數
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
Proxy中就不用去收集響應式函數了,直接調用addDependFn即可:
// 創建一個Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 直接調用addDepend方法,讓它去收集
dep.addDependFn()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根據當前對象target和設置的key,去獲取對應的dep
const dep = getDepend(target, key)
// 當set捕獲器捕獲到屬性變化時,自動去調用notify
dep.notify()
}
})
5.9.多個對象實現響應式
前面都只講了一個對象實現響應式的實現,如果有多個對象需要實現可響應式呢?將Proxy封裝一下,外面套一層函數即可,調用該函數,返回該對象的代理對象。
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 直接調用addDepend方法,讓它去收集
dep.addDependFn()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根據當前對象target和設置的key,去獲取對應的dep
const dep = getDepend(target, key)
// 當set捕獲器捕獲到屬性變化時,自動去調用notify
dep.notify()
}
})
}
看一下具體使用效果:
const obj1 = { name: 'curry', age: 30 }
const obj2 = { weight: '130', height: '180' }
const obj1Proxy = reactive(obj1)
const obj2Proxy = reactive(obj2)
watchFn(function() {
console.log('我依賴了obj1的name屬性')
console.log(obj1Proxy.name)
})
watchFn(function() {
console.log('我依賴了age屬性')
console.log(obj1Proxy.age)
})
watchFn(function() {
console.log('我依賴了obj2的weight屬性')
console.log(obj2Proxy.weight)
})
watchFn(function() {
console.log('我依賴了obj2的height屬性')
console.log(obj2Proxy.height)
})
console.log('----------以上為初始化執行,以下為修改后執行-------------')
obj1Proxy.name = 'kobe'
obj1Proxy.age = 24
obj2Proxy.weight = 100
obj2Proxy.height = 165
5.10.總結整理
通過上面9步完成了最終響應式原理的實現,下面對其進行整理一下:
- watchFn函數:傳入該函數的函數都是需要被收集為響應式函數的,對響應式函數進行初始化調用,使Proxy的get捕獲器能捕獲到屬性訪問;
- function watchFn(fn) { currentReactiveFn = fn // 先調用一次函數,提醒Proxy的get捕獲器需要收集響應式函數了 fn() // 收集完成將currentReactiveFn重置 currentReactiveFn = null }
- Depend類:reactiveFns用于存放響應式函數,addDependFn方法實現對響應式函數的收集,notify方法實現當屬性值變化時,去調用對應的響應式函數;
- // 將currentReactiveFn放到Depend之前,方便其拿到 let currentReactiveFn = null class Depend { constructor() { // 用于存放響應式函數 this.reactiveFns = new Set() } // 用戶添加響應式函數 addDependFn() { // 先判斷一下currentReactiveFn是否有值 if (currentReactiveFn) { this.reactiveFns.add(currentReactiveFn) } } // 用于執行響應式函數 notify() { this.reactiveFns.forEach(fn => { fn() }) } }
- reactive函數:實現將普通對象轉成代理對象,從而將其轉變為可響應式對象;
- function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { const dep = getDepend(target, key) // 直接調用addDepend方法,讓它去收集 dep.addDependFn() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // 根據當前對象target和設置的key,去獲取對應的dep const dep = getDepend(target, key) // 當set捕獲器捕獲到屬性變化時,自動去調用notify dep.notify() } }) }
- getDepend函數:根據指定的對象和對象屬性(key)去查找對應的dep對象;
- // 1.創建一個WeakMap存儲結構,存放對象 const objWeakMap = new WeakMap() // 2.封裝一個獲取dep的函數 function getDepend(obj, key) { // 2.1.根據對象,獲取對應的map let map = objWeakMap.get(obj) // 如果是第一次獲取這個map,那么需要先創建一個map if (!map) { map = new Map() // 將map存到objWeakMap中對應key上 objWeakMap.set(obj, map) } // 2.2.根據對象的屬性,獲取對應的dep let dep = map.get(key) // 如果是第一次獲取這個dep,那么需要先創建一個dep if (!dep) { dep = new Depend() // 將dep存到map中對應的key上 map.set(key, dep) } // 2.3最終將dep返回出去 return dep }
總結:以上通過Proxy來監聽對象操作的實現響應式的方法就是Vue3響應式原理了。
6.Vue2響應式原理的實現
Vue3響應式原理已經實現了,那么Vue2只需要將Proxy換成Object.defineProperty就可以了。
- 將reactive函數改一下即可;
function reactive(obj) {
// 1.拿到obj所有的key
const keys = Object.keys(obj)
// 2.遍歷所有的keys,添加存取屬性描述符
keys.forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const dep = getDepend(obj, key)
// 直接調用addDepend方法,讓它去收集
dep.addDependFn()
return value
},
set: function(newValue) {
value = newValue
// 根據當前對象設置的key,去獲取對應的dep
const dep = getDepend(obj, key)
// 監聽到屬性變化時,自動去調用notify
dep.notify()
}
})
})
// 3.將obj返回
return obj
}
本文來自
https://www.cnblogs.com/MomentYY/p/16065162.html