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 函数中做一些异步操作,比如发送网络请求等。