防止递归调用
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是否正在执行,如果正在执行那么就跳过,防止死循环