1、 數(shù)據(jù)響應(yīng)式
首先請(qǐng)大家認(rèn)真的思考一個(gè)問(wèn)題:什么是數(shù)據(jù)響應(yīng)式?
答:數(shù)據(jù)變化是可偵測(cè)的,并且和數(shù)據(jù)相關(guān)的內(nèi)容可以更新。
?這里一定要明確一個(gè)概念,數(shù)據(jù)響應(yīng)式和視圖更新是沒(méi)有關(guān)系的!數(shù)據(jù)響應(yīng)式是一種機(jī)制,一種數(shù)據(jù)變化的偵測(cè)機(jī)制。而實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式這種機(jī)制的方法不唯一。
那么,在vue
是如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式的?vue2和vue3的數(shù)據(jù)響應(yīng)式有什么區(qū)別?
2、vue如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式?
要知道,vue3.x
實(shí)現(xiàn)數(shù)據(jù)響應(yīng)的方案跟vue2.x
是不一樣的,所以在這里我將vue2.x
和vue3.x
分別說(shuō)說(shuō)。這也是理解vue2.x
和vue3.x
區(qū)別的時(shí)候,可以指出來(lái)的一個(gè)巨大的區(qū)別。
2.1 vue2.x的實(shí)現(xiàn)方案
我貼上一個(gè)vue2.x源碼-Object的變化偵測(cè)解讀的鏈接,方便大家理解和后續(xù)關(guān)于vue2.x的學(xué)習(xí)需要。
(特別是還沒(méi)閱讀過(guò)vue源碼的同學(xué),可以獨(dú)自過(guò)一遍這個(gè)文檔,能對(duì)vue有一個(gè)更深的認(rèn)識(shí))
在下面vue2
的源碼中可以看到,Observer
類(lèi)會(huì)通過(guò)遞歸的方式把一個(gè)對(duì)象的所有屬性都轉(zhuǎn)化成可觀測(cè)對(duì)象,所以我們可以知道vue2
需要遍歷對(duì)象的所有的key
。其實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式的核心思想就是通過(guò)defineProperty
,去定義get
、set
等方法。從而能夠攔截到對(duì)象屬性的訪問(wèn)和變更。
/**
* Observer類(lèi)會(huì)通過(guò)遞歸的方式把一個(gè)對(duì)象的所有屬性都轉(zhuǎn)化成可觀測(cè)對(duì)象
*/
export class Observer {
constructor (value) {
this.value = value
// 給value新增一個(gè)__ob__屬性,值為該value的Observer實(shí)例
// 相當(dāng)于為value打上標(biāo)記,表示它已經(jīng)被轉(zhuǎn)化成響應(yīng)式了,避免重復(fù)操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 當(dāng)value為數(shù)組時(shí)的邏輯
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
/**
* 使一個(gè)對(duì)象轉(zhuǎn)化成可觀測(cè)對(duì)象
* @param { Object } obj 對(duì)象
* @param { String } key 對(duì)象的key
* @param { Any } val 對(duì)象的某個(gè)key的值
*/
function defineReactive (obj,key,val) {
// 如果只傳了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}屬性被讀取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}屬性被修改了`);
val = newVal;
}
})
}
在日常開(kāi)發(fā)中,產(chǎn)品經(jīng)理總是會(huì)跟我們說(shuō),我們做了xxxx就是為了解決客戶(hù)的xxxx痛點(diǎn)。
那么,在繼續(xù)往下閱讀的時(shí)候,可以先思考一下vue2
這樣的實(shí)現(xiàn)方案的痛點(diǎn)有什么?或者說(shuō)缺點(diǎn)有什么?
因?yàn)樽鳛榭蛻?hù)(使用vue
開(kāi)發(fā)的前端同學(xué))的我們需要知道,vue3
是否解決了我們的痛點(diǎn)?
vue2的缺點(diǎn):(僅僅是關(guān)于數(shù)據(jù)響應(yīng)造成的缺點(diǎn)哦!)
- 1、影響初始化速度、數(shù)據(jù)過(guò)大時(shí)的資源問(wèn)題
(在源碼的Observer
方法上,對(duì)象的每一個(gè)屬性都要被攔截。所有的key都要有一次循環(huán)和遞歸) - 2、數(shù)組的特殊處理,導(dǎo)致其修改數(shù)據(jù)不能使用索引
(原因在于defineProperty
不支持?jǐn)?shù)組,參考vue源碼-Array的變化偵測(cè)) - 3、動(dòng)態(tài)添加或刪除對(duì)象屬性無(wú)法被偵測(cè)
(defineProperty
哭著對(duì)我說(shuō):臣妾的的setter
函數(shù)辦不到呀)
對(duì)于沒(méi)閱讀過(guò)vue源碼的前端開(kāi)發(fā)來(lái)說(shuō),應(yīng)該也遇到過(guò)修改了數(shù)組,或者修改對(duì)象后發(fā)現(xiàn),啥變化也沒(méi)有,一頭霧水,拍桌子直呼:vue真垃圾,有bug。
其實(shí)這些霧水大都是上面的2、3兩點(diǎn)引發(fā)的,vue也都提供了解決方案:$set
和$delete
,我都整理好了,需要理解的直接移步深入響應(yīng)式原理。
但是,這就體驗(yàn)極差
??小故事一則:去年還沒(méi)閱讀源碼的時(shí)候,公司一個(gè)大版本的發(fā)布后,出現(xiàn)了一個(gè)不是很?chē)?yán)重,卻影響使用范圍很廣的一個(gè)bug,我們從凌晨2點(diǎn)修到4點(diǎn),最后還是一個(gè)大牛搞了幾輪實(shí)驗(yàn)發(fā)現(xiàn)了問(wèn)題,說(shuō)vue有bug,某某地方賦值需要用$set
。沒(méi)錯(cuò),就是上面痛點(diǎn)里的第3點(diǎn)。原因還是我們太菜呀,沒(méi)有閱讀相關(guān)源碼。
2.2 vue3.x的實(shí)現(xiàn)方案
文章開(kāi)頭我就強(qiáng)調(diào)了:數(shù)據(jù)響應(yīng)式是一種機(jī)制,一種數(shù)據(jù)變化的偵測(cè)機(jī)制。而實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式這種機(jī)制的方法不唯一。于是乎,vue3.x
來(lái)了,他帶著vue2.x
痛點(diǎn)的解決方案來(lái)了!
解決方案其實(shí)一點(diǎn)也不神秘,在ES6
之后,出現(xiàn)了一個(gè)新的特性:Proxy
。Vue3.x
在使用了Proxy
之后,痛點(diǎn)們一下子就全都解決了。Proxy
是怎么解決的呢?請(qǐng)聽(tīng)下回...請(qǐng)繼續(xù)往下看哈看完手寫(xiě)reactive
之后,就全都明白啦。
順便給個(gè)Proxy
的MDN地址: Proxy MDN傳松門(mén)
3、手寫(xiě)reactive
在vue3.x中,定義響應(yīng)式對(duì)象的方法如下:
const obj = reactive({
name: 'chenjing',
age: 18
})
3.1 測(cè)試Proxy是否生效
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('target, key', target, key, target[key])
return target[key]
})
}
ok,生效。在簡(jiǎn)易版的
reactive
,我們要添加基本的屬性get
、set
和deleteProperty
。同時(shí),在上面代碼的get
里直接return target[key]
,一來(lái)不太優(yōu)雅、二來(lái)可能報(bào)錯(cuò)。我們先來(lái)看看vue3是怎么處理的:再來(lái)一個(gè)傳送門(mén):Reflect - MDN
Reflect 是一個(gè)內(nèi)置的對(duì)象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
Reflect
不是一個(gè)函數(shù)對(duì)象,因此它是不可構(gòu)造的。
與大多數(shù)全局對(duì)象不同Reflect
并非一個(gè)構(gòu)造函數(shù),所以不能通過(guò)new運(yùn)算符對(duì)其進(jìn)行調(diào)用,或者將Reflect
對(duì)象作為一個(gè)函數(shù)來(lái)調(diào)用。Reflect
的所有屬性和方法都是靜態(tài)的(就像Math
對(duì)象)。
Reflect
對(duì)象提供了以下靜態(tài)方法,這些方法與proxy handler methods的命名相同.
其中的一些方法與Object
相同, 盡管二者之間存在 某些細(xì)微上的差別 .
3.2 reactive基本形態(tài)
讓我們來(lái)學(xué)習(xí)一下vue3
的寫(xiě)法后,加上了Reflect
后,于是我們最基本的reactive
就是下面這樣的:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key) // 可以直接return target[key],避免報(bào)錯(cuò)和代碼的優(yōu)雅性,模仿源碼采用Reflect
console.log('get', key)
return (typeof res === 'object') ? reactive(res) : res // 子屬性若是對(duì)象 需要再次代理
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
通過(guò)跑腳本后的控制臺(tái),可以看到訪問(wèn)屬性成功的觸發(fā)了
get
。同時(shí)新增屬性也觸發(fā)了set
。到這里為止,
vue2
中的數(shù)據(jù)響應(yīng)式在vue3
里其實(shí)已經(jīng)完全實(shí)現(xiàn)了。回過(guò)頭來(lái)想想,是不是沒(méi)那么難理解了吧。沒(méi)有vue2
的循環(huán)遍歷遞歸,只是上了Proxy
的車(chē)當(dāng)然了在
Vue3
內(nèi)真正的實(shí)現(xiàn),肯定不是這么幾行代碼就搞定的。只是響應(yīng)式的原理就是利用了Proxy
!
既然要手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的reactive
函數(shù),讓我們繼續(xù)往下閱讀。
目前只是想簡(jiǎn)單理解vue3
數(shù)據(jù)響應(yīng)式原理,了解vue3
數(shù)據(jù)響應(yīng)和vue2
數(shù)據(jù)響應(yīng)的區(qū)別的同學(xué)可以直接點(diǎn)贊了哈哈,鼓勵(lì)一下互相學(xué)習(xí)進(jìn)步??
3.3 依賴(lài)的收集、觸發(fā)
既然要手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的reactive
函數(shù),我們就繼續(xù)。
要實(shí)現(xiàn)reactive
函數(shù),我們就要在get
內(nèi)進(jìn)行依賴(lài)收集,在set
中進(jìn)行觸發(fā)。即便是vue2
也是通過(guò)類(lèi)似的發(fā)布訂閱模式體現(xiàn)。在這里,我們也是通過(guò)發(fā)布訂閱模式去完成。
首先是依賴(lài)收集:在get內(nèi),我們需要對(duì)依賴(lài)進(jìn)行收集。在依賴(lài)收集的時(shí)候,將其按照依賴(lài)關(guān)系放入map中映射。
然后就是依賴(lài)觸發(fā):在set中,需要觸發(fā)響應(yīng)式函數(shù)。即完成了發(fā)布訂閱。
下面代碼 有需要的可以直接復(fù)制粘貼,直接跑。可以自行斷點(diǎn)看看,有疑問(wèn)的歡迎交流。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key)
console.log('get', key)
// 依賴(lài)收集
track(target, key)
return (typeof res === 'object') ? reactive(res) : res
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
// 觸發(fā)
trigger(target, key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
// 保存副作用函數(shù)
const effectStack = []
// 添加副作用函數(shù)
function effect (fn) {
const e = createReactiveEffect(fn)
// 立即執(zhí)行
e()
return e
}
function createReactiveEffect(fn) {
// 封裝fn,處理其錯(cuò)誤,執(zhí)行之,存放到stack
const effect = () => {
try {
// 0入棧
effectStack.push(effect)
// 1 執(zhí)行fn
return fn()
} finally {
// 2 出棧
effectStack.pop
}
}
return effect
}
// 保存映射關(guān)系的數(shù)據(jù)結(jié)構(gòu)
const targetMap = new WeakMap()
// 當(dāng)副作用函數(shù)觸發(fā)響應(yīng)式數(shù)據(jù)之后,執(zhí)行track,進(jìn)項(xiàng)依賴(lài)收集工作
// 目標(biāo)是將target, key和前面effectStack中的副作用函數(shù)之間建立映射關(guān)系
function track (target, key) {
// 1.先拿出響應(yīng)函數(shù)
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 獲取target對(duì)應(yīng)的map
let depMap = targetMap.get(target)
if (!depMap) {
// 初始化的時(shí)候 depMap不存在 初始化一次
depMap = new Map()
targetMap.set(target, depMap)
}
// 從depMap中 獲取對(duì)應(yīng)的set
let deps = depMap.get(key)
if (!deps) {
// 初始化需要?jiǎng)?chuàng)建一個(gè)Set
deps = new Set()
depMap.set(key, deps)
}
// 將副作用函數(shù)放到集合中
deps.add(effect)
}
}
// 觸發(fā)響應(yīng)式函數(shù)
function trigger (target, key) {
// 從targetMap中獲取對(duì)應(yīng)副作用函數(shù)集合
// 1. 獲取target對(duì)應(yīng)的map
const depMap = targetMap.get(target)
if (!depMap) return
// 根據(jù)key獲取對(duì)應(yīng)的deps
const deps = depMap.get(key)
if (deps) {
// 遍歷執(zhí)行他們
deps.forEach(dep => dep())
}
}
const obj = reactive({
name: 'chenjing',
age: 18,
look: {
height: '180cm'
}
})
effect(() => {
console.log('effect1', obj.name)
})
effect(() => {
console.log('effect2', obj.name, obj.look.height)
})
setTimeout(() => {
console.log('---- 分割線 -----')
obj.name = 'jay'
obj.look.height = '178cm'
}, 1000)
4. 結(jié)尾
好了,到此手寫(xiě)簡(jiǎn)易版vue3的reactive函數(shù)完成,希望可以幫助到打擊愛(ài)理解vue3數(shù)據(jù)響應(yīng)原理。
單純的理解數(shù)據(jù)響應(yīng)原理可以理解到Proxy就差不多了
后面依賴(lài)收集觸發(fā)就是具體到響應(yīng)后要做的事。