一篇搞懂Vue2、Vue3響應(yīng)式源碼的原理
前言
我們在編寫Vue2,Vue3代碼的時(shí)候,經(jīng)常會在data中定義某些數(shù)據(jù),然后在template用到的時(shí)候,可能會在多處用到這些數(shù)據(jù),通過對這些數(shù)據(jù)的操作,可以達(dá)到改變視圖的作用,即所謂數(shù)據(jù)驅(qū)動視圖。
我們可以通過Mustache 語法,讓data可以在頁面上顯示,隨著data的變化,視圖中也會隨之改變。
那么,這種響應(yīng)式操作在Vue2、Vue3中是怎么實(shí)現(xiàn)的呢?
Vue2響應(yīng)式操作
響應(yīng)式函數(shù)的封裝
在進(jìn)行響應(yīng)式操作前,我們需要簡單大致封裝一個(gè)響應(yīng)式函數(shù),參數(shù)接收的是函數(shù),凡是傳入到響應(yīng)式函數(shù)的函數(shù),就是需要響應(yīng)式的,其他默認(rèn)定義的函數(shù)是不需要響應(yīng)式的。
我們需要用一個(gè)數(shù)組將他們收集起來,(現(xiàn)在暫時(shí)使用函數(shù),最好的辦法是放入Set中,下文會講),代碼如下:
// 封裝一個(gè)響應(yīng)式的函數(shù)
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}等到我們需要執(zhí)行這些函數(shù)的時(shí)候(什么時(shí)候需要執(zhí)行是后話,先簡單提一下),可以遍歷這個(gè)數(shù)組然后執(zhí)行:
reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
fn()
})Depend類的封裝
我們需要封裝一個(gè)Depend類,這個(gè)類的作用是:這個(gè)類用于管理某一個(gè)對象的某一個(gè)屬性的所有響應(yīng)式函數(shù)。一個(gè)對象里面可能會有多個(gè)屬性并且有他們對應(yīng)的值,我們可能用到了這個(gè)對象里面多個(gè)屬性,所以我們要給這里的每個(gè)用到的屬性建立一個(gè)屬于自己的類,用來管理對這個(gè)屬性有依賴的所有函數(shù)。
所以我們得想辦法拿到剛才在響應(yīng)式函數(shù)里面?zhèn)鬟M(jìn)去的函數(shù),這里我們可以用activeReactiveFn暫時(shí)保存剛才傳進(jìn)去的函數(shù)。
所以我們對響應(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è)屬性可能會用多個(gè)函數(shù)進(jìn)行依賴,所有在這個(gè)類的內(nèi)部我們會定義一個(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)聽對象的變化
在Vue2中使用的監(jiān)聽對象的變化使用的方法是:使用Object.defineProperty。
我們可以封裝一個(gè)reactive函數(shù),參數(shù)傳入一個(gè)對象,函數(shù)內(nèi)部對這個(gè)對象進(jìn)行監(jiān)聽,遍歷這個(gè)對象,獲取所有的屬性和屬性值,對每個(gè)屬性使用Object.defineProperty,在Object.defineProperty第三個(gè)參數(shù)中,get和set方法中,在set方法中,修改值為新的值之后,之前提到,每個(gè)屬性都要有屬于自己的Depend對象,那么如何獲取這個(gè)對象呢?
那這里還有個(gè)問題,有不同的對象,對象里面又有多個(gè)屬性,那么這該如何解決呢?
可以定義一個(gè)WeakMap將各個(gè)對象保存成Map形式,然后在每個(gè)單一對象里面,我們可以用Map形式保存屬性的Depend類,如圖所示:

那么如何根據(jù)對象名,屬性名獲取depend呢?可以在getDepend函數(shù)實(shí)現(xiàn),參數(shù)傳入對象名,屬性名
,代碼如下:
// 封裝一個(gè)獲取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方法,使對這個(gè)屬性有依賴的所有函數(shù)執(zhí)行,也就是對數(shù)據(jù)進(jìn)行更新。
在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)實(shí)現(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()
})
}
}
// 封裝一個(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對象獲取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
// bbbVue3響應(yīng)式操作
Proxy、Reflect
Proxy:
在Vue2中,使用Object.defineProperty來監(jiān)聽對象的變化,但是這樣做有什么缺點(diǎn)呢?
首先,Object.defineProperty設(shè)計(jì)的初衷,不是為了去監(jiān)聽截止一個(gè)對象中所有的屬性的。
我們在定義某些屬性的時(shí)候,初衷其實(shí)是定義普通的屬性,但是后面我們強(qiáng)行將它變成了數(shù)據(jù)屬性描述符。
其次,如果我們想監(jiān)聽更加豐富的操作,比如新增屬性、刪除屬性,那么Object.defineProperty是無能為力的。
所以我們要知道,存儲數(shù)據(jù)描述符設(shè)計(jì)的初衷并不是為了去監(jiān)聽一個(gè)完整的對象
在ES6中,新增了一個(gè)Proxy類,這個(gè)類從名字就可以看出來,是用于幫助我們創(chuàng)建一個(gè)代理的:
也就是說,如果我們希望監(jiān)聽一個(gè)對象的相關(guān)操作,那么我們可以先創(chuàng)建一個(gè)代理對象(Proxy對象);
之后對該對象的所有操作,都通過代理對象來完成,代理對象可以監(jiān)聽我們想要對原對象進(jìn)行哪些操作;
如果我們想要偵聽某些具體的操作,那么就可以在handler中添加對應(yīng)的捕捉器(Trap):
set函數(shù)有四個(gè)參數(shù):
target:目標(biāo)對象(偵聽的對象);
property:將被設(shè)置的屬性key;
value:新屬性值;
receiver:調(diào)用的代理對象;
get函數(shù)有三個(gè)參數(shù):
target:目標(biāo)對象(偵聽的對象);
property:被獲取的屬性key;
receiver:調(diào)用的代理對象
實(shí)例代碼如下;
const obj = {
name: "cy",
age: 18
}
const objProxy = new Proxy(obj, {
// 獲取值時(shí)的捕獲器
get: function(target, key) {
console.log(`監(jiān)聽到對象的${key}屬性被訪問了`, target)
return target[key]
},
// 設(shè)置值時(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
// 30Reflect:
Reflect也是ES6新增的一個(gè)API,它是一個(gè)對象,字面的意思是反射。
那么這個(gè)Reflect有什么用呢?
它主要提供了很多操作JavaScript對象的方法,有點(diǎn)像Object中操作對象的方法;
比如Reflect.getPrototypeOf(target)類似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)類似于Object.defineProperty() ;
如果我們有Object可以做這些操作,那么為什么還需要有Reflect這樣的新增對象呢?
這是因?yàn)樵谠缙诘腅CMA規(guī)范中沒有考慮到這種對 對象本身 的操作如何設(shè)計(jì)會更加規(guī)范,所以將這些API放到了Object上面;
但是Object作為一個(gè)構(gòu)造函數(shù),這些操作實(shí)際上放到它身上并不合適;
另外還包含一些類似于 in、delete操作符,讓JS看起來是會有一些奇怪的;
所以在ES6中新增了Reflect,讓我們這些操作都集中到了Reflect對象上;
Reflect中常見的方法:

那么我們可以將之前Proxy案例中對原對象的操作,都修改為Reflect來操作;
我們發(fā)現(xiàn)在使用getter、setter的時(shí)候有一個(gè)receiver的參數(shù),它的作用是什么呢?
如果我們的源對象(obj)有setter、getter的訪問器屬性,那么可以通過receiver來改變里面的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獲取對應(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)實(shí)現(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()
})
}
}
// 封裝一個(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對象獲取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)式源碼的原理的詳細(xì)內(nèi)容,更多關(guān)于Vue2、Vue3響應(yīng)式源碼的原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一個(gè)超簡單的JS拖拽實(shí)現(xiàn)代碼(兼容IE,Firefox)
網(wǎng)上找的一個(gè)超簡單的JS拖拽,喜歡拖拽效果的朋友可以參考下。2010-04-04
JavaScript中的undefined學(xué)習(xí)總結(jié)
這篇文章主要是對JavaScript中的undefined進(jìn)行了介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11
js實(shí)現(xiàn)當(dāng)復(fù)選框選擇匿名登錄時(shí)隱藏登錄框效果
這篇文章主要介紹了js實(shí)現(xiàn)當(dāng)復(fù)選框選擇匿名登錄時(shí)隱藏登錄框效果,實(shí)例分析了javascript動態(tài)操作頁面元素樣式的相關(guān)技巧,非常簡單實(shí)用,需要的朋友可以參考下2015-08-08
關(guān)于JavaScript中forEach和each用法淺析
這篇文章主要給大家介紹了關(guān)于JavaScript中forEach和each使用方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-07-07

