由淺入深,帶你用JavaScript實現響應式原理
前言
為什麼前端框架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) | Object.getOwnPropertyDescriptor(),如果物件中存在該屬性,則返回對應屬性描述符,否則返回undefined |
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
}