Skip to content

依赖清理

在上一节中,我们实现了一个简单的响应式系统,但是它存在一些问题,比如:

vue
<template>
    <div ref="wrapper"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { effect, reactive } from '../source/reactivity'

const wrapper = ref(null)

const state = reactive({
    name: '小李大人',
    age: 18,
    flag: true
})

onMounted(() => {
    // effect(() => {
    //     wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    // })

    effect(() => {
        console.log(state)
        wrapper.value.innerHTML = state.flag ? `姓名:${state.name}` : `年龄:${state.age}`
    })

    setTimeout(() => {
        state.flag = !state.flag

        setTimeout(() => {
            state.name = 'lordly'
        }, 1000)
    }, 1000)
})

</script>

页面效果 页面效果

1s后:

页面效果

控制台输出 控制台输出

这里面有一个很明显的问题就是:

当我在第一次执行effect的时候,state.flagtrue会对state.name进行依赖收集,然后在1s后我修改了state.flag的值,此时重新执行effect的时候使用到的是state.age,并没有使用到state.name,但是state.name的依赖并没有被清除,所以当state.name的值发生改变的时候,effect函数还会重新执行。但是其实这里是应该要清除掉state.name的依赖的,因为state.name已经不再被effect函数所使用了。

所以我们需要在effect函数执行的时候,把上一次的依赖关系给清除掉,然后再进行依赖收集。

js
effect(() => {
    wrapper.value.innerHTML = state.flag ? `姓名:${state.name} ${state.name} ${state.name}` : `年龄:${state.age}`
})

同时,像是上面这样的代码,state.name被使用了3次,但是其实我们只需要收集一次依赖,所以我们需要在依赖收集的时候,对相同的依赖进行去重。

js

/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

function preCleanEffect(effect) {
    // 将effect的deps长度置0
    effect._depsLength = 0
    // 将_trackId加1
    // 如果是同一个effect执行,那么_trackId就会是相同的
    effect._trackId++
}

class ReactiveEffect {
    /** 记录当前effect执行的次数 */
    _trackId = 0
    /** 依赖收集表 */
    deps = []
    /** 依赖收集表的长度 */
    _depsLength = 0

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        let lastEffect = activeEffect
        try {
            activeEffect = this

            // 执行前,先清空依赖收集表

            preCleanEffect(this)

            return this.fn()
        } finally {
            activeEffect = lastEffect
        }
    }
}

function cleanDepEffect(dep, effect) {
    // 将effect从dep中移除
    dep.delete(effect)
    // 如果删除后,dep的长度为0,那么就删除dep
    if (dep.size <= 0) {
        dep.cleanUp()
    }
}

// 依赖收集
export function trackEffect(effect, dep) {
    // 收集依赖,将不需要的依赖移除
    // 如果用一个effect收集了多次,那么就只保留第一次收集的依赖
    if (dep.get(effect) !== effect._trackId) {
        dep.set(effect, effect._trackId)

        // 因为代码的执行都是按照顺序执行的,所以这里的依赖dep也是按照顺序写入到 effect.deps 中的
        // 由于在每一次执行前都进行了清空操作,所以这里的 effect._depsLength 的值是 0
        // 所以这里将当前的 effect 在当前这次执行中的依赖取出来,跟新的依赖进行比较
        // 如果新旧的依赖发生变化了,那么就用新的依赖覆盖旧的依赖,反之则将 effect.depsLength + 1 跳过
        const oldDep = effect.deps[effect._depsLength]
        if (oldDep !== dep) {
            // 如果旧的依赖存在,那么就清空旧的依赖
            if (oldDep) {
                cleanDepEffect(oldDep, effect)
            }
            effect.deps[effect._depsLength++] = dep
        } else {
            effect._depsLength++
        }
    }
}

// 触发更新
export function triggerEffects(dep) {
    // 遍历依赖收集表,执行effect
    for (const effect of dep.keys()) {
        effect?.scheduler()
    }
}

为了解决同一个属性在同一个effect中被多次调用导致这个属性被多次收集的问题,我们在每一次执行前都对effect._trackId进行了+1操作, 这样一来在同一个effect中,如果同一个属性被多次调用,那么effect._trackId的值就会是相同的,接下来在trackEffect中根据判断dep.get(effect)effect._trackId是否相等就可以判断这个属性是否已经被收集了。

另外对于以前收集过,但是当前这一次effect中没有使用到的属性的依赖,采用了一个简单的diff算法来解决这个多余的依赖的问题; 首先在每一个effect中都保存了它的所有dep也就是effect.deps,以及它的长度effect._depsLength, 我们在每一次执行前都将effect._depsLength设置为0,然后在trackEffect中使用effect.deps[effect._depsLength]来获取对应顺序的依赖收集表oldDep,用它来跟这一次新的依赖收集表dep进行比较,如果是同一个收集表,那么直接跳过不做处理; 反之,两次的收集表不相同,说明了这个属性的收集表以前不存在或者当前执行顺序下的收集表对应的属性发生了变化,如果是有oldDep的存在我们则是将oldDep中对于当前effect的记录删除,如果删除完成后oldDep是一张空表了,那么就说明这个属性没有被任何的effect所依赖, 那么就可以直接将这个oldDepdepsMap(对象的依赖收集表)中删除,反之则不做处理,最后将新的dep添加到effect.deps的对应位置,并将effect._depsLength的值+1

控制台输出 控制台输出

从更改后的控制台打印信息看到,最后一次对state.name的更改已经不会再触发effect的执行了,因为state.name的依赖收集表dep中已经没有effect的记录了。

然后还有一个剩余的问题: 假定effect.deps是如下的结构:[flag,name,xxx,xxx]flag改变后的依赖属性表变成了:[flag, age]

最后应该是需要将多余的属性从effect.deps中删掉的,但是当前的diff算法并没有做到这一点,所以需要改进一下diff算法。

在每次执行完成后,将effect.deps中多余的属性删除,也就是effect.depseffect._depsLength之后的所有属性都删除掉。

js
/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

function preCleanEffect(effect) {
    // 将effect的deps长度置0
    effect._depsLength = 0
    // 将_trackId加1
    // 如果是同一个effect执行,那么_trackId就会是相同的
    effect._trackId++
}

function postCleanEffect(effect) {
    // 如果effect.deps.length > effect._depsLength,说明有多余的依赖
    if (effect.deps.length > effect._depsLength) {
        for (const i = effect._depsLength; i < effect.deps.length; i++) {
            // 删除多余的依赖
            cleanDepEffect(effect.deps[i], effect)
        }
        // 更新列表的长度
        effect.deps.length = effect._depsLength
    }
}

class ReactiveEffect {
    /** 记录当前effect执行的次数 */
    _trackId = 0
    /** 依赖收集表 */
    deps = []
    /** 依赖收集表的长度 */
    _depsLength = 0

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        let lastEffect = activeEffect
        try {
            activeEffect = this

            // 执行前,先清空依赖收集表
            preCleanEffect(this)

            return this.fn()
        } finally {
            // 执行完成后,清除多余依赖收集表
            postCleanEffect(this)

            activeEffect = lastEffect
        }
    }
}

function cleanDepEffect(dep, effect) {
    // 将effect从dep中移除
    dep.delete(effect)
    // 如果删除后,dep的长度为0,那么就删除dep
    if (dep.size <= 0) {
        dep.cleanUp()
    }
}

// 依赖收集
export function trackEffect(effect, dep) {
    // 收集依赖,将不需要的依赖移除
    // 如果用一个effect收集了多次,那么就只保留第一次收集的依赖
    if (dep.get(effect) !== effect._trackId) {
        dep.set(effect, effect._trackId)

        // 因为代码的执行都是按照顺序执行的,所以这里的依赖dep也是按照顺序写入到 effect.deps 中的
        // 由于在每一次执行前都进行了清空操作,所以这里的 effect._depsLength 的值是 0
        // 所以这里将当前的 effect 在当前这次执行中的依赖取出来,跟新的依赖进行比较
        // 如果新旧的依赖发生变化了,那么就用新的依赖覆盖旧的依赖,反之则将 effect.depsLength + 1 跳过
        const oldDep = effect.deps[effect._depsLength]
        if (oldDep !== dep) {
            // 如果旧的依赖存在,那么就清空旧的依赖
            if (oldDep) {
                cleanDepEffect(oldDep, effect)
            }
            effect.deps[effect._depsLength++] = dep
        } else {
            effect._depsLength++
        }
    }
}

// 触发更新
export function triggerEffects(dep) {
    // 遍历依赖收集表,执行effect
    for (const effect of dep.keys()) {
        effect?.scheduler()
    }
}