深入理解Vue3響應(yīng)式原理
響應(yīng)式原理
利用ES6中Proxy作為攔截器,在get時(shí)收集依賴,在set時(shí)觸發(fā)依賴,來(lái)實(shí)現(xiàn)響應(yīng)式。
手寫實(shí)現(xiàn)
1、實(shí)現(xiàn)Reactive
基于原理,我們可以先寫一下測(cè)試用例
//reactive.spec.ts describe("effect", () => { it("happy path", () => { const original = { foo: 1 }; //原始數(shù)據(jù) const observed = reactive(original); //響應(yīng)式數(shù)據(jù) expect(observed).not.toBe(original); expect(observed.foo).toBe(1); //正常獲取數(shù)據(jù) expect(isReactive(observed)).toBe(true); expect(isReactive(original)).toBe(false); expect(isProxy(observed)).toBe(true); }); });
首先實(shí)現(xiàn)數(shù)據(jù)的攔截處理,通過(guò)ES6的Proxy,實(shí)現(xiàn)獲取和賦值操作。
//reactive.ts //對(duì)new Proxy()進(jìn)行包裝 export function reactive(raw) { return createActiveObject(raw, mutableHandlers); } function createActiveObject(raw: any, baseHandlers) { //直接返回一個(gè)Proxy對(duì)象,實(shí)現(xiàn)響應(yīng)式 return new Proxy(raw, baseHandlers); }
//baseHandler.ts //抽離出一個(gè)handler對(duì)象 export const mutableHandlers = { get:createGetter(), set:createSetter(), }; function createGetter(isReadOnly: Boolean = false, shallow: Boolean = false) { return function get(target, key) { const res = Reflect.get(target, key); // 看看res是否是一個(gè)object if (isObject(res)) { //如果是,則進(jìn)行嵌套處理,使得返回的對(duì)象中的 對(duì)象 也具備響應(yīng)式 return isReadOnly ? readonly(res) : reactive(res); } if (!isReadOnly) { //如果不是readonly類型,則收集依賴 track(target, key); } return res; }; } function createSetter() { return function set(target, key, value) { const res = Reflect.set(target, key, value); //觸發(fā)依賴 trigger(target, key); return res; }; }
從上述代碼中,我們可以??注意到track(target, key) 和trigger(target, key) 這兩個(gè)函數(shù),分別是對(duì)依賴的收集和觸發(fā)。
依賴:我們可以把依賴認(rèn)為是把用戶對(duì)數(shù)據(jù)的操控(用戶函數(shù),副作用函數(shù))包裝成一個(gè)東西,我們?cè)趃et的時(shí)候?qū)⒁蕾囈粋€(gè)一個(gè)收集起來(lái),set的時(shí)候全部觸發(fā),即可實(shí)現(xiàn)響應(yīng)式效果。
2、實(shí)現(xiàn)依賴的收集和觸發(fā)
//effect.ts //全局變量 let activeEffect: ReactiveEffect; //當(dāng)前的依賴 let shouldTrack: Boolean; //是否收集依賴 const targetMap = new WeakMap(); //依賴樹
targetMap結(jié)構(gòu):
targetMap: {
每一個(gè)target(depsMap):{
每一個(gè)key(depSet):[
每一個(gè)依賴
]
}
}
WeakMap和Map的區(qū)別
1、WeakMap只接受對(duì)象作為key,如果設(shè)置其他類型的數(shù)據(jù)作為key,會(huì)報(bào)錯(cuò)。
2、WeakMap的key所引用的對(duì)象都是弱引用,只要對(duì)象的其他引用被刪除,垃圾回收機(jī)制就會(huì)釋放該對(duì)象占用的內(nèi)存,從而避免內(nèi)存泄漏。
3、由于WeakMap的成員隨時(shí)可能被垃圾回收機(jī)制回收,成員的數(shù)量不穩(wěn)定,所以沒有size屬性。
4、沒有clear()方法
5、不能遍歷
首先我們定義一個(gè)依賴類,稱為ReactiveEffect,對(duì)用戶函數(shù)進(jìn)行包裝,賦予一些屬性和方法。參考:前端手寫面試題詳細(xì)解答
//effect.ts //響應(yīng)式依賴 — ReactiveEffect類 class ReactiveEffect { private _fn: any; //用戶函數(shù), active = true; //表示當(dāng)前依賴是否激活,如果清除過(guò)則為false deps: any[] = []; //包含該依賴的deps onStop?: () => void; //停止該依賴的回調(diào)函數(shù) public scheduler: Function; //調(diào)度函數(shù) //構(gòu)造函數(shù) constructor(fn, scheduler?) { this._fn = fn; this.scheduler = scheduler; } //執(zhí)行副作用函數(shù) run() { //用戶函數(shù),可以報(bào)錯(cuò),需要用try包裹 try { //如果當(dāng)前依賴不是激活狀態(tài),不進(jìn)行依賴收集,直接返回 if (!this.active) { return this._fn(); } //開啟依賴收集 shouldTrack = true; activeEffect = this; //調(diào)用時(shí)會(huì)觸發(fā)依賴收集 const result = this._fn(); //關(guān)閉依賴收集 shouldTrack = false; //返回結(jié)果 return result; } finally { //todo } } }
effect影響函數(shù)
創(chuàng)建一個(gè)用戶函數(shù)作用函數(shù),稱為effect,這個(gè)函數(shù)的功能為基于ReactiveEffect類創(chuàng)建一個(gè)依賴,觸發(fā)用戶函數(shù)(的時(shí)候,觸發(fā)依賴收集),返回用戶函數(shù)。
//創(chuàng)建一個(gè)依賴 export function effect(fn, option: any = {}) { //為當(dāng)前的依賴創(chuàng)建響應(yīng)式實(shí)例 const _effect = new ReactiveEffect(fn, option.scheduler); Object.assign(_effect, option); //最開始調(diào)用一次,其中會(huì)觸發(fā)依賴收集 _effect.run() -> _fn() -> get() -> track() _effect.run(); const runner: any = _effect.run.bind(_effect); //在runner上掛載依賴,方便在其他地方通過(guò)runner訪問(wèn)到該依賴 runner.effect = _effect; return runner; }
bind():在原函數(shù)的基礎(chǔ)上創(chuàng)建一個(gè)新函數(shù),使新函數(shù)的this指向傳入的第一個(gè)參數(shù),其他參數(shù)作為新函數(shù)的參數(shù)
用戶觸發(fā)依賴收集時(shí),將依賴添加到targetMap中。
收集/添加依賴
//把依賴添加到targetMap對(duì)應(yīng)target的key中,在重新set時(shí)在trigger中重新觸發(fā) export function track(target: Object, key) { //如果不是track的狀態(tài),直接返回 if (!isTracking()) return; // target -> key -> dep //獲取對(duì)應(yīng)target,獲取不到則創(chuàng)建一個(gè),并加進(jìn)targetMap中 let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } //獲取對(duì)應(yīng)key,獲取不到則創(chuàng)建一個(gè),并加進(jìn)target中 let depSet = depsMap.get(key); if (!depSet) { depsMap.set(key, (depSet = new Set())); } //如果depSet中已經(jīng)存在該依賴,直接返回 if (depSet.has(activeEffect)) return; //添加依賴 trackEffects(depSet); } export function trackEffects(dep) { //往target中添加依賴 dep.add(activeEffect); //添加到當(dāng)前依賴的deps數(shù)組中 activeEffect.deps.push(dep); }
觸發(fā)依賴
//一次性觸發(fā)對(duì)應(yīng)target中key的所有依賴 export function trigger(target, key) { let depsMap = targetMap.get(target); let depSet = depsMap.get(key); //觸發(fā)依賴 triggerEffects(depSet); } export function triggerEffects(dep) { for (const effect of dep) { if (effect.scheduler) { effect.scheduler(); } else { effect.run(); } } }
3、移除/停止依賴
我們?cè)赗eactiveEffect這個(gè)類中,增加一個(gè)stop方法,來(lái)暫停依賴收集和清除已經(jīng)存在的依賴
//響應(yīng)式依賴 — 類 class ReactiveEffect { private _fn: any; //用戶函數(shù), active = true; //表示當(dāng)前依賴是否激活,如果清除過(guò)則為false deps: any[] = []; //包含該依賴的deps onStop?: () => void; //停止該依賴的回調(diào)函數(shù) public scheduler: Function; //調(diào)度函數(shù) //... stop() { if (this.active) { cleanupEffect(this); //執(zhí)行回調(diào) if (this.onStop) { this.onStop(); } //清除激活狀態(tài) this.active = false; } } } //清除該依賴掛載的deps每一項(xiàng)中的該依賴 function cleanupEffect(effect) { effect.deps.forEach((dep: any) => { dep.delete(effect); }); effect.deps.length = 0; } //移除一個(gè)依賴 export function stop(runner) { runner.effect.stop(); }
衍生類型
1、實(shí)現(xiàn)readonly
readonly相比于reactive,實(shí)現(xiàn)上相對(duì)比較簡(jiǎn)單,它是一個(gè)只讀類型,不會(huì)涉及set操作,更不需要收集/觸發(fā)依賴。
export function readonly(raw) { return createActiveObject(raw, readonlyHandlers); } export const readonlyHandlers = { get: readonlyGet, set: (key, target) => { console.warn(`key:${key} set 失敗,因?yàn)閠arget是一個(gè)readonly對(duì)象`, target); return true; }, }; const readonlyGet = createGetter(true); function createGetter(isReadOnly: Boolean = false, shallow: Boolean = false) { return function get(target, key) { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadOnly; } else if (key === ReactiveFlags.IS_READONLY) { return isReadOnly; } //... // 看看res是否是一個(gè)object if (isObject(res)) { return isReadOnly ? readonly(res) : reactive(res); } ? if (!isReadOnly) { //收集依賴 track(target, key); } return res; }; }
2、實(shí)現(xiàn)shallowReadonly
我們先看一下shallow的含義
shallow:不深的, 淺的,不深的, 不嚴(yán)肅的, 膚淺的,淺薄的。
那么shallowReadonly,指的是只對(duì)最外層進(jìn)行限制,而內(nèi)部的仍然是一個(gè)普通的、正常的值。
//shallowReadonly.ts export function shallowReadonly(raw) { return createActiveObject(raw, shallowReadonlyHandlers); } export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet, }); const shallowReadonlyGet = createGetter(true, true); function createGetter(isReadOnly: Boolean = false, shallow: Boolean = false) { return function get(target, key) { //.. const res = Reflect.get(target, key); //是否shallow,是的話很直接返回 if (shallow) { return res; } if (isObject(res)) { //... } }; }
3、實(shí)現(xiàn)ref
ref相對(duì)reactive而言,實(shí)際上他不存在嵌套關(guān)系,就是一個(gè)value。
//ref.ts export function ref(value: any) { return new RefImpl(value); }
我們來(lái)實(shí)現(xiàn)一下RefImpl類,原理其實(shí)跟reactive類似,只是一些細(xì)節(jié)處不同。
//ref.ts class RefImpl { private _value: any; //轉(zhuǎn)化后的值 public dep; //依賴容器 private _rawValue: any; //原始值, public _v_isRef = true; //判斷ref類型 constructor(value) { this._rawValue = value; //記錄原始值 this._value = convert(value); //存儲(chǔ)轉(zhuǎn)化后的值 this.dep = new Set(); //創(chuàng)建依賴容器 } get value() { trackRefValue(this); //收集依賴 return this._value; } set value(newValue) { //新老值不同,才觸發(fā)更改 if (hasChanged(newValue, this._rawValue)) { // 一定先修改value,再觸發(fā)依賴 this._rawValue = newValue; this._value = convert(newValue); triggerEffects(this.dep); } } }
//ref.ts //對(duì)value進(jìn)行轉(zhuǎn)換(value可能是object) export function convert(value: any) { return isObject(value) ? reactive(value) : value; } export function trackRefValue(ref: RefImpl) { if (isTracking()) { trackEffects(ref.dep); } } //effect.ts export function isTracking(): Boolean { //是否開啟收集依賴 & 是否有依賴 return shouldTrack && activeEffect !== undefined; } export function trackEffects(dep) { dep.add(activeEffect); activeEffect.deps.push(dep); } export function triggerEffects(dep) { for (const effect of dep) { if (effect.scheduler) { effect.scheduler(); } else { effect.run(); } } }
實(shí)現(xiàn)proxyRefs
//實(shí)現(xiàn)對(duì)ref對(duì)象進(jìn)行代理 //如user = { // age:ref(10), // ... //} export function proxyRefs(ObjectWithRefs) { return new Proxy(ObjectWithRefs, { get(target, key) { // 如果是ref 返回.value //如果不是 返回value return unRef(Reflect.get(target, key)); }, set(target, key, value) { if (isRef(target[key]) && !isRef(value)) { target[key].value = value; return true; //? } else { return Reflect.set(target, key, value); } }, }); }
4、實(shí)現(xiàn)computed
computed的實(shí)現(xiàn)也很巧妙,利用調(diào)度器機(jī)制和一個(gè)私有變量_value,實(shí)現(xiàn)緩存和惰性求值。
通過(guò)注解(一)(二)(三)可理解其實(shí)現(xiàn)流程
//computed import { ReactiveEffect } from "./effect"; ? class computedRefImpl { private _dirty: boolean = true; private _effect: ReactiveEffect; private _value: any; constructor(getter) { //創(chuàng)建時(shí),會(huì)創(chuàng)建一個(gè)響應(yīng)式實(shí)例,并且掛載 this._effect = new ReactiveEffect(getter, () => { //(三) //當(dāng)監(jiān)聽的值發(fā)生改變時(shí),會(huì)觸發(fā)set,此時(shí)觸發(fā)當(dāng)前依賴 //因?yàn)榇嬖谡{(diào)度器,不會(huì)立刻執(zhí)行用戶fn(實(shí)現(xiàn)了lazy),而是將_dirty更改為true //在下一次用戶get時(shí),會(huì)調(diào)用run方法,重新拿到最新的值返回 if (!this._dirty) { this._dirty = true; } }); } get value() { //(一) //默認(rèn)_dirty是true //那么在第一次get的時(shí)候,會(huì)觸發(fā)響應(yīng)式實(shí)例的run方法,觸發(fā)依賴收集 //同時(shí)拿到用戶fn的值,存儲(chǔ)起來(lái),然后返回出去 if (this._dirty) { this._dirty = false; this._value = this._effect.run(); } //(二) //當(dāng)監(jiān)聽的值沒有改變時(shí),_dirty一直為false //所以,第二次get時(shí),因?yàn)開dirty為false,那么直接返回存儲(chǔ)起來(lái)的_value return this._value; } } export function computed(getter) { //創(chuàng)建一個(gè)computed實(shí)例 return new computedRefImpl(getter); }
工具類
//是否是reactive響應(yīng)式類型 export function isReactive(target) { return !!target[ReactiveFlags.IS_REACTIVE]; } //是否是readonly響應(yīng)式類型 export function isReadOnly(target) { return !!target[ReactiveFlags.IS_READONLY]; } //是否是響應(yīng)式對(duì)象 export function isProxy(target) { return isReactive(target) || isReadOnly(target); } //是否是對(duì)象 export function isObject(target) { return typeof target === "object" && target !== null; } //是否是ref export function isRef(ref: any) { return !!ref._v_isRef; } //解構(gòu)ref export function unRef(ref: any) { return isRef(ref) ? ref.value : ref; } //是否改變 export const hasChanged = (val, newVal) => { return !Object.is(val, newVal); };
判斷響應(yīng)式類型的依據(jù)是,在get的時(shí)候,檢查傳進(jìn)來(lái)的key是否等于某枚舉值來(lái)做為判斷依據(jù),在get中加入
//reactive.ts export const enum ReactiveFlags { IS_REACTIVE = "__v_isReactive", IS_READONLY = "__v_isReadOnly", } //baseHandler.ts function createGetter(isReadOnly: Boolean = false, shallow: Boolean = false) { return function get(target, key) { //... if (key === ReactiveFlags.IS_REACTIVE) { return !isReadOnly; } else if (key === ReactiveFlags.IS_READONLY) { return isReadOnly; } //... }; }
到此這篇關(guān)于深入理解Vue3響應(yīng)式原理的文章就介紹到這了,更多相關(guān)Vue3響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Vue數(shù)據(jù)驅(qū)動(dòng)原理
這篇文章主要介紹了詳解Vue數(shù)據(jù)驅(qū)動(dòng)原理的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)vue框架的相關(guān)知識(shí),感興趣的朋友可以了解下2020-11-11vue3實(shí)現(xiàn)圖片縮放拖拽功能的示例代碼
v3-drag-zoom 是基于 vue3 開發(fā)的一個(gè)縮放拖拽組件,方便開發(fā)者快速實(shí)現(xiàn)縮放拖拽功能,效果類似地圖的縮放與拖拽,本文給大家介紹了vue3如何快速實(shí)現(xiàn)圖片縮放拖拽功能,感興趣的朋友可以參考下2024-04-04vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作
這篇文章主要介紹了vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08如何在vue-cli中使用css-loader實(shí)現(xiàn)css module
這篇文章主要介紹了如何在vue-cli中使用css-loader實(shí)現(xiàn)css module,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2021-01-01Vue?實(shí)現(xiàn)新國(guó)標(biāo)紅綠燈效果實(shí)例詳解
這篇文章主要為大家介紹了Vue?實(shí)現(xiàn)新國(guó)標(biāo)紅綠燈效果實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08自定義input組件如何實(shí)現(xiàn)拖拽文件上傳
這篇文章主要介紹了自定義input組件如何實(shí)現(xiàn)拖拽文件上傳問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Vue3項(xiàng)目中引入html頁(yè)面的方法舉例
這篇文章主要給大家介紹了關(guān)于Vue3項(xiàng)目中引入html頁(yè)面的相關(guān)資料,Vue3是一個(gè)JavaScript框架,通常我們使用它來(lái)構(gòu)建單頁(yè)應(yīng)用程序(SPA),如果你想在HTML頁(yè)面中使用Vue3,可以參考這篇文章,需要的朋友可以參考下2023-09-09vue通過(guò)v-html指令渲染的富文本無(wú)法修改樣式的解決方案
這篇文章主要介紹了vue通過(guò)v-html指令渲染的富文本無(wú)法修改樣式的解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05