Vue3源码学习 --- 响应式一

响应式系统

响应式系统可以说 vue 的驱动器,当读取模板中的数据时触发 getter,此时收集渲染函数;模板中的数据被修改触发 setter,此时执行收集到的渲染函数。vue 响应式的实现的核心思想是数据劫持,在 vue2 中是通过 Object.defineProperty 实现,vue3 是通过 proxy 实现。这些都是一些基本知识点,对于响应式系统除了数据劫持外还做了什么。

副作用函数和简单的响应式系统

在说明响应式系统的功能时,有提到两个关键的名词:模板中的数据和渲染函数。其中渲染函数就是副作用函数,因为该函数有访问函数外部的变量。副作用函数执行导致外部变量的改变,直接或者间接的影响到使用该外部变量的其他函数。模板中的数据的读取或修改都会和副作用函数产生关系,下面通过代码来完成一个简单的响应式系统

const data = {
    str: 'hello world',
}
// 定义副作用函数 effect
function effect() {
    document.body.innerText = proxy.str;
}
let fn;
// 进行数据劫持
const proxy = new Proxy(data, {
    get(target, key) {
        fn = effect;
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal;
        fn();
        return true;
    }
})
effect();
setTimeout(()=>{
    proxy.str='111'
},1000)

虽然完成了一个简单的响应式系统,但是考虑以下几种情况,代码该如何修改增加功能呢

  • 情况一:如果副作用函数是一个匿名函数

    function effect() {
        document.body.innerText = proxy.str;
    }
    // 副作用函数是一个匿名函数
    function fn(()=>{
        document.body.innerText = proxy.str;
    }) 
    
  • 情况二:data 中有多个属性,每个属性都存在与之相对的副作用函数

    data ={
        key1:value1,
        key2:value2,
        key3:value3,
    }
    function effect1(){data.key1}
    function effect2(){data.key2}
    function effect3(){data.key3}
    
  • 情况三:data 中的同一个属性被多个副作用函数使用

    data ={
        key1:value1,
    }
    function effect1(){data.key1}
    function effect2(){data.key1}
    function effect3(){data.key1}
    
  • 情况四:data 中有多个属性,每个属性都被同一个副作用函数使用

    data ={
        key1:value1,
        key2:value2,
        key3:value3,
    }
    function effect1(){data.key1;data.key2;data.key3;}
    
  • 情况五:访问了 data 中不存在的属性

    data ={
        key1:value1,
    }
    function effect1(){data.key2}
    
  • 情况六:存在多个 data

    data1 ={
        key1:value1,
    }
    data2 ={
        key1:value2,
    }
    function effect1(){data1.key1}
    function effect2(){data2.key1}
    

完善响应式系统

针对以上提到的问题,逐一完成要求的功能

副作用函数是一个匿名函数

对于情况一:副作用函数是匿名函数,可以封装一个 effect 函数,提供一个参数 fn 用来接收并注册副作用函数。同时创建一个全局变量用来保存副作用函数。

// 创建一个全局变量,用来保存被注册的副作用函数
export let activeEffect:()=>void;
// effect 函数用来注册副作用函数,而非副作用函数本身
export function effect<T>(fn:()=>T){
    activeEffect = fn;
    fn();
}

data 中有多个属性

上面说的情况二/三/四/五其实是实现方式都是一样的,需要做的是副作用函数和目标属性建立明确联系:用到同一个属性的所有的 effect 函数保存在同一个 Set 对象内。创建一个 Map 对象,保存对象属性和 set 对象的映射。

data ={
    key:value,
}
function effect(){data.key}
// 下面表示的是 key/effect 和 Set/Map 之间的关系
// Set 对象保存所有的 effect 函数
Set: effect
// Map 对象保存属性 key 和 Set 对象的映射
Map: key → Set

按照上方所述的原理,代码如下:

// 创建一个全局变量,用来保存被注册的副作用函数
let activeEffect;
// effect 函数用来注册副作用函数,而非副作用函数本身
function effect(fn) {
    activeEffect = fn;
    fn();
}

// 创建一个全局的 bucket 变量,用来保存属性和 Set 对象的映射关系
const bucket = new Map();
// get 拦截函数中调用 track 记录各属性对应的副作用函数
function track(target, key) {
    if (!activeEffect)
        return;
    let depsMap = bucket.get(key);
    !depsMap && bucket.set(key, (depsMap = new Set()));
    depsMap.add(activeEffect);
    // 记录完副作用函数后置空
    activeEffect = null;
}
function trigger(target, key) {
    let depsMap = bucket.get(key);
    depsMap && depsMap.forEach(cb => cb());
}
// 封装一个 reactive 函数,实现对对象的拦截
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            track(target, key);
            return target[key];
        },
        set(target, key, newVal) {
            target[key] = newVal;
            trigger(target, key);
            return true;
        }
    });
}

测试代码:

<div id="person1">
    <h1 class="effect1"></h1>
    <h1 class="effect2"></h1>
    <h1 class="effect22"></h1>
    <h1 class="effect23"></h1>
    <h1 class="effect3"></h1>
    <h1 class="effect4"></h1>
</div>
<script>
    const person1 = reactive({
        name: 'tom',
        age: 11,
        sex: 'man'
    });
    const person2 = {
        name: 'mary',
        age: 10,
        sex: 'women'
    }

    effect(() => {
        document.querySelector('#person1 .effect1').innerText = person1.name;
    });
    effect(() => {
        document.querySelector('#person1 .effect2').innerText = person1.age;
    });
    effect(() => {
        document.querySelector('#person1 .effect22').innerText = person1.age + 100;
    });
    effect(() => {
        document.querySelector('#person1 .effect23').innerText = person1.age + 200;
    });
    effect(() => {
        document.querySelector('#person1 .effect3').innerText = person1.sex;
    });
    effect(() => {
        document.querySelector('#person1 .effect4').innerText = person1.a
    });
    setTimeout(() => {
        Object.assign(person1, person2);
    }, 2000);
    setTimeout(() => {
        person1.a = 'hello world';
    }, 3000);
</script>

存在多个 data

对于情况六存在多个 data 的情况,实现思想和上面类似。只需要新增一组 WeakMap 对象,表示 data 和 Map 的映射关系。

data1 ={
    key1:value1,
}
data2 ={
    key1:value2,
}
function effect1(){data1.key1}
function effect2(){data2.key1}
// 下面表示的是 key/effect 和 Set/Map 之间的关系
// Set 对象保存所有的 effect 函数
Set: effect
// Map 对象保存属性 key 和 Set 对象的映射
Map: key → Set
// WeakMap 对象保存 data 和 Map 对象的映射关系
WeakMap: data → Map

代码修改如下:

// 创建一个全局变量,用来保存被注册的副作用函数
let activeEffect;
// effect 函数用来注册副作用函数,而非副作用函数本身
function effect(fn) {
    activeEffect = fn;
    fn();
}
function setActiveEffectNull() {
    activeEffect = null;
}

// 创建一个全局的 bucket 变量,用来保存 data 和 Map 对象的映射关系
const bucket = new WeakMap();
// get 拦截函数中调用 track 记录各属性对应的副作用函数
function track(target, key) {
    if (!activeEffect)
        return;
    // 创建一个 depsMap 变量,用来保存属性和 Set 对象的映射关系
    let depsMap = bucket.get(target);
    // 如果 data 不存在,则新建一个以 data 为 key,value 为 Map 的映射
    !depsMap && bucket.set(target, (depsMap = new Map()));
    // 创建一个 dep 变量,用来保存所有的 effect 函数
    let deps = depsMap.get(key);
    !deps && depsMap.set(key, deps = new Set());
    deps.add(activeEffect);
    // 记录完副作用函数后 activeEffect 置空
    setActiveEffectNull();
}
function trigger(target, key) {
    const depsMap = bucket.get(target);
    if (!depsMap)
        return;
    const deps = depsMap.get(key);
    deps && deps.forEach(cb => cb());
}
// 封装一个 reactive 函数,实现对对象的拦截
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            track(target, key);
            return target[key];
        },
        set(target, key, newVal) {
            target[key] = newVal;
            trigger(target, key);
            return true;
        }
    });
}

响应式数据和副作用函数之间的关系

const person3 = reactive({
    name: 'Andy',
    age: 5,
});
effect(() => {
    document.querySelector('#person3 .effectOuter').innerText = person3.name;
})
effect(() => {
    document.querySelector('#person3 .effectInner').innerText = person3.age + 2;
})
effect(() => {
    document.querySelector('#person3 .effectInner').innerText = person3.age + 3;
})

// WeakMap
WeakMap {{} => Map(2)}
	0: {Object => Map(2)}
		key: {name: 'Tom', age: 100}
		value: Map(2)
			0: {"name" => Set(1)}
			1: {"age" => Set(2)}

图示:
请添加图片描述

副作用函数嵌套的处理

在副作用函数 effect 函数中嵌套使用 effect,对应到 vue 的使用情形是在一个组件中使用了另一个组件。看如下情况,出现了副作用函数未被正确的属性收集的问题:

<div id="person3">
    <h1 class="effectOuter"></h1>
    <h1 class="effectInner"></h1>
</div>
<script>
    const person3 = reactive({
        name: 'Andy',
        age: 5,
    });
    effect(() => {
        console.log('effectOuter')
        effect(() => {
            console.log('effectInner');
            document.querySelector('#person3 .effectInner').innerText = person3.age + 1;
        })
        document.querySelector('#person3 .effectOuter').innerText = person3.name;
    })

    setTimeout(() => {
        person3.name = 'Tom';
    }, 3000)
    // effectOuter
    // effectInner
    // effectInner
    /*
    	符合预期的结果,同时页面上的 name 改为 Tom
    	// effectOuter
    	// effectInner
    	// effectOuter
    	// effectInner
    */
</script>

出现以上问题的原因在于副作用函数收集出了问题:activeEffect 变量在执行嵌套 effect 函数时发生了改变。执行到内部的 effect 函数时 activeEffect 被覆盖为内部的 effect 函数并且不再改变,再之后执行外部的代码触发 track 函数此时保存的是内部的 effect 函数。知道原因后,我们可以添加 effectStack 变量,用来记录 activeEffect。 修改后的代码如下:

// 创建一个全局变量,用来保存被注册的副作用函数
let activeEffect;
// 创建变量 effectStack 用来保存 activeEffect
const effectStack = [];
// effect 函数用来注册副作用函数,而非副作用函数本身
function effect(fn) {
    activeEffect = fn;
    // activeEffect 入栈,当存在 effect 嵌套时保存所有的 activeEffect(副作用函数)
    effectStack.push(activeEffect);
    // 副作用函数执行,若存在 effect 嵌套,则会重新调用 effect,将 activeEffect 入栈
    fn();
    // 将已执行的副作用函数出栈
    effectStack.pop();
    // 还原副作用函数为之前的值
    activeEffect = effectStack[effectStack.length - 1];
}

避免无限递归循环

考虑如下情况,当属性自加时,会直接报错 Uncaught RangeError: Maximum call stack size exceeded

const person = reactive({
    name: 'Andy',
    age: 5,
});
effect(() => {
    console.log('effect');
    document.querySelector('#person3 .effectInner').innerText = person.age++;
})

原因发生在 person.age++ 上,当进行自增操作时其实会分成两步:读取 person.age 的值,加 1 后再修改 person.age 值。既触发了 get 执行 track 函数收集副作用函数,又触发 set 执行 trigger 函数执行副作用函数。解决方式很简单:在 trigger 函数中增加判断,如果trigger 函数中执行的副作用函数与需要收集的副作用函数相同,则不执行副作用函数,代码修改如下:

function trigger(target: obj, key: string) {
    const depsMap = bucket.get(target);
    if (!depsMap) return
    const deps = depsMap.get(key);
    //新增变量 effectToRun 保存需要执行的副作用函数,避免无线递归循环
    const effectToRun = new Set<fn>();
    deps && deps.forEach(effect => {
        if (effect !== activeEffect) {
            effectToRun.add(effect)
        }
    });
    effectToRun.forEach(effect => effect())
}

整个代码部分:

    // 创建一个全局变量,用来保存被注册的副作用函数
    let activeEffect;
    // 创建变量 effectStack 用来保存 activeEffect
    const effectStack = [];
    // effect 函数用来注册副作用函数,而非副作用函数本身
    function effect(fn) {
        activeEffect = fn;
        // activeEffect 入栈,当存在 effect 嵌套时保存所有的 activeEffect(副作用函数)
        effectStack.push(activeEffect);
        // 副作用函数执行,若存在 effect 嵌套,则会重新调用 effect,将 activeEffect 入栈
        fn();
        // 将已执行的副作用函数出栈
        effectStack.pop();
        // 还原副作用函数为之前的值
        activeEffect = effectStack[effectStack.length - 1];
    }

    // 创建一个全局的 bucket 变量,用来保存 data 和 Map 对象的映射关系
    const bucket = new WeakMap();
    // get 拦截函数中调用 track 记录各属性对应的副作用函数
    function track(target, key) {
        if (!activeEffect)
            return;
        // 创建一个 depsMap 变量,用来保存属性和 Set 对象的映射关系
        let depsMap = bucket.get(target);
        // 如果 data 不存在,则新建一个以 data 为 key,value 为 Map 的映射
        !depsMap && bucket.set(target, (depsMap = new Map()));
        // 创建一个 dep 变量,用来保存所有的 effect 函数
        let deps = depsMap.get(key);
        !deps && depsMap.set(key, deps = new Set());
        deps.add(activeEffect);
        // 记录完副作用函数后 activeEffect 置空
        // setActiveEffectNull();
    }
    function trigger(target, key) {
        const depsMap = bucket.get(target);
        if (!depsMap)
            return;
        const deps = depsMap.get(key);
        //新增变量 effectToRun 保存需要执行的副作用函数,避免无线递归循环
        const effectToRun = new Set();
        deps && deps.forEach(effect => {
            if (effect !== activeEffect) {
                effectToRun.add(effect);
            }
        });
        effectToRun.forEach(effect => effect());
    }
    // 封装一个 reactive 函数,实现对对象的拦截
    function reactive(obj) {
        return new Proxy(obj, {
            get(target, key) {
                track(target, key);
                return target[key];
            },
            set(target, key, newVal) {
                target[key] = newVal;
                trigger(target, key);
                return true;
            }
        });
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值