好久没看啦,都忘了,最近没啥事,捡起来再看看。
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 里拿到带新旧值的回调。
所以这样的代码只会触发一次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回调,将新旧值传回去。