再不会Watch、Effect,给你一嘴巴子

240 阅读3分钟

概述

今天我们研究副作用函数,有人提出疑问:副作用函数是个啥?

举例:小明有十个暗恋对象,十个人都是未婚,那她们每个人都有可能和小明结婚。有一天,其中一个名叫刘大漂亮嫁给隔壁村的王二狗。刘大漂亮和王二狗结婚没有对小明造成直接影响,但小明能结婚的选择却少了一个。

代码举例: 我们把id为name的dom内容改成了'我是王二狗',那么所有使用到它的地方获取到的值都会发生变化。

const dom = document.getElementById('name');
dom.innerHTML = '我是王二狗'

effect副作用函数

effect和watch本质上是一样的,今天我们用vue3种的effect举例,搞起。

最简单的effect

声明一个对象,当对象中的内容发生变化时,就触发一个函数。

function effectFn() {
    console.log('effectFn被触发了')
}
const data = {
    ok: true,
    text: '哈哈哈'
}
const proxyData = new Proxy(data, {
    get(target, key) {
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal;
        effectFn()
    }
})
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23

上面的例子很简单,问题也很多:

  1. effectFn是固定的。
  2. 对象的key并没有触发使用过自己的函数,只能触发同一个函数。

动态的effect

下面我们解决这个问题,先看副作用响应系统设计图:

WechatIMG241.jpeg

  1. 利用WeakMap以target对象为key,Map为值。
  2. 再已key为Map的key,set为值。这样每个key都能存储到使用过自己的函数。
  3. 当自己被修改时,把储存在set中的函数全部执行一遍。
let activeEffect;

function effect(fn) {
    activeEffect = fn;
    fn()
}
const data = {
    ok: true,
    text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
    get(target, key) {
        let map = bucket.get(target);
        if (!map) {
            bucket.set(target, map = new Map())
        }
        let set = map.get(key);
        if (!set) {
            map.set(key, set = new Set())
        }
        set.add(activeEffect);
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal;
        let map = bucket.get(target);
        if (!map) return;
        let set = map.get(key);
        set.forEach(fn => fn())
    }
})

effect(() => {
    console.log(proxyData.ok ? proxyData.text : 321321321)
})
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23

依旧存在问题: proxyData.ok = false后,函数中便不再使用到proxyData.text,但是proxyData.text改变时,函数依旧会执行。马上解决它,搞起!!!

进阶版effect

  1. effectFn函数添加一个dep属性值为数组,同时去收集执行时使用到的所有key的set。 设计图为: WechatIMG243.jpeg
  2. 每次effectFn执行前,会遍历所有收集到的set,然后把自己删除。此时,key被修改时便不会执行effectFn。
  3. 但每次删除之后函数会再次执行,此时用到的key将会再次将函数收集。此时不在用到text,之后text被修改,也就不在触发函数。
let activeEffect;

function effect(fn) {
    const effectFn = () => {
        cleaup(effectFn)
        activeEffect = effectFn;
        fn()
    }
    effectFn.dep = [];
    effectFn()
}
function cleaup(effectFn) {
    for (let i = 0; i < effectFn.dep.length; i++) {
        const dep = effectFn.dep[i];
        dep.delete(effectFn)
    }
    effectFn.dep.length = 0
}
const data = {
    ok: true,
    text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
    get(target, key) {
        let map = bucket.get(target);
        if (!map) {
            bucket.set(target, map = new Map())
        }
        let set = map.get(key);
        if (!set) {
            map.set(key, set = new Set())
        }
        set.add(activeEffect);
        activeEffect.dep.push(set);
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal;
        let map = bucket.get(target);
        if (!map) return;
        let set = map.get(key);
        set.forEach(fn => fn())
    }
})

存在的问题: 运行之后会发现,无限循环。因为被修改时,遍历set执行了里面所有收集的函数。而执行函数时又会在set中添加函数。就如同:

const set = new Set([1]);
set.forEach(() => {
    set.delete(1)
    set.add(1)
    console.log('遍历中') 
})

完整版effect

只需要重新声明一个set就可以解决无限循环的问题:

const set = new Set([1]);
const newSet = new Set(set)
newSet.forEach(() => {
    set.delete(1)
    set.add(1)
    console.log('遍历中') 
})

完整代码

let activeEffect;

function effect(fn) {
    const effectFn = () => {
        cleaup(effectFn)
        activeEffect = effectFn;
        fn()
    }
    effectFn.dep = [];
    effectFn()
}
function cleaup(effectFn) {
    for (let i = 0; i < effectFn.dep.length; i++) {
        const dep = effectFn.dep[i];
        dep.delete(effectFn)
    }
    effectFn.dep.length = 0
}
const data = {
    ok: true, 
    text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
    get(target, key) {
        let map = bucket.get(target);
        if (!map) {
            bucket.set(target, map = new Map())
        }
        let set = map.get(key);
        if (!set) {
            map.set(key, set = new Set())
        }
        set.add(activeEffect);
        activeEffect.dep.push(set);
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal;
        let map = bucket.get(target);
        if (!map) return;
        let set = map.get(key);
        const effectsToRun = new Set(set);
        effectsToRun.forEach(fn => fn())
    }
})

effect(() => {
    console.log(proxyData.ok === true ? proxyData.text : 321321321)
})
console.log(activeEffect)
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23