Skip to content

响应式数据伴侣 - reactiveEffect

构造函数

在之前的的实现中,effect函数是这样的

  • effect
ts
export let activeSub = null
// effect 函数用于注册副作用函数
// 执行传入的函数,并在执行期间自动收集依赖
export function effect(fn) {
    // 设置当前活跃的副作用函数,方便在 get 中收集依赖
    activeSub = fn
    // 执行副作用函数,此时会触发依赖收集
    fn()
    // 清空当前活跃的副作用函数
    activeSub = null
}

但是实际上,vue中需要考虑的问题比较多,所以 effect 函数中创建了一个类的实例,这个类就是 ReactiveEffect

  • effect
ts
class ReactiveEffect {
    constructor(public fn) {}

    run() {
        activeSub = this
         try {
            return this.fn()
        } finally {
            // fn 执行完毕后将 activeSub 回收
            activeSub = undefined
        }
    }
}
export function effect(fn) {
    // 创建一个 ReactiveEffect 实例
    const e = new ReactiveEffect(fn)
    e.run() // 执行 fn
}

那么此时对应的propagate函数中的依赖触发也要修改,因为此时activeSub已经变成了一个对象

  • system.ts
ts
/*
 * 传播更新的函数
 * @param subs
 */
export function propagate(subs) {
    let link = subs
    let queuedEffect = []
    while (link) {
        queuedEffect.push(link.sub)
        link = link.nextSub
    }

    queuedEffect.forEach((effect) => effect.run())
}

嵌套effect

看一个案例:

ts
const count = ref(0)

// effect1
effect(() => {
    //effect2 在 effect1 中执行
    effect(() => {
        console.log('effect2', count.value)
    })
    console.log('effect1', count.value)
})

setTimeout(() => {
    count.value = 1
}, 1000)

这个时候输出结果是:effect2,1

我们期望的是输出:effect2,1effect1, 1

看一下原因:

ts
class ReactiveEffect {
    // 表示当前是否被激活,如果为false则不收集依赖
    active = true
    constructor(public fn) {}

    run() {
        // 如果当前的effect未激活,那么就不收集依赖,直接返回fn的结果
        if(!this.active) {
            return this.fn()
        }
        // 得将当前的fn保存到全局,以便于收集依赖
        activeSub = this
        try {
            return this.fn()
        } finally {
            // fn 执行完毕后将 activeSub 回收
            activeSub = undefined // 🚨 fn 执行完毕后,被置空了
        }
    }
}
  • activeSub = undefined看一下这行代码,乍一看,似乎没有什么问题。
  • 但是,回到案例中看一下,当effect1执行的时候,activeSub = effect1,然后effect1中又创建了一个effect2,此时,执行effect2run方法,然后马上activeSub又变成了effect2,等effect2执行完毕后,将activeSub设置为undefined,但是此时effect1还没执行完。后面访问ref就不会被收集了。
  • 所以当一个effect执行完毕后,不能把它设置为undefined
  • activeSub = this,也就是effect2执行之前,activeSub是有值的,在effect2执行前,它的值是effect1,把它存起来,等activeSub执行完毕后,在重新赋值。
ts
class ReactiveEffect {
    // 表示当前是否被激活,如果为 false 则不收集依赖
    active = true
    constructor(public fn) {}

    run() {
        // 💡 保存之前的 activeSub
        const prevSub = activeSub
        // 将当前的 effect 保存到全局,以便于收集依赖
        activeSub = this
        try {
            return this.fn()
        } finally {
            // 💡 fn 执行完毕后将 activeSub 恢复为 prevSub
            activeSub = prevSub
        }
    }
}
  • 这样在执行activeSub = this之前,先将他保存起来,等fn执行完毕后,在将它重新恢复,这样嵌套的问题就解决了。
  • 如果没有嵌套的情况下,第一次执行的时候,prevSub就是undefined
ts
const count = ref(0)

// effect1
effect(() => {
  // 🚨 effect2 在 effect1 中执行
  effect(() => {
    console.log('effect2', count.value)
  })
  console.log('effect1', count.value)
})

setTimeout(() => {
  count.value = 1
}, 1000)

此时这段代码在定时器修改完后会正常打印出:

  • 'effect2' 1
  • 'effect1' 1

调度器(scheduler)

调度器是响应式系统一个重要概念,我们默认使用effect访问响应式属性的时候,会收集依赖,修改响应式属性后,这个effectfn会重新执行,而scheduler的作用是,当响应式数据发生变化的时候,执行scheduler,而不是重新执行fn。在创建effect的时候,还是会执行fn,因为要靠他收集依赖。

ts
const count = ref(0)

effect(() => {
    console.log('在 fn 中收集了依赖', count.value)
}, {
    scheduler() {
        console.log('scheduler', count.value)
    }
})

setTimeout(() => {
  // ⭐️ 由于传递了 scheduler ,所以我们更新响应式属性的时候,会触发 scheduler
  count.value++ // scheduler
}, 1000)

如何实现功能:

  • 默认:effect在创建时会执行一次fn,当fn中访问的响应式数据发生变化时,会重新执行,无论初始化,还是数据发生变化,都会重新执行fn
  • 调度器:当传递了scheduler,首次创建effect的时候,依然会执行fn,但是当数据发生变化的时候,就会执行scheduler,也就是说响应式数据更新的时候,不能执行fn了。
  • 或者说可以这样,ReactiveEffect本身就存在scheduler,这个方法默认会调用run方法,但是如果传递了scheduler,调用对象本身的scheduler
ts
class ReactiveEffect {
    constructor(public fn) {}

    run() {
        const prevSub = activeSub

        // 每次执行 fn 之前,把 this 放到 activeSub 上面
        activeSub = this

        try {
            return this.fn()
        } finally {
            // 执行完成后,恢复之前的 effect
            activeSub = prevSub
        }
    }

    /**
     * 默认调用 run,如果用户传了,那以用户的为主,实例属性的优先级,由于原型属性
     */
    scheduler() {
        this.run()
    }
}

export function effect(fn, options) {
    const e = new ReactiveEffect(fn)
    // 将传递的属性合并到 ReactiveEffect 的实例中
    Object.assign(e, options)
    // 执行 run 方法
    e.run()
}

此时需要修改propagate中执行的方法也需要修改一洗,因为之前执行的是run方法

搞一个 notify 方法,我只管调用你的 notify 方法,至于你最终执行那个方法,你自己决定:

ts
/**
 * 传播更新的函数
 * @param subs
 */
export function propagate(subs) {
  // 省略部分代码...

  // 这里执行 notify 方法
  queuedEffect.forEach((effect) => effect.notify())
}

ReactiveEffect 中添加一个notify方法

ts
class ReactiveEffect {
    // 省略部分代码...

    /**
     * 通知更新的方法,如果依赖的数据发生了变化,会调用这个函数
     */
    notify() {
        this.scheduler()
    }

    // 省略部分代码...
}

总结:

因为例属性优先级,优于原型属性 所以在用户传了scheduler的时候,notifythis.scheduler,就调用了传入scheduler。在没有传入的时候,调用了内部scheduler,然后内部scheduler调用run

Released under the MIT License.