Vue3响应式

238 阅读1分钟

Vue3响应式

  • vue3的响应式使用的是reactivity中的reactive与effect来实现
  • 该响应式应用广泛

需要用到一个html来展示

  • 使用下载的包体验效果
<body>
    <script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
    <!-- <script src="./reactivity.global.js"></script> -->
    <div id="app"></div>
    <script>
        const App = document.querySelector('#app');
        // 缓存代理结果 require()
        const {
            effect,
            reactive
        } = VueReactivity;
        const obj = {
            name: 'cc',
            age: 0,
            address: {
                code: 1
            }
        }
        const state = reactive(obj);
        effect(() => {
            App.innerHTML = `
                姓名:${state.name}<br />
                年龄:${state.age}<br />
                邮政编码:${state.address.code}
            `
        });
        setTimeout(() => {
            state.name = '崔';
        }, 1000)
    </script>
</body>

reactive

  • 通过proxy来代理,返回一个proxy对象
  • 接收一个非null的object
//第一版
/*
    @vue/shared代码:检测是否为一个对象
        export const isObject = (value) =>{
            return typeof value === 'object' && value !== null
        }
*/
import { isObject } from '@vue/shared';
export function reactive(target) {
    //如果不是一个对象,原样返回
    if (!isObject(target)) {
        return target;
    }
    //创建proxy对象
    const proxy = new Proxy(target, {
        get(target, key, reactive) {
        //target为对象,key为属性名,reactive为proxy对象
            return target[key];
        },
        set(target, key, value, reactive) {
            target[key] = value;
            return true;
        }
    });
    //返回proxy对象
    return proxy;
}

问题1

  • 第一版,是存在一些问题
    • 比如使用属性访问器来拿到值的话,获取的是源对象,且get只能监听到当前访问值,无法监听到访问器函数内部依赖的值
...
//在obj中新增属性访问器,且让其内部依赖name
    get getName() {
        console.log(this === obj);
        return this.name;
    }
...

//访问getName
state.getName;//只触发了一次get,key为getName,内部依赖的name却没有触发,通过this指向可以看到它的this为obj
  • 解决办法,使用es6的Reflect来处理get和set
//修改porxy对象
const proxy = new Proxy(target, {
    get(target, key, reactive) {
        return Reflect.get(target, key, reactive)
    },
    set(target, key, value, reactive) {
        return Reflect.set(target, key, value, reactive)
    }
})

问题2

  • 使用官网代码可得到以下结果:
    • 也就是说reactive方法会走缓存,类似require
      • 如果传入的值相同,返回原有数据
      • 如果传入的是返回的proxy,那么依然返回原有的proxy
  • 我们的代码暂时不具备这种功能
        const state1 = reactive(obj);
        const obj1 = reactive(state);
        const address = reactive(obj.address);
        console.log(state === state1, '两次代理obj是否相等');//true
        console.log(state === obj1, '等不等于state它自身再代理');//true
        console.log(state === address, 'state等不等于内部object再代理');//false
  • 实现步骤:
    • 通过WeakMap来存储this指向
    • 在代码中添加缓存判断条件
const reactiveMap = new WeakMap();
const enum ReactiveFlags {
    IS_REACTIVE = '__v_isRective'
};
export function reactive(target) {
    ...
    //如果存在,那么返回它
    if (reactiveMap.get(target)) {
        return reactiveMap.get(target);
    }
    //查看是否为已代理对象
    //如果没被代理过那么就是undefined,否则走的是proxy的get监听,那么就会返回true
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    const proxy = new Proxy(target, {
    get(target, key, reactive) {
        if (key === ReactiveFlags.IS_REACTIVE) {
            //如果被代理过,才会走这个,否则走不到这里
            return true;
        }
        return Reflect.get(target, key, reactive)
        }
        ...
    })
    //已target为this,将proxy存储起来
    reactiveMap.set(target, proxy);
    return proxy;
}
  • 感觉核心逻辑没有动,那么我们把它抽离出来
  • 新建文件baseHandler.ts
    • 把proxy的配置与缓存enmu拿到里面
  • 原文件导入使用
export const enum ReactiveFlags {
    IS_REACTIVE = '__v_isRective'
};
export const mutableHandlers = {
    get(target, key, reactive) {
        if (key === ReactiveFlags.IS_REACTIVE) {
            //如果被代理过,才会走这个,否则走不到这里
            return true;
        }
        return Reflect.get(target, key, reactive)
    },
    set(target, key, value, reactive) {
        return Reflect.set(target, key, value, reactive)
    }
}

effect

  • 接收一个函数
  • 首次执行一次,后续数据改变,函数执行
    • 我们需要做一下优化,不能说每一个key改变,所有的effect都重新执行
    • 那么我们需要做一个依赖收集,也就是将函数按照使用的key进行分类
    • effect会嵌套使用
export let activeEffect = undefined;
class ReactiveEffect {
    public active = true // 这个effect是否为激活状态
    public parent = null // 父节点
    public deps = [];
    constructor(public fn) {
        //pubilc fn 等价于 this.fn = fn
    }
    run() {
        //如果active为false,那么不需要进行依赖收集,直接执行函数即可
        if (!this.active) {
            return this.fn();
        }
        //在这里进行依赖收集
        //让当前effect和稍后渲染的属性关联在一起
        try {
            //进来先让父节点为activeEffect,然后再改变activeEffect为当前this,防止parent丢失
            //因为代码是同步执行
            this.parent = activeEffect;
            activeEffect = this;

            return this.fn();
        } finally {
            activeEffect = this.parent;
        }
    }
}
export function effect(fn) {
    const _effect = new ReactiveEffect(fn);//创建一个响应式的effect
    _effect.run();//默认先执行一次
}
  • 依赖收集我们需要考虑一些情况,比如说,一个key对应多个fn,一个fn对应多个key,也就是所谓的多对多关系
  • 使用如下结构:WeakMap{this:Map{name:Set[]}}
track
  • 依赖收集函数
const targetMap = new WeakMap();
export function track(target, type, key) {
    /*
        因为target是objec,而我们想存储object为指针,那么需要用到WeakMap结构
        内部的话,因为是多对对,想以key存储,而key是string,所以使用Map结构
        key的值有多个,可以使用数组,我们选择使用Set
        最终结构:WeakMap{
            target: Map{
                key: Set[]
            }
        }
    */
    //如果不是在effect中执行的,不进行依赖收集
    if (!activeEffect) return;
    let depsMap = targetMap.get(target);//第一次获取必然不存在
    if (!depsMap) {
        targetMap.set(target, depsMap = new Map());
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, dep = new Set())
    }
    //虽然set会自动去重,但是还是手动处理下(性能优化,在已知重复的情况下,直接不存储,也就不需要使用set的内部去重)
    let shouldTrack = !dep.has(activeEffect);
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);

    }
}
trigger
  • 执行函数
  • 查找当前函数是否存在,存在就执行
export function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target);
    //如果不存在,代表你改的值跟effect完全没关系,也就不用执行更新函数了
    if (!depsMap) return;
    const effects = depsMap.get(key);
    effects && effects.forEach(effect => {
        // 可能会有用户在effect里修改依赖,那么会产生死循环,这时候需要判断下
        // 如果当前effect不等于上次执行的effect,那么执行run
        if(effect!==activeEffect)effect.run();
    })
}
cleanupEffect
  • 清除存储的effect
  • 为什么要清除,看下面demo
    //render执行了3次,但实际上,name再次更新的时候,我们不需要它render了
    effect(() => {
        console.log('render');
        App.innerHTML = state.flag ? state.name : state.age;
    });
    state.getName;
    setTimeout(() => {
        // state.name = '崔'
        state.flag = false;
    }, 1000);
    setTimeout(()=>{
        state.name = '更新';
    },2000)
  • 那么,我们需要在每次收集依赖前,清除上一次的依赖
//它并没有放在ReactiveEffect里
function cleanupEffect(effect) {
    const { deps } = effect;
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect);//这里是清除set里面的effect引用
    }
    deps.length = 0;//清空数组
    /*
    为什么说直接deps.length = 0不行呢
    因为清除的是deps的引用,但set内部还是留着
    */
}
class ReactiveEffect {
    ...
    run() {
        ...
        try {
            ...
            cleanupEffect(this);//放在this.fn()前面
            return this.fn();
        } 
        ....
    }
}

//这里需要注意
export function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target);
    //如果不存在,代表你改的值跟effect完全没关系,也就不用执行更新函数了
    if (!depsMap) return;
    let effects = depsMap.get(key);
    /*
    effects && effects.forEach(effect => {
        // 可能会有用户在effect里修改依赖,那么会产生死循环,这时候需要判断下
        // 如果当前effect不等于上次执行的effect,那么执行run
         if (effect !== activeEffect) effect.run();
     })
    这块需要改变下方式,因为当前方式会引起死循环,set死循环例子:
     const test = new Set([1]);
     test.forEach(item=>{
         test.delete(1);
         test.add(1);
     })
    */
    if (effects) {
        //换一个空间地址
        effects = new Set(effects);
        effects.forEach(effect => {
            // 可能会有用户在effect里修改依赖,那么会产生死循环,这时候需要判断下
            // 如果当前effect不等于上次执行的effect,那么执行run
            if (effect !== activeEffect) effect.run();
        })
    }
}
stop
  • 失活,执行以后,后续更新不会再执行当前effect
// 我们先把render注释,会发现一秒过去后,还是name,没有变成age
//执行render后,发现更新又恢复了
let render = effect(() => {
    console.log('render');
    App.innerHTML = state.flag ? state.name : state.age;
});
render.effect.stop(); //失活
setTimeout(() => {
    state.flag = false;
    setTimeout(()=>{
        state.age++;
        // render();
    },1000)
},1000)

  • 我们失活的时候,需要将active变为false,而且要清除effect
  • effect要有返回值,render实际上还是effect
  • 实现:
class ReactiveEffect {
    ...
    stop() {
        if (this.active) {
            this.active = false;
            cleanupEffect(this);
        }

    }
    ...
}
export function effect(fn) {
    const _effect = new ReactiveEffect(fn);
    _effect.run();//初始化,先执行一次

    let render = _effect.run.bind(_effect);
    render.effect = _effect;
    return render;
};
effect的第二个参数-调度器
  • 第二个参数是一个object
  • 里面可以传一个scheduler函数,我们可以在这个函数内决定什么时候更新
    let render = effect(() => {
        console.log('render');
        // App.innerHTML = state.flag ? state.name : state.age;
        App.innerHTML = state.age;
    }, {
        scheduler() {
            //只有为5的时候才渲染
            if(state.age===5){
                render();
            }
            console.log('run')
        }
    });
    state.age = 2;
    state.age = 3;
    state.age = 4;
    state.age = 5;
  • 实现:
class ReactiveEffect {
    ...
    constructor(public fn, public scheduler) { }
    ...
}
export function effect(fn, options: any = {}) {
    const _effect = new ReactiveEffect(fn, options.scheduler);
    ...
};
export function trigger(target, type, key, value, oldValue) {
    ...
    if (effects) {
        //换一个空间地址
        effects = new Set(effects);
        effects.forEach(effect => {
            if (effect !== activeEffect) {
                if (effect.scheduler) {//如果scheduler存在就执行scheduler,否则执行run
                    effect.scheduler();
                } else {
                    effect.run();
                }

            }
        })
    }
}

computed

  • 计算属性
  • 可传递function与object
    • function只能获取
    • object内置get与set
  • 如果依赖没改变,function与get函数不会多次执行,会走缓存(性能优化点)
  • computed执行会返回一个object,里面内置一个value,我们可以对value进行获取与修改
  • computed不会立即触发,而是在调用value时触发
const { computed } = VueReactivity;

const obj = {
    name: '崔',
    age: 1
}
const state = reactive(obj);

let fullName = computed(() => {
    console.log('执行计算');
    return state.name + state.age;
});

console.log(fullName.value);
console.log(fullName.value);
console.log(fullName.value);
state.name = '崔';
console.log(fullName.value);
console.log(fullName.value);
//log会输出两次,一次是初始计算,另一次是依赖改变后再次计算
  • 当在effect中使用时候,计算属性依赖的值改变,那么会重新执行effect,然后执行计算
let fullName = computed(() => {
    console.log('执行计算');
    return state.name + state.age;
});
effect(() => {
        console.log('runder')
        App.innerHTML = fullName.value;
});

setTimeout(() => {
    state.age = 2;
})
//runder 计算 runder 计算
  • 传入object
let fullName = computed({
    get() {
        return state.name + state.age;
    },
    set(value) {
        state.name = value;
    }
});
effect(() => {
    console.log('runder')
    App.innerHTML = fullName.value;
});
setTimeout(() => {
    fullName.value = '崔崔崔'
})
  • 可以看到规律,调用才会执行,如果在effect中,那么会执行effect,然后effect会调用计算函数,依赖值没变,不重新计算
//新增isFunction函数
export const isFunction = (value) => {
    return typeof value === 'function'
}
//effect.js模块改动
//抽离了两个公共方法triggerEffect与trackEffect
export function track(target, type, key) {
    //如果不是在effect中执行的,不进行依赖收集
    if (!activeEffect) return;
    let depsMap = targetMap.get(target);//第一次获取必然不存在
    if (!depsMap) {
        targetMap.set(target, depsMap = new Map());
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, dep = new Set())
    }
    trackEffect(dep);

}
//将收集代码抽离
export function trackEffect(deps) {
    let shouldTrack = !deps.has(activeEffect);
    if (shouldTrack) {
        deps.add(activeEffect);
        activeEffect.deps.push(deps);
    }
}
export function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let effects = depsMap.get(key);
    triggerEffect(effects);
}
//将执行代码抽离出来
export function triggerEffect(effects) {
    if (effects) {
        //换一个空间地址
        effects = new Set(effects);
        effects.forEach(effect => {
            if (effect !== activeEffect) {
                if (effect.scheduler) {
                    effect.scheduler();
                } else {
                    effect.run();
                }
            }
        })
    }
}
  • 代码实现
import { isFunction } from '@vue/shared';
import { activeEffect, ReactiveEffect, trackEffect, triggerEffect } from './effect'
export function computed(getterOrOptions) {
    let isGetter = isFunction(getterOrOptions)
    let getter;
    let setter;
    let fn = () => console.warn('没传递set,不能修改');
    if (isGetter) {
        getter = getterOrOptions;
        setter = fn;
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set || fn;
    }
    return new ComputedRefImpl(getter, setter);
}
class ComputedRefImpl {
    private _value;
    private _dirty = true;//是否需要更新
    public effect;
    public deps;//依赖
    constructor(getter, public setter) {
        //ReactiveEffect就是执行一次函数,然后将当前函数依赖数据收集,等待数据更新渲染依赖函数,也就是重新执行计算属性函数
        //而我们的getter就是一个函数,且它就是获取数据,所以放在ReactiveEffect里,然后将实例返回给effect

        this.effect = new ReactiveEffect(getter, () => {
            if (!this._dirty) {
                //如果依赖改变,就重新计算,计算完成,将effect执行
                this._dirty = true;
                triggerEffect(this.deps);
            }
        });
    }
    get value() {
        if (activeEffect) {
            //依赖收集,将effect收集起来
            trackEffect(this.deps || (this.deps = new Set()))
        }
        //只有获取value的时候,才会执行函数
        if (this._dirty) {
            this._dirty = false;
            this._value = this.effect.run();
        }
        return this._value;
    }
    set value(value) {
        this.setter(value);
    }
}
ref
//ref放在Vue上
const {
    ref,
    effect
} = Vue;
const flag = ref(true);
effect(() => {
    console.log(flag.value ? 'true' : 'false');
});
setTimeout(() => {
    flag.value = !flag.value;
})
实现
import { isObject } from "@vue/shared";
import { trackEffect, triggerEffect } from "./effect";
import { reactive } from "./reactive";

/**
 * 
 * @param value 
 * @returns 如果是对象,就将他变成proxy,否则返回原始值
 */
function toReactive(value) {
    return isObject(value) ? reactive(value) : value;
}
class RefImpl {
    public dep = new Set;
    public _value;
    public __v_isRef = true;
    constructor(public rawValue) {
        this._value = toReactive(rawValue);
    }
    get value() {
        trackEffect(this.dep);
        return this._value;
    }
    set value(newValue) {
        if (newValue !== this.rawValue) {
            this._value = toReactive(newValue);
            this.rawValue = newValue;
            triggerEffect(this.dep);
        }
    }
}
export function ref(value) {
    return new RefImpl(value);
}
toRefs
  • 将多条数据变为有ref组成的对象
const {
    ref,
    effect,
    reactive,
    toRefs
} = Vue;
const state = reactive({
    name: 'cc',
    age: 1
});
//每次访问需要state.某某某
effect(() => {
    console.log(`姓名:${state.name},年龄:${state.age}`);
})
setTimeout(() => {
    state.name = 'aa';
})

//使用toRefs后
const state = reactive({
    name: 'cc',
    age: 1
});
const {
    name,
    age
} = toRefs(state);
//通过.value访问
effect(() => {
    console.log(`姓名:${name.value},年龄:${age.value}`);
})
setTimeout(() => {
    state.name = 'aa';
})
实现
class ObjectRefImpl {
    constructor(public object, public key) {

    }
    get value() {
        return this.object[this.key]
    }
    set value(newValue) {
        this.object[this.key] = newValue;
    }
}
function toRef(object, key) {
    return new ObjectRefImpl(object, key);
}
export function toRefs(object) {
    const result = isArray(object) ? new Array(object.length) : {};
    for (let key in object) {
        result[key] = toRef(object, key);
    }
    return result;
}
proxyRefs
  • 反向代理,实现不使用.value访问
  • 体验
    • 下面的例子中还是将proxyRefs返回的对象结构的,但实际在vue中,我们是可以省略with(school)的
const {
    ref,
    effect,
    reactive,
    toRefs,
    proxyRefs
} = Vue;
const state = reactive({
    name: 'cc',
    age: 1
});
const school = proxyRefs(state);
effect(() => {
    with(school){
        console.log(this);
        console.log(`姓名:${name},年龄:${age}`);
    }
})
// effect(() => {
//     console.log(`姓名:${state.name},年龄:${state.age}`);
// })
setTimeout(() => {
    state.name = 'aa';
})
实现
export function proxyRefs(object) {
    return new Proxy(object, {
        get(target, key, recevier) {
            let r = Reflect.get(target, key, recevier);
            //如果是ref那么返回value,否则直接返回
            return r.__v_isRef ? r.value : r;
        },
        set(target, key, value, recevier) {
            let oldValue = target[key];
            if (oldValue['__v_isRef']) {
                oldValue.value = value;
                return true;
            } else {
                Reflect.set(target, key, value, recevier);
            }
        }
    })
}