這篇文章帶大家學習vue,聊聊vue如何處理響應式資料?希望對大家有幫助!
關於vue的響應式數據,你可能有很多疑惑。
例如為什麼要改用proxy?
例如為什麼有reactive和ref兩個api?
例如vue是如何實現響應式的?
其實這些在原始碼中,都能找到答案。
在第一篇vue3有哪些升級中,我也提了一下使用proxy的好處,以及Object.defineProperty的缺陷。但今天,我想換個角度,讓chatGPT來回答這個問題。 【相關推薦:vuejs影片教學、web前端開發】
#嗯,我感覺比我總結的好。
那繼續下一個問題。
這個回答感覺就比較官方了,沒有回答到我想要的答案。
原因其實很簡單,因為proxy方法不能代理值型,只能代理物件。所以需要一個額外的方法,來處理值類型的資料。
當然你也可以一個ref走天下,vue原始碼做了相容會自動進行轉換
有了這些理解之後,我們來看今天的重頭戲,自己仿照vue3源碼,實作一個響應式系統。
相關程式碼可以點擊查看,根據文章的標題選擇不同的分支即可。
在前文我們也介紹了,reactive其實實用的是proxy代理物件。
我們可以使用proxy實作一個簡單的代理函數reactive。
function reactive(target) { const isObject = (val) => val !== null && typeof val === 'object' if (!isObject(target)) { console.warn(`数据必须是对象: ${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) // 这句很关键,不然嵌套数据,里面的不会响应 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'复制代码
注意:Reflect
Reflect.get(target, key)跟直接存取target[key]是有一點差別的。
當代理物件中,有get,set等this指向的時候,可以重定向this。
舉例:
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)复制代码
使用Reflect,我們在存取name, age的時候均可以觸發。
改成target之後,則只會在info觸發一次。
return target[key]复制代码
在vue3中,ref是透過get和set來實現的。
跟上面類似,還是先宣告一個函數,然後透過get,set來存取資料。
function ref(value) { return new RefImpl(value) } class RefImpl { constructor(value) { // 如果值是对象,则用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)) { // 更新原始数据 this._rawValue = newVal // 更新 .value 的值 this._value = isObject(newVal) ? reactive(newVal) : value // triggerRefValue(this) } } }复制代码
原始碼也很直觀的解釋了,為什麼ref的使用一定要用.value的方式使用,因為get/set就是透過value來設定的。
資料代理程式我們已經完成了,但是資料變更之後,怎麼通知元件實作雙向綁定呢?
答案就是依賴收集與觸發,也就是當觸發get的時候,我把觸發get的這個條件【函數】給保存起來,當觸發set的時候,重新執行一下觸發這個條件【函數】不就行了嗎。
還是來看程式碼,我加一個track收集的方法,加一個target觸發的方法。 (也就是上面程式碼片段註解掉的兩行程式碼)
除此之外,還需要一個effect函數來管理觸發函數。
let activeEffect = null const targetMap = new WeakMap() // 依赖收集/触发 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)复制代码
activeEffect用來儲存觸發的條件函數。
targetMap用來存放依賴字典,格式如下
{ target: { key: [] } }复制代码
輸出結果為hello world,2秒之後,重新執行依賴性函數,輸出setTimeout world
資料的代理程式並不複雜,就是在proxy的基礎上,加上一些邊界處理。而要實現響應式則需要加上依賴收集,依賴觸發的實作。
effect是一個很重要的函數,很多api是基於這個函數開發的,像是useEffect, watch。組件的更新也是基於effect函數,這個後續還會提到。
如果不太理解effect,可以梳理一下執行順序。
相關程式碼可以點選檢視,選擇lesson3分支即可。
文章相關程式碼,可在vue/examples中查看,仿vue實作版本可在packages/reactivity中查看。
以上是深入淺析vue處理響應式資料的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!