網(wǎng)站首頁 編程語言 正文
Vue和MobX中的數(shù)據(jù)可響應(yīng)給我們留下了深刻的印象,在React函數(shù)組件中我們也可以依賴hooks來實(shí)現(xiàn)一個簡易好用的useReactive。
看一下我們的目標(biāo)
const CountDemo = () => { const reactive = useReactive({ count: 0, }); return ( <div onClick={() => { reactive.count++; }} > {reactive.count} </div> ); };
簡單來說就是我們不需要再手動觸發(fā)setState的handler了,修改數(shù)據(jù),組件中的數(shù)據(jù)就會直接更新。
在Vue中我們實(shí)現(xiàn)數(shù)據(jù)可響應(yīng)概括來講需要:
1.解析模板收集依賴
2.發(fā)布訂閱實(shí)現(xiàn)更新
而React函數(shù)組件憑借函數(shù)的特性這個過程將更加簡單,因?yàn)楹瘮?shù)組件每一次render都會重新"執(zhí)行"一遍,我們只需要改變數(shù)據(jù)之后再觸發(fā)組件渲染就能達(dá)到我們的目的。
因此實(shí)現(xiàn)這個自定義hook的核心就是:
1.維護(hù)同一份數(shù)據(jù)
2.劫持對數(shù)據(jù)的操作
3.在劫持操作中觸發(fā)組件更新 (setState)
使用Proxy代理數(shù)據(jù)
這個代理模式是實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)的核心。Vue2.0 中使用defineProperty來做數(shù)據(jù)劫持,現(xiàn)在則是被Proxy模式所替代了,一句話概括defineProperty和proxy的區(qū)別就是前者劫持的是屬性訪問器,而后者可以代理整個對象(Vue3.0,MobX)。
Proxy有多達(dá)13種攔截器,我們這次用到的有 get
, set
, delete
const observer = (initialState, cb) => { const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 遞歸處理object類型 }, set(target, key, val) { const ret = Reflect.set(target, key, val); cb(); return ret; }, deleteProperty(target, key) { const ret = Reflect.deleteProperty(target, key); cb(); return ret; }, }); return proxy; };
上面這個observer完成了對數(shù)據(jù)的基本操作代理。
這里補(bǔ)充一個知識點(diǎn): 為什么Proxy代理的對象經(jīng)常搭配Reflect而不是操作符訪問?
Reflect
更加全面,功能更強(qiáng)大:
- 只要Proxy對象具有的代理方法,Reflect對象全部具有,以靜態(tài)方法的形式存在。這些方法能夠執(zhí)行默認(rèn)行為,無論 Proxy 怎么修改默認(rèn)行為,總是可以通過 Reflect 對應(yīng)的方法獲取默認(rèn)行為。
比如上文第4行這里 Reflect.get(target,key,receiver)
咋一看似乎可以和target[key]
等價,但實(shí)際上不是的看下面的例子,正是由于Reflect的靜態(tài)方法的第三個參數(shù)receiver可以用來指定被調(diào)用時的this,所以使用 Reflect.get(target,key,receiver)
才能如我們預(yù)期返回正確結(jié)果。
let o = { getb() { return this.a; }, }; let o1 = Object.create( newProxy(o, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, }) ); o1.a = 42; o1.b; // 42 let o2 = Object.create( newProxy(o, { get(target, key) { return target[key]; }, }) ); o2.a = 42; o2.b; // undefined
- 修改某些Object方法的返回結(jié)果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
- 讓Object操作都變成函數(shù)行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數(shù)行為。
const useReactive = (initState) => { return observer(initState); };
我們的基本結(jié)構(gòu)大概如上面代碼段所示,但是這里有兩個問題 :
1.我們希望函數(shù)組件每次 執(zhí)行
的時候它都引用同一個代理對象
2.在組件的生命周期里observer只需要代理一次
使用useRef創(chuàng)建同一份數(shù)據(jù)引用
看到維護(hù)同一份數(shù)據(jù)我們第一反應(yīng)可能就是使用閉包來創(chuàng)建引用,但是如此就還需要我們手動維護(hù)組件的創(chuàng)建卸載和這份數(shù)據(jù)的關(guān)系,而React中天生就包含了ref
這樣的api,所以我們不需要從自行管理數(shù)據(jù)的卸載和綁定,在函數(shù)組件中直接使用useRef就可以達(dá)到我們的目的。
const useReactive = (initState) => { const ref = useRef(initState); return observer(ref.current); };
這樣子我們就使用useRef和Proxy實(shí)現(xiàn)了對initialState的代理
添加更新handler
我們發(fā)現(xiàn)還少了一個handler即數(shù)據(jù)更改后觸發(fā)組件更新,其實(shí)到這一步就比較簡單了只需要在操作ref的值之后setState一下就可以了。
因?yàn)槭窃诤瘮?shù)組件內(nèi)部所以我們可以直接借用useState引入一個“更新觸發(fā)器”,并將這個觸發(fā)器傳入observer代理方法。
function useReactive<S extends object>(initialState: S): S { const [, setFlag] = useState({}); const ref = useRef < S > (initialState) return observer(ref.current, () => { setFlag({}); // {} !== {} 因此會觸發(fā)組件更新 }); }
去除多次Proxy
在完成上面幾個步驟之后,我們基本已經(jīng)可以實(shí)現(xiàn)開頭demo中的效果了,但是還有一個問題:
由于是函數(shù)組件在state更新之后useReactive
也會執(zhí)行,因此observer
就會被多次執(zhí)行,而我們的預(yù)期,這個代理行為應(yīng)該只在組件創(chuàng)建之初執(zhí)行一次就可以了,因此這里我們也需要進(jìn)行一些改造,方法依然是依靠 ref在函數(shù)組件多次執(zhí)行時返回同一份數(shù)據(jù)這個特點(diǎn):
function useReactive(initialState) { const refState = useRef(initialState); const [, setUpdate] = useState({}); const refProxy = useRef({ data: null, initialized: false, }); // 在創(chuàng)建proxy的ref時我們加一個initialized標(biāo)志位,這樣當(dāng)組件state更新執(zhí)行時 // useReactive再次執(zhí)行就可以根據(jù)這個標(biāo)志位來決定是直接返回current上的data值還是重新執(zhí)行proxy了 if (refProxy.current.initialized === false) { refProxy.current.data = observer(refState.current, () => { setUpdate({}); }); refProxy.current.initialized = true; return refProxy.current.data; } return refProxy.current.data; }
添加緩存完善代碼
上面解決了函數(shù)組件更新方式所帶來的重復(fù)執(zhí)行問題,這里還需要解決外部操作導(dǎo)致的重復(fù)代理,即如果一個initialState已經(jīng)被代理過了,那么我們是不希望它被二次代理的(用戶可能使用了兩次useReactive來代理同一個對象),我們可以使用 WeakMap
來進(jìn)行緩存記錄
const proxyMap = new WeakMap(); const observer = (initialState, cb) => { const existing = proxyMap.get(initialState); // 添加緩存 防止重新構(gòu)建proxy if (existing) { return existing; } const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 遞歸處理object類型 }, set(target, key, val) { const ret = Reflect.set(target, key, val); cb(); return ret; }, deleteProperty(target, key) { const ret = Reflect.deleteProperty(target, key); cb(); return ret; }, }); proxyMap.set(initialState, proxy); return proxy; };
總結(jié)
至此我們的useReactive
就基本可用了,回顧一下全部代碼:
const proxyMap = new WeakMap(); const observer = (initialState, cb) => { const existing = proxyMap.get(initialState); if (existing) return existing; const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 遞歸處理object類型 }, set(target, key, val) { const ret = Reflect.set(target, key, val); cb() return ret; }, deleteProperty(target, key) { const ret = Reflect.deleteProperty(target, key); cb(); return ret; }, }); return proxyMap.set(initialState, proxy) && proxy; }; function useReactive(initialState) { const refState = useRef(initialState); const [, setUpdate] = useState({}); const refProxy = useRef({ data: null, initialized: false, }); if (refProxy.current.initialized === false) { refProxy.current.data = observer(refState.current, () => { setUpdate({}); }); refProxy.current.initialized = true; return refProxy.current.data; } return refProxy.current.data; }
Sandbox 示例
https://codesandbox.io/s/silly-haze-xfuxoy?file=/src/App.js
代碼雖少但五臟俱全,上面這個useReactive實(shí)現(xiàn)方式幾乎和ahooks中的useReactive一致,這個包里還包含了很多其他簡單有用的hooks集合,感興趣的朋友可以了解一下其他hooks的實(shí)現(xiàn),輔助你業(yè)務(wù)開發(fā)的同時幫助你加深對 React 工作原理的理解。
原文鏈接:https://juejin.cn/post/7090061596445114399
相關(guān)推薦
- 2022-11-12 CSS單標(biāo)簽實(shí)現(xiàn)復(fù)雜的棋盤布局_經(jīng)驗(yàn)交流
- 2022-05-27 C++?超詳細(xì)快速掌握二叉搜索樹_C 語言
- 2022-07-12 oracle?指定類型和指定位數(shù)創(chuàng)建序列號的代碼詳解_oracle
- 2022-09-26 Linux查看操作系統(tǒng)的版本,關(guān)閉防火墻,主機(jī)名配置,時區(qū)時間配置
- 2022-10-16 Python?讀取?Word?文檔操作_python
- 2022-02-10 Error: Cannot find module ‘webpack/lib/RuleSet‘ 解決
- 2022-12-30 Python利用tkinter和socket實(shí)現(xiàn)端口掃描_python
- 2023-01-02 Kotlin?fun函數(shù)使用方法_Android
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支