本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
hello 大家好,🙎🏻♀️🙋🏻♀️🙆🏻♀️
我是一个热爱知识传递,正在学习写作的作者,ClyingDeng 凳凳!
今儿,咱们接着分析这个 reactiveEffect 其他的功能方法。比如:在响应式数据中,不渲染的数据不在页面展示是否还需要再次渲染;响应数据能否自己手动暂停等。
分支切换
问:在响应式数据中,不渲染的数据不在页面展示是否还需要再次渲染?🙋🏼♀️🙋🏼♀️🙋🏼♀️
答:不展示在页面的数据,发生变化应该不会重新渲染。🙅🏼♀️🙅🏼♀️🙅🏼♀️
举个栗子🌰:
<script src="../../../../node_modules/@vue/reactivity/dist//reactivity.global.js"></script>
<script>
const { reactive, effect } = VueReactivity
const obj = {
flag: true,
name: 'dy',
age: 25,
get fn() {
return this.age // 25
}
}
const state = reactive(obj)
// 分支切换
effect(() => {
// 每次执行effect的时候都需要清理依赖重新收集
console.log('render');
document.getElementById('app').innerHTML
= state.flag ? '姓名' + state.name : '年龄' + state.age
})
setTimeout(() => {
state.flag = false
setTimeout(() => {
console.log('修改name,应该不更新');// 执行后应该不再输出render
state.name = 18
}, 1000);
}, 1000)
</script>
使用vue3自身的effect,我们可以看出,在执行完flag值变化后,再次修改name后,页面并没有重新渲染。
很好,没有👀!
vue3能实现,那么我自己也来搞一个呗~
先看我们之前实现的能不能直接使用:
原本不要更新的,又再次执行了effect。很显然,是我们想多了-_-!
肯定需要功能补充的啊!
我们可以看出,在数据发生变化后都会重新渲染一次页面,但其实我们只需要监听页面需要渲染的属性。
源码中是这样判断的:
// effect文件 run()函数
try {
// 将effect和稍后渲染的属性关联在一起
this.parent = activeEffect
activeEffect = this
shouldTrack = true
// 到某一层就左移
trackOpBit = 1 << ++effectTrackDepth // effectTrackDepth当前effect递归的层数
// 递归层数没有超过最大深度30
if (effectTrackDepth <= maxMarkerBits) {
// 收集依赖
initDepMarkers(this)
} else {
// 否则清除
cleanupEffect(this)
}
return this.fn() // 执行用户自己回调
} finally {
// 退出effect嵌套 finally 在return前操作 所以退出时进行了去重依赖的操作
if (effectTrackDepth <= maxMarkerBits) {
// 清除重复依赖
finalizeDepMarkers(this)
}
// 某一层执行完毕右移
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined // 递归到最外层置空parent
if (this.deferStop) {
this.stop() // 停止收集
}
}
在不超过最大嵌套深度的情况下,每个属性的依赖,通过位运算做区分(上篇文中🈶️提及)。在超出最大嵌套深度的时候,就直接清除相关属性的依赖。
简单点,我们可以这样:在执行用户回调函数时,我们先清空当前依赖,然后再重新收集。这里直接采用的是源码中超出最大嵌套深度后的cleanupEffect方法。直接清空当前属性依赖,然后重新收集。
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect // deps 里面装的是name对应的effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect) // 解除相关effect 重新依赖收集
}
deps.length = 0 // 如果直接将deps置空,只是清空数组,但是set中的name依旧存在
}
}
cleanupEffect通过传入的effect中的deps,去遍历相关属性依赖的deps,并将其effect清除。
在依赖触发的时候,去重新收集依赖。
export function trigger(target, type, key, value, oldValue) {
// 判断targetMap是否存在target
// 不存在 直接返回 不需要收集
// 存在 取depsMap中对应key的effect 执行run
const depsMap = targetMap.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
if (effects) {
effects = [...effects] // effects 中 set结构删除再添加会导致死循环
effects.forEach(effect => {
// 在执行effect时,又要执行自己,需要屏蔽自己的effect
if (effect !== activeEffect)
effect.run()
});
}
}
现在,我们可以看出name即使修改了,也不会再次执行effect✌️✌️✌️
这里的清除方法与普通数组不同的是:在deps中存放的是属性对应的相关依赖的set集合。如果直接赋值成空数组deps = [],其内部的set集合中的属性依旧存在。所以我们需要通过遍历删除相关属性依赖。
此外,set集合遇到遍历还有个特点,就是在遍历的时候,删除后再新增,会导致死循环。
大家可以在本地自己跑下下面这段代码:
let sets = new Set(['a'])
sets.forEach(item => {
sets.delete('a');
sets.add('a')
console.log(sets);
})
控制台会无限输出这个set集合。
所以我们在清空之后,需要重新赋值一下我们的effects,effects = [...effects]相当于我们的effect又是一个新的effects数组,新的数组重新进行遍历。
调度器
我们可以上面的效果,间隔一秒后渲染了响应式数据的其他属性。 那我们是不是也可以实现手动的将effect停止,修改属性后,继续执行effect呢?
答案是肯定可以的!
比如这个例子:
// index.html 文件
let runner = effect(() => {
document.getElementById('app').innerHTML = '年龄' + state.age
})
// effectScope
runner.effect.stop()
setTimeout(() => {
state.age = 18
setTimeout(() => {
runner()
}, 2000)
}, 1000);
我们在手动停止effect监听,修改age属性值后,再次手动执行effect。发现页面上的数据间隔三秒后,就可以重新渲染了。
可以看到下面的动画效果:
这边主要的核心功能涉及到的函数就是effect的stop,和 effect 的run函数。
effect返回值
我们需要手动执行effect函数,那我们就需要用个变量存储,在合适的时机再次执行,这说明effect是有返回值的。并且这个返回值需要执行的其实就是执行effect 的run函数🙇♀️🙇♀️🙇♀️
export function effect(fn) {
// fn可以根据状态变化 重新执行,effect可以嵌套
const _effect = new ReactiveEffect(fn) // 创建响应式的effect 数据发生变化会重新执行该函数
// 默认先执行一次
_effect.run()
const runner = _effect.run.bind(_effect)// 绑定this 执行
runner.effect = _effect // 将effect挂载到runner函数上
return runner
}
既然需要执行run函数,那么就直接将effect作为返回值。只是这里需要注意的是内部的this指向问题。
源码中的思路跟我们的其实是一致的(当然一致,参考的源码啊🤔),哈哈~
effect的stop
手动停止effect,我们简单化一点,就是将effect的激活状态改为false,停止监听。
stop() {
if (this.active) {
this.active = false
cleanupEffect(this) // 停止effect收集
}
}
我们实现的只是简版的,在源码中还会牵扯到自身停止状态、通过effect第二个参数传入的options中的onStop。
调度器
上面的隔一段时间,停止监听后,修改参数,再开启监听,我们也可以写自己的调度函数scheduler。
既然谈及effect第二个参数,那么我们可以看看这个
options。
export interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
export interface ReactiveEffectOptions extends DebuggerOptions {
lazy?: boolean // 是否缓存
scheduler?: EffectScheduler // 调度
scope?: EffectScope
allowRecurse?: boolean
onStop?: () => void
}
通过源码的options接口定义可以看出有很多可传递的参数。我们可以先了解scheduler这个函数,onStop函数与此类似。
我们可以在创建_effect实例的时候,类似用户自身回调传入方法,将我们的调度函数传入,在ReactiveEffect构造函数中接收。
// ReactiveEffect 类
constructor(public fn, public scheduler) { } // fn 用户自定义回调函数
参数接收有了,那么就剩使用了🤪🤪🤪
自己的调度函数肯定在触发的时候执行的呀!
毋庸置疑了,trigger!
effect遍历执行的时候,我们需要判断scheduler属性是否存在:
export function trigger(target, type, key, value, oldValue) {
// 判断targetMap是否存在target
// 不存在 直接返回 不需要收集
// 存在 取depsMap中对应key的effect 执行run
const depsMap = targetMap.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
if (effects) {
effects = [...effects] // effects 中 set结构删除再添加会导致死循环
effects.forEach(effect => {
// 在执行effect时,又要执行自己,需要屏蔽自己的effect
if (effect !== activeEffect) {
if (effect.scheduler)
effect.scheduler() // 如果存在自己的调度函数就执行自己的scheduler
else effect.run() // 否则就执行run
}
});
}
}
依旧是参照源码,在triggerEffect执行的时候判断scheduler:
综上,我们的第二个参数的调度函数就可以拥有和vue3一样的调度功能了。其最大的用途,应该就是类似批处理。在我们多次修改属性的时候,我们只需最后的值时,我们就可以通过这个scheduler去实现。
就像年龄,不管长了几岁,永远18岁~
注意
如果options上一定还存在其他属性,我们可以通过继承的方法将其合并🧐🧐🧐
// options
{
scope: true,
scheduler() { // 调度器 如何更新自己决定
if (!isWait) {
isWait = true
setTimeout(() => {
runner()
}, 3000);
}
console.log('执行自己的 scheduler');
}
这里的参数合并使用的是export const extend = Object.assign。在我们额外传入的scope参数,与之前原有的参数进行了合并,并返回到内部effect上。
补充
// 更新
export function trigger(target, type, key, value, oldValue) {
// 判断targetMap是否存在target
// 不存在 直接返回
// 存在 取depsMap中对应key的effect 执行run
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => {
// 在执行effect时,又要执行自己,需要屏蔽自己的effect
if (effect !== activeEffect)
effect.run()
});
}
其实,上篇文章中就已经提到了一个effect不能快速执行n次,其实代码就包含在上文中。我们在遍历执行effect时,需要将自身的effect忽略不执行。
感兴趣的朋友可以关注 手写vue3系列 专栏或者点击关注作者我哦(●'◡'●)!。 如果不足,请多指教。