本篇文章帶大家學(xué)習(xí)vue,聊聊vue如何處理響應(yīng)式數(shù)據(jù)?希望對(duì)大家有所幫助!
關(guān)于vue的響應(yīng)式數(shù)據(jù),你可能有很多疑惑。
比如為什么要改用proxy?
(資料圖片僅供參考)
比如為什么有reactive和ref兩個(gè)api?
比如vue是如何實(shí)現(xiàn)響應(yīng)式的?
其實(shí)這些在源碼中,都能找到答案。
在第一篇vue3有哪些升級(jí)中,我也提了一下使用proxy的好處,以及Object.defineProperty的缺陷。但是今天,我想換個(gè)角度,讓chatGPT來(lái)回答一下這個(gè)問(wèn)題?!鞠嚓P(guān)推薦:vuejs視頻教程、web前端開(kāi)發(fā)】
嗯,我感覺(jué)比我總結(jié)的好。
那繼續(xù)下一個(gè)問(wèn)題。
這個(gè)回答感覺(jué)就比較官方了,沒(méi)有回答到我想要的答案。
理由其實(shí)很簡(jiǎn)單,因?yàn)閜roxy方法不能代理值類型,只能代理對(duì)象。所以需要一個(gè)額外的方法,來(lái)處理值類型的數(shù)據(jù)。
當(dāng)然你也可以一個(gè)ref走天下,vue源碼做了兼容會(huì)自動(dòng)進(jìn)行轉(zhuǎn)換
有了這些理解之后,我們來(lái)看今天的重頭戲,自己仿照vue3源碼,實(shí)現(xiàn)一個(gè)響應(yīng)式系統(tǒng)。
相關(guān)代碼可以點(diǎn)擊查看,根據(jù)文章的標(biāo)題選擇不同的分支即可。
在前文我們也介紹了,reactive其實(shí)用的是proxy代理對(duì)象。
我們可以使用proxy實(shí)現(xiàn)一個(gè)簡(jiǎn)單的代理函數(shù)reactive。
function reactive(target) { const isObject = (val) => val !== null && typeof val === "object" if (!isObject(target)) { console.warn(`數(shù)據(jù)必須是對(duì)象: ${String(target)}`) return target } const proxy = new Proxy(target, { get: (target, key, receiver) => { console.log("get", key) const res = Reflect.get(target, key, receiver) // track(target, key) // 這句很關(guān)鍵,不然嵌套數(shù)據(jù),里面的不會(huì)響應(yīng) if (isObject(res)) { return reactive(res) } return res }, set: (target, key, value, receiver) => { console.log("set", key, value) const result = Reflect.set(target, key, value, receiver) // trigger(target, key, value) return result }, deleteProperty: () => { const result = Reflect.deleteProperty(target, key) return result } }) return proxy } const person = reactive({ name: "", age: 18, like: ["apple"] }) person.name = "vue test"復(fù)制代碼注意:Reflect
Reflect.get(target, key)跟直接訪問(wèn)target[key]是有一點(diǎn)差別的。
當(dāng)代理對(duì)象中,有g(shù)et,set等this指向的時(shí)候,可以重定向this。
舉個(gè)例子:
const person = new Proxy({ name: "vue test", age: 18, get info() { return "name is :" + this.name + " age is:" + this.age } }, { get: (target, key, receiver) => { console.log("get", key) // return target[key] return Reflect.get(target, key, receiver) } }) console.log(person.info)復(fù)制代碼使用Reflect,我們?cè)谠L問(wèn)name, age的時(shí)候均可以觸發(fā)。
改成target之后,則只會(huì)在info觸發(fā)一次。
return target[key]復(fù)制代碼
在vue3中,ref是通過(guò)get和set來(lái)實(shí)現(xiàn)的。
跟上面類似,也還是先聲明一個(gè)函數(shù),然后通過(guò)get,set來(lái)訪問(wèn)數(shù)據(jù)。
function ref(value) { return new RefImpl(value) } class RefImpl { constructor(value) { // 如果值是對(duì)象,則用reactive處理 this._value = isObject(value) ? reactive(value) : value // 記錄一下初始值 this._rawValue = value } get value() { // trackRefValue(this) return this._value } set value(newVal) { if (!Object.is(newVal, this._rawValue)) { // 更新原始數(shù)據(jù) this._rawValue = newVal // 更新 .value 的值 this._value = isObject(newVal) ? reactive(newVal) : value // triggerRefValue(this) } } }復(fù)制代碼源碼也很直觀的解釋了,為什么ref的使用一定要用.value的方式使用,因?yàn)間et/set就是通過(guò)value來(lái)設(shè)置的。
數(shù)據(jù)代理我們已經(jīng)完成了,但是數(shù)據(jù)更改之后,怎么通知組件實(shí)現(xiàn)雙向綁定呢?
答案就是依賴收集與觸發(fā),也就是當(dāng)觸發(fā)get的時(shí)候,我把觸發(fā)get的這個(gè)條件【函數(shù)】給保存起來(lái),當(dāng)觸發(fā)set的時(shí)候,重新執(zhí)行一下觸發(fā)一下這個(gè)條件【函數(shù)】不就行了嗎。
還是來(lái)看代碼,我加一個(gè)track收集的方法,加一個(gè)target觸發(fā)的方法。(也就是上面代碼片段注釋掉的兩行代碼)
除此之外,還需要一個(gè)effect函數(shù)來(lái)管理觸發(fā)函數(shù)。
let activeEffect = null const targetMap = new WeakMap() // 依賴收集/觸發(fā) function track(target, key) { let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { dep = new Set() } dep.add(activeEffect) depsMap.set(key, dep) } function trigger(target, key, value) { const depsMap = targetMap.get(target) if (!depsMap) { return } const deps = depsMap.get(key) if (!deps) { return } deps.forEach(effectFn => { if (effectFn.scheduler) { effectFn.scheduler() } else { effectFn() } }) } function effect(fn,options = {}) { const effectFn = () => { try { activeEffect = effectFn return fn() } catch (error) { activeEffect = null } } if (!options.lazy) { effectFn() } effectFn.scheduler = options.scheduler return effectFn } const person = reactive({ name: "hello world" }) effect(() => { console.log("effect person", person.name) }) setTimeout(() => { person.name = "setTimeout world" }, 2000)復(fù)制代碼activeEffect用來(lái)存觸發(fā)的條件函數(shù)。
targetMap用來(lái)存放依賴字典,格式如下
{ target: { key: [] }}復(fù)制代碼輸出結(jié)果為hello world,2秒之后,重新執(zhí)行依賴性函數(shù),輸出setTimeout world
數(shù)據(jù)的代理并不復(fù)雜,就是在proxy的基礎(chǔ)上,加上一些邊界處理。而要實(shí)現(xiàn)響應(yīng)式則需要加上依賴收集,依賴觸發(fā)的實(shí)現(xiàn)。
effect是一個(gè)很重要的函數(shù),很多api是基于這個(gè)函數(shù)開(kāi)發(fā)的,比如useEffect, watch。組件的更新也是基于effect函數(shù),這個(gè)后續(xù)還會(huì)提到。
如果不太理解effect,可以梳理一下執(zhí)行順序。
1.調(diào)用effect函數(shù),并傳入?yún)?shù)fn2.申明一個(gè)effectFn函數(shù),并執(zhí)行,將函數(shù)保存為activeEffect3.執(zhí)行fn,讀取person.name4.走proxy的get代理5.收集依賴,將activeEffect保存的函數(shù)存到全局map中6.此時(shí),effect函數(shù)執(zhí)行完畢,等待數(shù)據(jù)更新7.2s后,走proxy的set代理8.執(zhí)行全局map中保存的函數(shù),重新執(zhí)行effect函數(shù),再次回到步驟1相關(guān)代碼可以點(diǎn)擊查看,選擇lesson3分支即可。
文章相關(guān)代碼,可在vue/examples中查看,仿vue實(shí)現(xiàn)版本可在packages/reactivity中查看。
(學(xué)習(xí)視頻分享:vuejs入門教程、編程基礎(chǔ)視頻)
以上就是深入淺析vue處理響應(yīng)式數(shù)據(jù)的方法的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!
關(guān)鍵詞: