一篇搞懂Vue2、Vue3響應(yīng)式源碼的原理
前言
我們在編寫Vue2,Vue3代碼的時候,經(jīng)常會在data中定義某些數(shù)據(jù),然后在template用到的時候,可能會在多處用到這些數(shù)據(jù),通過對這些數(shù)據(jù)的操作,可以達到改變視圖的作用,即所謂數(shù)據(jù)驅(qū)動視圖。
我們可以通過Mustache 語法,讓data可以在頁面上顯示,隨著data的變化,視圖中也會隨之改變。
那么,這種響應(yīng)式操作在Vue2、Vue3中是怎么實現(xiàn)的呢?
Vue2響應(yīng)式操作
響應(yīng)式函數(shù)的封裝
在進行響應(yīng)式操作前,我們需要簡單大致封裝一個響應(yīng)式函數(shù),參數(shù)接收的是函數(shù),凡是傳入到響應(yīng)式函數(shù)的函數(shù),就是需要響應(yīng)式的,其他默認定義的函數(shù)是不需要響應(yīng)式的。
我們需要用一個數(shù)組將他們收集起來,(現(xiàn)在暫時使用函數(shù),最好的辦法是放入Set中,下文會講),代碼如下:
// 封裝一個響應(yīng)式的函數(shù) let reactiveFns = [] function watchFn(fn) { reactiveFns.push(fn) }
等到我們需要執(zhí)行這些函數(shù)的時候(什么時候需要執(zhí)行是后話,先簡單提一下),可以遍歷這個數(shù)組然后執(zhí)行:
reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E--> fn() })
Depend類的封裝
我們需要封裝一個Depend類,這個類的作用是:這個類用于管理某一個對象的某一個屬性的所有響應(yīng)式函數(shù)。一個對象里面可能會有多個屬性并且有他們對應(yīng)的值,我們可能用到了這個對象里面多個屬性,所以我們要給這里的每個用到的屬性建立一個屬于自己的類,用來管理對這個屬性有依賴的所有函數(shù)。
所以我們得想辦法拿到剛才在響應(yīng)式函數(shù)里面?zhèn)鬟M去的函數(shù),這里我們可以用activeReactiveFn暫時保存剛才傳進去的函數(shù)。
所以我們對響應(yīng)式函數(shù)的封裝進行重構(gòu)一下,如下:
// 保存當(dāng)前需要收集的響應(yīng)式函數(shù) let activeReactiveFn = null // 封裝一個響應(yīng)式的函數(shù) function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null }
因為某個屬性可能會用多個函數(shù)進行依賴,所有在這個類的內(nèi)部我們會定義一個Set, reactiveFns = new Set(),定義成Set而不是數(shù)組是因為Set數(shù)據(jù)結(jié)構(gòu)沒有重復(fù)的數(shù)據(jù),從而防止了重復(fù)的操作。
這里定義了一個depend方法可以將activeReactiveFn在有值的情況下,放入reactiveFns中,notify函數(shù)就是將這些收集了的函數(shù)進行執(zhí)行。
class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } }
監(jiān)聽對象的變化
在Vue2中使用的監(jiān)聽對象的變化使用的方法是:使用Object.defineProperty。
我們可以封裝一個reactive函數(shù),參數(shù)傳入一個對象,函數(shù)內(nèi)部對這個對象進行監(jiān)聽,遍歷這個對象,獲取所有的屬性和屬性值,對每個屬性使用Object.defineProperty,在Object.defineProperty第三個參數(shù)中,get和set方法中,在set方法中,修改值為新的值之后,之前提到,每個屬性都要有屬于自己的Depend對象,那么如何獲取這個對象呢?
那這里還有個問題,有不同的對象,對象里面又有多個屬性,那么這該如何解決呢?
可以定義一個WeakMap將各個對象保存成Map形式,然后在每個單一對象里面,我們可以用Map形式保存屬性的Depend類,如圖所示:
那么如何根據(jù)對象名,屬性名獲取depend呢?可以在getDepend函數(shù)實現(xiàn),參數(shù)傳入對象名,屬性名
,代碼如下:
// 封裝一個獲取depend函數(shù) const targetMap = new WeakMap() function getDepend(target, key) { // 根據(jù)target對象獲取map的過程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根據(jù)key獲取depend對象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend }
獲取到屬性特定的depend后,回到原來的話題,那么在set方法中,修改值為新的值之后,獲取到屬性特定的depend后,要調(diào)用depend里面的notify方法,使對這個屬性有依賴的所有函數(shù)執(zhí)行,也就是對數(shù)據(jù)進行更新。
在get方法中,在返回屬性值之前,要先獲取到屬性特定的depend后,調(diào)用depend里面的depend方法,將對此屬性依賴的函數(shù)保存下來。
代碼如下:
function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj }
至此,Vue2的響應(yīng)式操作就已經(jīng)實現(xiàn)了
所有代碼以及測試代碼如下:
// 保存當(dāng)前需要收集的響應(yīng)式函數(shù) let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } } // 封裝一個響應(yīng)式的函數(shù) function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } // 封裝一個獲取depend函數(shù) const targetMap = new WeakMap() function getDepend(target, key) { // 根據(jù)target對象獲取map的過程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根據(jù)key獲取depend對象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj } // 監(jiān)聽對象的屬性變量: Proxy(vue3)/Object.defineProperty(vue2) const objProxy = reactive({ name: "cy", // depend對象 age: 18 // depend對象 }) const infoProxy = reactive({ address: "安徽省", height: 1.88 }) watchFn(() => { console.log(infoProxy.address) }) infoProxy.address = "北京市" const foo = reactive({ name: "foo" }) watchFn(() => { console.log(foo.name) }) foo.name = "aaa" foo.name = "bbb" // 安徽省 // 北京市 // foo // aaa // bbb
Vue3響應(yīng)式操作
Proxy、Reflect
Proxy:
在Vue2中,使用Object.defineProperty來監(jiān)聽對象的變化,但是這樣做有什么缺點呢?
首先,Object.defineProperty設(shè)計的初衷,不是為了去監(jiān)聽截止一個對象中所有的屬性的。
我們在定義某些屬性的時候,初衷其實是定義普通的屬性,但是后面我們強行將它變成了數(shù)據(jù)屬性描述符。
其次,如果我們想監(jiān)聽更加豐富的操作,比如新增屬性、刪除屬性,那么Object.defineProperty是無能為力的。
所以我們要知道,存儲數(shù)據(jù)描述符設(shè)計的初衷并不是為了去監(jiān)聽一個完整的對象
在ES6中,新增了一個Proxy類,這個類從名字就可以看出來,是用于幫助我們創(chuàng)建一個代理的:
也就是說,如果我們希望監(jiān)聽一個對象的相關(guān)操作,那么我們可以先創(chuàng)建一個代理對象(Proxy對象);
之后對該對象的所有操作,都通過代理對象來完成,代理對象可以監(jiān)聽我們想要對原對象進行哪些操作;
如果我們想要偵聽某些具體的操作,那么就可以在handler中添加對應(yīng)的捕捉器(Trap):
set函數(shù)有四個參數(shù):
target:目標(biāo)對象(偵聽的對象);
property:將被設(shè)置的屬性key;
value:新屬性值;
receiver:調(diào)用的代理對象;
get函數(shù)有三個參數(shù):
target:目標(biāo)對象(偵聽的對象);
property:被獲取的屬性key;
receiver:調(diào)用的代理對象
實例代碼如下;
const obj = { name: "cy", age: 18 } const objProxy = new Proxy(obj, { // 獲取值時的捕獲器 get: function(target, key) { console.log(`監(jiān)聽到對象的${key}屬性被訪問了`, target) return target[key] }, // 設(shè)置值時的捕獲器 set: function(target, key, newValue) { console.log(`監(jiān)聽到對象的${key}屬性被設(shè)置值`, target) target[key] = newValue } }) console.log(objProxy.name) console.log(objProxy.age) objProxy.name = "kobe" objProxy.age = 30 console.log(obj.name) console.log(obj.age) // 監(jiān)聽到對象的name屬性被訪問了 { name: 'cy', age: 18 } // cy // 監(jiān)聽到對象的age屬性被訪問了 { name: 'cy', age: 18 } // 18 // 監(jiān)聽到對象的name屬性被設(shè)置值 { name: 'cy', age: 18 } // 監(jiān)聽到對象的age屬性被設(shè)置值 { name: 'kobe', age: 18 } // kobe // 30
Reflect:
Reflect也是ES6新增的一個API,它是一個對象,字面的意思是反射。
那么這個Reflect有什么用呢?
它主要提供了很多操作JavaScript對象的方法,有點像Object中操作對象的方法;
比如Reflect.getPrototypeOf(target)類似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)類似于Object.defineProperty() ;
如果我們有Object可以做這些操作,那么為什么還需要有Reflect這樣的新增對象呢?
這是因為在早期的ECMA規(guī)范中沒有考慮到這種對 對象本身 的操作如何設(shè)計會更加規(guī)范,所以將這些API放到了Object上面;
但是Object作為一個構(gòu)造函數(shù),這些操作實際上放到它身上并不合適;
另外還包含一些類似于 in、delete操作符,讓JS看起來是會有一些奇怪的;
所以在ES6中新增了Reflect,讓我們這些操作都集中到了Reflect對象上;
Reflect中常見的方法:
那么我們可以將之前Proxy案例中對原對象的操作,都修改為Reflect來操作;
我們發(fā)現(xiàn)在使用getter、setter的時候有一個receiver的參數(shù),它的作用是什么呢?
如果我們的源對象(obj)有setter、getter的訪問器屬性,那么可以通過receiver來改變里面的this
Vue3響應(yīng)式
Vue3響應(yīng)式使用的是Proxy,我們需要在Vue2的reactive函數(shù)里面進行一些改變:
function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { // 根據(jù)target.key獲取對應(yīng)的depend const depend = getDepend(target, key) // 給depend對象中添加響應(yīng)函數(shù) depend.depend() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // depend.notify() const depend = getDepend(target, key) depend.notify() } }) }
其他方面的代碼同Vue2基本沒啥變化,Vue3的響應(yīng)式操作就已經(jīng)實現(xiàn)了
所有代碼以及測試代碼如下:
// 保存當(dāng)前需要收集的響應(yīng)式函數(shù) let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } } // 封裝一個響應(yīng)式的函數(shù) function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } // 封裝一個獲取depend函數(shù) const targetMap = new WeakMap() function getDepend(target, key) { // 根據(jù)target對象獲取map的過程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根據(jù)key獲取depend對象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { // 根據(jù)target.key獲取對應(yīng)的depend const depend = getDepend(target, key) // 給depend對象中添加響應(yīng)函數(shù) depend.depend() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // depend.notify() const depend = getDepend(target, key) depend.notify() } }) } // 監(jiān)聽對象的屬性變量: Proxy(vue3)/Object.defineProperty(vue2) const objProxy = reactive({ name: "cy", // depend對象 age: 18 // depend對象 }) const infoProxy = reactive({ address: "安徽省", height: 1.88 }) watchFn(() => { console.log(infoProxy.address) }) infoProxy.address = "北京市" const foo = reactive({ name: "foo" }) watchFn(() => { console.log(foo.name) }) foo.name = "bar" // 安徽省 // 北京市 // foo // bar
以上就是一篇搞懂Vue2、Vue3響應(yīng)式源碼的原理的詳細內(nèi)容,更多關(guān)于Vue2、Vue3響應(yīng)式源碼的原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一個超簡單的JS拖拽實現(xiàn)代碼(兼容IE,Firefox)
網(wǎng)上找的一個超簡單的JS拖拽,喜歡拖拽效果的朋友可以參考下。2010-04-04JavaScript中的undefined學(xué)習(xí)總結(jié)
這篇文章主要是對JavaScript中的undefined進行了介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11js實現(xiàn)當(dāng)復(fù)選框選擇匿名登錄時隱藏登錄框效果
這篇文章主要介紹了js實現(xiàn)當(dāng)復(fù)選框選擇匿名登錄時隱藏登錄框效果,實例分析了javascript動態(tài)操作頁面元素樣式的相關(guān)技巧,非常簡單實用,需要的朋友可以參考下2015-08-08關(guān)于JavaScript中forEach和each用法淺析
這篇文章主要給大家介紹了關(guān)于JavaScript中forEach和each使用方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-07-07