vue3中的響應式原理-effect

effect的基本實現

export let activeEffect = undefined;// 當前正在執行的effect
class ReactiveEffect {
    active = true;
    deps = []; // 收集effect中使用到的屬性
    parent = undefined;
    constructor(public fn) { }
    run() {
        if (!this.active) { // 不是激活狀態
            return this.fn();
        }
        try {
            this.parent = activeEffect; // 當前的effect就是他的父親
            activeEffect = this; // 設置成正在激活的是當前effect
            return this.fn();
        } finally {
            activeEffect = this.parent; // 執行完畢後還原activeEffect
            this.parent = undefined;
        }
    }
}
export function effect(fn, options?) {
    const _effect = new ReactiveEffect(fn); // 創建響應式effect
    _effect.run(); // 讓響應式effect默認執行
}

依賴收集

get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);  // 依賴收集
    return res;
}
const targetMap = new WeakMap(); // 記錄依賴關系
export function track(target, type, key) {
    if (activeEffect) {
        let depsMap = targetMap.get(target); // {對象:map}
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set())) // {對象:{ 屬性 :[ dep, dep ]}}
        }
        let shouldTrack = !dep.has(activeEffect)
        if (shouldTrack) {
            dep.add(activeEffect);
            activeEffect.deps.push(dep); // 讓effect記住dep,這樣後續可以用於清理
        }
    }
}

將屬性和對應的effect維護成映射關系,後續屬性變化可以觸發對應的effect函數重新run

觸發更新

set(target, key, value, receiver) {
    // 等會賦值的時候可以重新觸發effect執行
    let oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver);
    if (oldValue !== value) {
        trigger(target, 'set', key, value, oldValue)
    }
    return result;
}
export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target); // 獲取對應的映射表
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key);
    effects && effects.forEach(effect => {
        if (effect !== activeEffect) effect.run(); // 防止循環
    })
}

分支切換與cleanup

在渲染時我們要避免副作用函數產生的遺留

const state = reactive({ flag: true, name: 'jw', age: 30 })
effect(() => { // 副作用函數 (effect執行渲染瞭頁面)
    console.log('render')
    document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
    state.flag = false;
    setTimeout(() => {
        console.log('修改name,原則上不更新')
        state.name = 'zf'
    }, 1000);
}, 1000)
function cleanupEffect(effect) {
    const { deps } = effect; // 清理effect
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect);
    }
    effect.deps.length = 0;
}
class ReactiveEffect {
    active = true;
    deps = []; // 收集effect中使用到的屬性
    parent = undefined;
    constructor(public fn) { }
    run() {
        try {
            this.parent = activeEffect; // 當前的effect就是他的父親
            activeEffect = this; // 設置成正在激活的是當前effect
+           cleanupEffect(this);
            return this.fn(); // 先清理在運行
        }
    }
}

這裡要註意的是:觸發時會進行清理操作(清理effect),在重新進行收集(收集effect)。在循環過程中會導致死循環。

let effect = () => {};
let s = new Set([effect])
s.forEach(item=>{s.delete(effect); s.add(effect)}); // 這樣就導致死循環瞭

停止effect

export class ReactiveEffect {
    stop(){
        if(this.active){ 
            cleanupEffect(this);
            this.active = false
        }
    }
}
export function effect(fn, options?) {
    const _effect = new ReactiveEffect(fn); 
    _effect.run();
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner; // 返回runner
}

調度執行

trigger觸發時,我們可以自己決定副作用函數執行的時機、次數、及執行方式

export function effect(fn, options:any = {}) {
    const _effect = new ReactiveEffect(fn,options.scheduler); // 創建響應式effect
    // if(options){
    //     Object.assign(_effect,options); // 擴展屬性
    // }
    _effect.run(); // 讓響應式effect默認執行
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner; // 返回runner
}
export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return
    }
    let effects = depsMap.get(key);
    if (effects) {
        effects = new Set(effects);
        for (const effect of effects) {
            if (effect !== activeEffect) { 
                if(effect.scheduler){ // 如果有調度函數則執行調度函數
                    effect.scheduler()
                }else{
                    effect.run(); 
                }
            }
        }
    }
}

深度代理 

get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    // 等會誰來取值就做依賴收集
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);
    if(isObject(res)){
        return reactive(res);
    }
    return res;
}

當取值時返回的值是對象,則返回這個對象的代理對象,從而實現深度代理

總結

為瞭實現響應式,我們使用瞭new Proxy

effect默認數據變化要能更新,我們先將正在執行的effect作為全局變量,渲染(取值),然後在get方法中進行依賴收集

依賴收集的數據格式weakMap(對象:map(屬性:set(effect))

用戶數據發生變化,會通過對象屬性來查找對應的effect集合,全部執行;

調度器的實現,創建effect時,把scheduler存在實例上,調用runner時,判斷如果有調度器就調用調度器,否則執行runner

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: