响应性复习

37 阅读4分钟

好久没看啦,都忘了,最近没啥事,捡起来再看看。

Reactive、Ref

核心内容

建立响应式数据与activeEffect之间的关系,进行依赖收集/触发。从而实现视图的更新。

计算属性

计算属性:依赖的响应性数据发生变化的时候才重新计算数据。

核心内容

第一次执行计算属性时会将脏变为false,并且进行计算属性的依赖收集+依赖的响应式数据的依赖收集。

依赖的响应式数发生改变时,会执行ComputedRefImpl的实例的effect.scheduler函数,将脏改为true,并重新执行计算属性的依赖触发,进而调整视图。

依赖收集

计算属性所在的函数fn,会被建立fn与ComputedRefImpl的dep关系。计算属性的fn(getter函数)会被建立响应式数据的dep关系。

依赖触发

依赖的响应式数据发生改变时,会调用ComputedRefImpl的实例的scheduler方法,也就是上图中的红框内容。然后脏变为true。触发当前ComputedRefImpl类的实例的依赖触发。重新执行effect函数的fn,获取数据的同时,重新收集依赖值。

原理图

计算属性依赖收集:

  • ComputedRefImpl 的 dep 收集 effect 的 fn
    • dep → fn

响应式数据依赖收集:

  • reactive/ref 的 dep 收集 computed 的 fn
    • dep → fn

依赖触发:

reactive/ref →执行effect.scheduler 将脏变为true,然后触发计算属性的依赖触发。进行数据的重新拿取。

脏:

  • 脏为true时 会执行run方法,即computed的参数fn;
  • 脏为false时 会执行依赖触发,即computed的依赖触发。

activeEffect指向问题:

  • 第一个是effect函数,与computed绑定关系
  • 第二个是计算属性的getter函数,与响应式数据绑定关系

侦听器

侦听器:数据源发生变化就执行一次回调函数。

复杂数据类型 reactive

核心内容

侦听器内部创建了一个effect实例,通过建立响应式数据与这个实例的关系(依赖收集/触发),来实现数据发生变化,就执行一次回调函数。

建立关系:通过getter函数中的traverse函数依次将reactive的key值与当前watch中的effect实例建立关系。任意一个变了都会执行依赖触发然后执行调度器内容。

调度器

watch 里,调度器先把 job 推入微任务队列,待本轮同步代码结束后flushJobs 统一执行;job 内部调用 effect.run() 取得最新值、对比新旧后触发用户回调 cb(newVal, oldVal),于是我们就能在 watch 里拿到带新旧值的回调。

image.png

所以这样的代码只会触发一次watch:

watch(count,cb(newValue,oldValue){
     ... 
})

count.value++          // 1. 同步修改
count.value++          // 2. 再次修改
// 3. 同步代码结束 → 微任务队列里已有 flushJobs
// 4. 微任务开始运行 → flushJobs 依次执行所有 job(包括你的 watch)

为什么只会执行最后一遍,是内部对等待的所有effect做了set去重复处理。

依赖收集

const baseGetter = getter
getter = () => traverse(baseGetter())

....

const effect = new ReactiveEffect(getter, scheduler)

if (cb) {
    if (immediate) {
        job()
    } else {
        oldValue = effect.run()
    }
} else {
    effect.run() // 其实就是执行getter函数,这里实现首次的依赖收集,traverse。
}

建立键与effect的关系。

依赖触发

响应式数据变更后触发依赖触发,然后执行调度器函数

// 旧值
let oldValue = {}
// job 执行方法
const job = () => {
    if (cb) {
        // watch(source, cb)
        const newValue = effect.run()
        if (deep || hasChanged(newValue, oldValue)) {
            cb(newValue, oldValue) // cb就是watch的第二个参数
            oldValue = newValue
        }
    }
}

// 调度器
let scheduler = () => queuePreFlushCb(job)

const effect = new ReactiveEffect(getter, scheduler)

watch任务队列

将所有任务统一处理,先收集 然后使用set去重,然后在执行。

内部使用了resolvedPromise

const resolvedPromise = Promise.resolve()

function queueFlush() {
    if (!isFlushPending) {
        isFlushPending = true
        currentFlushPromise = resolvedPromise.then(flushJobs)
    }
}

watch的执行顺序是,等所有同步执行结束在执行flushJobs

/**
 * 依次处理队列中的任务
 */
export function flushPreFlushCbs() {
    if (pendingPreFlushCbs.length) {
        // set去重复,同一数据源产生的多次副作用只执行做后一次。
        let activePreFlushCbs = [...new Set(pendingPreFlushCbs)] 

        // 清空就数据
        pendingPreFlushCbs.length = 0
        // 循环处理
        for (let i = 0; i < activePreFlushCbs.length; i++) {
            activePreFlushCbs[i]()
        }
    }
}

基本数据类型 ref

对于 ref(原始值),watch 就是通过 一次 .value 的读取 把自身 effect 登记进 RefImpl 的唯一 dep;后续任何 .value 重新赋值都会直接通知这个 dep,从而触发回调。

总结

computed:建立ComputedRefImpl与effect的fn的关系,ComputedRefImpl的参数fn建立与响应式数据的关系。响应式数据变更通过调用调度器来触发ComputedRefImpl 实例的依赖触发。

watch:内部生成effect实例,参数getter和scheduler,初次执行effect.run(等价于执行getter函数),进行依赖收集。侦听的数据源发生变化时,会触发effect.scheduler,在调度器中,queuePreFlushCb通过任务队列+Promise.resolve().then(jb)+set集合去重复,然后重新执行effect.run获取新值。执行cb回调,将新旧值传回去。