Skip to content

effect的调度

目前的effect函数的执行时机是不可控的,它随着响应式数据的改变而执行。但在某些情况下,我们希望按需执行effect,比如在某个时间段内只执行一次,或者将多个响应式数据的effect合并起来执行。

通过effect的调度器可以实现这些需求。

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(() => {
    const runner = effect(() => {
        wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    }, {
        scheduler: () => {
            console.log('不再自动更新,手动更新')
            console.log(runner, runner.effect)
            runner()
        }
    })

    setTimeout(() => {
        state.age++
    }, 1000)
})

</script>
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

    // 判断当前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()
    }
}

页面效果 页面效果

1s后:

页面效果

控制台输出 控制台输出

可以看到当前的数据变化后不再会触发 effect 的执行,而是通过 scheduler 函数来执行, 这样我们就可以在 scheduler 函数中做一些异步操作,比如发送网络请求等。