深入理解Vue3響應(yīng)式原理
響應(yīng)式原理
利用ES6中Proxy作為攔截器,在get時(shí)收集依賴,在set時(shí)觸發(fā)依賴,來實(shí)現(xiàn)響應(yīng)式。
手寫實(shí)現(xiàn)
1、實(shí)現(xiàn)Reactive
基于原理,我們可以先寫一下測試用例
//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ù)的攔截處理,通過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è)收集起來,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)前依賴是否激活,如果清除過則為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上掛載依賴,方便在其他地方通過runner訪問到該依賴
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方法,來暫停依賴收集和清除已經(jīng)存在的依賴
//響應(yīng)式依賴 — 類
class ReactiveEffect {
private _fn: any; //用戶函數(shù),
active = true; //表示當(dāng)前依賴是否激活,如果清除過則為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ì)比較簡單,它是一個(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);
}
我們來實(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)緩存和惰性求值。
通過注解(一)(二)(三)可理解其實(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ǔ)起來,然后返回出去
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ǔ)起來的_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)來的key是否等于某枚舉值來做為判斷依據(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-11
vue3實(shí)現(xiàn)圖片縮放拖拽功能的示例代碼
v3-drag-zoom 是基于 vue3 開發(fā)的一個(gè)縮放拖拽組件,方便開發(fā)者快速實(shí)現(xiàn)縮放拖拽功能,效果類似地圖的縮放與拖拽,本文給大家介紹了vue3如何快速實(shí)現(xiàn)圖片縮放拖拽功能,感興趣的朋友可以參考下2024-04-04
vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作
這篇文章主要介紹了vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
如何在vue-cli中使用css-loader實(shí)現(xiàn)css module
這篇文章主要介紹了如何在vue-cli中使用css-loader實(shí)現(xiàn)css module,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2021-01-01
Vue?實(shí)現(xiàn)新國標(biāo)紅綠燈效果實(shí)例詳解
這篇文章主要為大家介紹了Vue?實(shí)現(xiàn)新國標(biāo)紅綠燈效果實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
自定義input組件如何實(shí)現(xiàn)拖拽文件上傳
這篇文章主要介紹了自定義input組件如何實(shí)現(xiàn)拖拽文件上傳問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
vue通過v-html指令渲染的富文本無法修改樣式的解決方案
這篇文章主要介紹了vue通過v-html指令渲染的富文本無法修改樣式的解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

