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