(二)響應式原理
利用ES6中Proxy作為攔截器,在get時收集依賴,在set時觸發依賴,來實現響應式。
(三)手寫實現
1、實現Reactive
基於原理,我們可以先寫一下測試用例
//reactive.spec.ts
describe("effect", () => {
it("happy path", () => {
const original = { foo: 1 }; //原始資料
const observed = reactive(original); //響應式資料
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1); //正常獲取資料
expect(isReactive(observed)).toBe(true);
expect(isReactive(original)).toBe(false);
expect(isProxy(observed)).toBe(true);
});
});
首先實現資料的攔截處理,透過ES6的Proxy,實現獲取和賦值操作。
//reactive.ts
//對new Proxy()進行包裝
export function reactive(raw) {
return createActiveObject(raw, mutableHandlers);
}
function createActiveObject(raw: any, baseHandlers) {
//直接返回一個Proxy物件,實現響應式
return new Proxy(raw, baseHandlers);
}
//baseHandler.ts
//抽離出一個handler物件
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是否是一個object
if (isObject(res)) {
//如果是,則進行巢狀處理,使得返回的物件中的 物件 也具備響應式
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);
//觸發依賴
trigger(target, key);
return res;
};
}
從上述程式碼中,我們可以⚠️注意到track(target, key) 和trigger(target, key) 這兩個函式,分別是對依賴的收集和觸發。
依賴:我們可以把依賴認為是把使用者對資料的操控(使用者函式,副作用函式)包裝成一個東西,我們在get的時候將依賴一個一個收集起來,set的時候全部觸發,即可實現響應式效果。
2、實現依賴的收集和觸發
//effect.ts
//全域性變數
let activeEffect: ReactiveEffect; //當前的依賴
let shouldTrack: Boolean; //是否收集依賴
const targetMap = new WeakMap(); //依賴樹
targetMap結構:
targetMap: {
每一個target(depsMap):{
每一個key(depSet):[
每一個依賴
]
}
}WeakMap和Map的區別
1、WeakMap只接受物件作為key,如果設定其他型別的資料作為key,會報錯。
2、WeakMap的key所引用的物件都是弱引用,只要物件的其他引用被刪除,垃圾回收機制就會釋放該物件佔用的記憶體,從而避免記憶體洩漏。
3、由於WeakMap的成員隨時可能被垃圾回收機制回收,成員的數量不穩定,所以沒有size屬性。
4、沒有clear()方法
5、不能遍歷
首先我們定義一個依賴類,稱為ReactiveEffect,對使用者函式進行包裝,賦予一些屬性和方法。
//effect.ts
//響應式依賴 — ReactiveEffect類
class ReactiveEffect {
private _fn: any; //使用者函式,
active = true; //表示當前依賴是否啟用,如果清除過則為false
deps: any[] = []; //包含該依賴的deps
onStop?: () => void; //停止該依賴的回撥函式
public scheduler: Function; //排程函式
//建構函式
constructor(fn, scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
//執行副作用函式
run() {
//使用者函式,可以報錯,需要用try包裹
try {
//如果當前依賴不是啟用狀態,不進行依賴收集,直接返回
if (!this.active) {
return this._fn();
}
//開啟依賴收集
shouldTrack = true;
activeEffect = this;
//呼叫時會觸發依賴收集
const result = this._fn();
//關閉依賴收集
shouldTrack = false;
//返回結果
return result;
} finally {
//todo
}
}
}
參考 前端進階面試題詳細解答
effect影響函式
建立一個使用者函式作用函式,稱為effect,這個函式的功能為基於ReactiveEffect類建立一個依賴,觸發使用者函式(的時候,觸發依賴收集),返回使用者函式。
//建立一個依賴
export function effect(fn, option: any = {}) {
//為當前的依賴建立響應式例項
const _effect = new ReactiveEffect(fn, option.scheduler);
Object.assign(_effect, option);
//最開始呼叫一次,其中會觸發依賴收集 _effect.run() -> _fn() -> get() -> track()
_effect.run();
const runner: any = _effect.run.bind(_effect);
//在runner上掛載依賴,方便在其他地方透過runner訪問到該依賴
runner.effect = _effect;
return runner;
}
bind():在原函式的基礎上建立一個新函式,使新函式的this指向傳入的第一個引數,其他引數作為新函式的引數
使用者觸發依賴收集時,將依賴新增到targetMap中。
收集/新增依賴
//把依賴新增到targetMap對應target的key中,在重新set時在trigger中重新觸發
export function track(target: Object, key) {
//如果不是track的狀態,直接返回
if (!isTracking()) return;
// target -> key -> dep
//獲取對應target,獲取不到則建立一個,並加進targetMap中
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
//獲取對應key,獲取不到則建立一個,並加進target中
let depSet = depsMap.get(key);
if (!depSet) {
depsMap.set(key, (depSet = new Set()));
}
//如果depSet中已經存在該依賴,直接返回
if (depSet.has(activeEffect)) return;
//新增依賴
trackEffects(depSet);
}
export function trackEffects(dep) {
//往target中新增依賴
dep.add(activeEffect);
//新增到當前依賴的deps陣列中
activeEffect.deps.push(dep);
}
觸發依賴
//一次性觸發對應target中key的所有依賴
export function trigger(target, key) {
let depsMap = targetMap.get(target);
let depSet = depsMap.get(key);
//觸發依賴
triggerEffects(depSet);
}
export function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
3、移除/停止依賴
我們在ReactiveEffect這個類中,增加一個stop方法,來暫停依賴收集和清除已經存在的依賴
//響應式依賴 — 類
class ReactiveEffect {
private _fn: any; //使用者函式,
active = true; //表示當前依賴是否啟用,如果清除過則為false
deps: any[] = []; //包含該依賴的deps
onStop?: () => void; //停止該依賴的回撥函式
public scheduler: Function; //排程函式
//...
stop() {
if (this.active) {
cleanupEffect(this);
//執行回撥
if (this.onStop) {
this.onStop();
}
//清除啟用狀態
this.active = false;
}
}
}
//清除該依賴掛載的deps每一項中的該依賴
function cleanupEffect(effect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
//移除一個依賴
export function stop(runner) {
runner.effect.stop();
}
(四)衍生型別
1、實現readonly
readonly相比於reactive,實現上相對比較簡單,它是一個只讀型別,不會涉及set操作,更不需要收集/觸發依賴。
export function readonly(raw) {
return createActiveObject(raw, readonlyHandlers);
}
export const readonlyHandlers = {
get: readonlyGet,
set: (key, target) => {
console.warn(`key:${key} set 失敗,因為target是一個readonly物件`, 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是否是一個object
if (isObject(res)) {
return isReadOnly ? readonly(res) : reactive(res);
}
if (!isReadOnly) {
//收集依賴
track(target, key);
}
return res;
};
}
2、實現shallowReadonly
我們先看一下shallow的含義
shallow:不深的, 淺的,不深的, 不嚴肅的, 膚淺的,淺薄的。
那麼shallowReadonly,指的是隻對最外層進行限制,而內部的仍然是一個普通的、正常的值。
//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、實現ref
ref相對reactive而言,實際上他不存在巢狀關係,就是一個value。
//ref.ts
export function ref(value: any) {
return new RefImpl(value);
}
我們來實現一下RefImpl類,原理其實跟reactive類似,只是一些細節處不同。
//ref.ts
class RefImpl {
private _value: any; //轉化後的值
public dep; //依賴容器
private _rawValue: any; //原始值,
public _v_isRef = true; //判斷ref型別
constructor(value) {
this._rawValue = value; //記錄原始值
this._value = convert(value); //儲存轉化後的值
this.dep = new Set(); //建立依賴容器
}
get value() {
trackRefValue(this); //收集依賴
return this._value;
}
set value(newValue) {
//新老值不同,才觸發更改
if (hasChanged(newValue, this._rawValue)) {
// 一定先修改value,再觸發依賴
this._rawValue = newValue;
this._value = convert(newValue);
triggerEffects(this.dep);
}
}
}
//ref.ts
//對value進行轉換(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();
}
}
}
實現proxyRefs
//實現對ref物件進行代理 //如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、實現computed
computed的實現也很巧妙,利用排程器機制和一個私有變數_value,實現快取和惰性求值。
透過註解(一)(二)(三)可理解其實現流程
//computed
import { ReactiveEffect } from "./effect";
class computedRefImpl {
private _dirty: boolean = true;
private _effect: ReactiveEffect;
private _value: any;
constructor(getter) {
//建立時,會建立一個響應式例項,並且掛載
this._effect = new ReactiveEffect(getter, () => {
//(三)
//當監聽的值發生改變時,會觸發set,此時觸發當前依賴
//因為存在排程器,不會立刻執行使用者fn(實現了lazy),而是將_dirty更改為true
//在下一次使用者get時,會呼叫run方法,重新拿到最新的值返回
if (!this._dirty) {
this._dirty = true;
}
});
}
get value() {
//(一)
//預設_dirty是true
//那麼在第一次get的時候,會觸發響應式例項的run方法,觸發依賴收集
//同時拿到使用者fn的值,儲存起來,然後返回出去
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}
//(二)
//當監聽的值沒有改變時,_dirty一直為false
//所以,第二次get時,因為_dirty為false,那麼直接返回儲存起來的_value
return this._value;
}
}
export function computed(getter) {
//建立一個computed例項
return new computedRefImpl(getter);
}
(五)工具類
//是否是reactive響應式型別
export function isReactive(target) {
return !!target[ReactiveFlags.IS_REACTIVE];
}
//是否是readonly響應式型別
export function isReadOnly(target) {
return !!target[ReactiveFlags.IS_READONLY];
}
//是否是響應式物件
export function isProxy(target) {
return isReactive(target) || isReadOnly(target);
}
//是否是物件
export function isObject(target) {
return typeof target === "object" && target !== null;
}
//是否是ref
export function isRef(ref: any) {
return !!ref._v_isRef;
}
//解構ref
export function unRef(ref: any) {
return isRef(ref) ? ref.value : ref;
}
//是否改變
export const hasChanged = (val, newVal) => {
return !Object.is(val, newVal);
};
判斷響應式型別的依據是,在get的時候,檢查傳進來的key是否等於某列舉值來做為判斷依據,在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;
}
//...
};
}