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.name = Math.random()
    })
})

</script>

控制台输出 控制台输出

像是这样的情况,在effect中对响应式对象的属性进行了修改,导致effect函数再次执行,从而形成递归调用。

js

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

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

    if (options) {
        Object.assign(_effect, options)
    }

    const runner = _effect.run.bind(_effect)
    runner.effect = _effect

    return runner
}

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
    /** 是否正在执行 */
    _running = 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)

            this._running++
            return this.fn()
        } finally {
            this._running--
            // 执行完成后,清除多余依赖收集表
            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正在执行那么就不用执行了,防止死循环
        if (effect._running) continue
        effect?.scheduler()
    }
}

通过加入一个_running来判断当前effect是否正在执行,如果正在执行那么就跳过,防止死循环