readonly
readonly 顾名思义就是只可读,不可改,对比正常的响应式对象,我们可以得到它们的差异
编辑
因为readonly只可读不可改,所以我们对于set改值操作自然可以忽略,并且不需要再收集依赖了,我们依赖收集的目的就是为了在响应式数据发生变化时,触发依赖,达到数据视图实时同步的目的,既然数据不可变,那我们也不需要再收集了,那么我们就可以书写代码了
export function readonly(raw) { return new Proxy(raw, { get(target, key) { const res = Reflect.get(target, key) return res }, set(target, key, value) { return true } }) }
stop
stop的功能是当我们调用stop函数时,取消对响应式数据的监听,如何做到这个呢,我们一起来想一下思路,我们如何对数据实现监听的呢,肯定是跟依赖收集和触发有关,因为在我们的dep中存放着很多依赖,当我们触发set时,会将dep中的依赖取出来执行,因此我们可以利用一个思路,当我们调用stop之后,可以将收集的依赖清空,这样它没有取不出来东西就执行不了,就达到了停止监听的效果,顺着这个思路,我们来实现一下
我们上一节提到过在调用run方法时,activeEffect会把reactiveEffect这个类的实例绑定到自身,并且this指向也会变成它,这样我们可以在收集依赖的时候往他身上也存一份,当调用stop函数时,我们也就调用了其上的stop方法来清空依赖
我们希望的效果是这样:
编辑
清空依赖
class reactiveEffect { private _fn: any //用于将那个依赖存进去 deps = [] constructor(fn) { this._fn = fn } run() { activeEffect = this return this._fn() } stop() { //这里存放的dep是set结构,所以需要delete来操作 this.deps.forEach((dep: any) => { dep.delete(this) }) } }
let activeEffect export function effect(fn) { const _effect = new reactiveEffect(fn) const runner: any = _effect.run.bind(_effect) runner._effect = _effect return runner } const targetMap = new WeakMap() export function track(target, key) { const depMap = targetMap.get(target) if (!depMap) { const depMap = new Map() targetMap.set(target, depMap) } const dep = depMap.get(key) if (!dep) { const dep = new Set() depMap.set(key, dep) } dep.add(activeEffect) //这里我们将依赖存一份 activeEffect.deps.push(dep) }
javascript
复制代码
export function stop(runner) { runner._effect.stop() }
这样我们就实现了stop函数的功能
停止依赖收集
但是这里有个问题,当我们再次改变reactive数据的值的时候,发现还会收集依赖,触发依赖,那我们这样做的话,清除依赖就没有意义了,所以要在源头上停止依赖的收集
我们可以用一个变量shouldTrack来控制是否收集依赖
const targetMap = new WeakMap() let shouldTrack = true export function track(target, key) { //没有依赖时 if (!activeEffect) return //不应该收集依赖时 if (!shouldTrack) return const depMap = targetMap.get(target) if (!depMap) { const depMap = new Map() targetMap.set(target, depMap) } const dep = depMap.get(key) if (!dep) { const dep = new Set() depMap.set(key, dep) } dep.add(activeEffect) //这里我们将依赖存一份 activeEffect.deps.push(dep) }
class reactiveEffect { private _fn: any //用于将那个依赖存进去 deps = [] active = true onStop?: () => void constructor(fn, public schduler?: Function) { this._fn = fn this.schduler = schduler } run() { if (!this.active) { return this._fn() } shouldTrack = true activeEffect = this const res = this._fn() shouldTrack = false return res } stop() { if (this.active) { this.active = false //这里存放的dep是set结构,所以需要delete来操作 clearEffect(this) if (this.onStop) { this.onStop() } } } }
当我们调用stop时,active会变成false,所以下一次执行fn的时候,我们会判断一下active的状态,如果时是false我们会直接返回fn的值,不做依赖收集。 我们这里声明了一个shouldTrack变量,
当我们没有调用stop时,active是true,因此run函数执行时会做依赖收集并且每一次收集前会将shouldTrack的状态初始化为true,再依赖收集结束后,会将shouldTrack状态变成false,这样如果我们调用stop时,shouldTrack状态一直都是false,那我我们执行track函数的时候都会跳过依赖的收集,就实现了在源头上控制了依赖的停止收集
优化代码
对于stop函数,我们调用一次后就已经清空了依赖,有的用户可能一直调用,这样我们后面再清空都是undefined了,没有必要,所以我们可以拿个active标记,来控制是否调用stop
class reactiveEffect { private _fn: any //用于将那个依赖存进去 deps = [] active = true constructor(fn) { this._fn = fn } run() { activeEffect = this return this._fn() } stop() { if (this.active) { this.active = false //这里存放的dep是set结构,所以需要delete来操作 clearEffect(this) } } }
function clearEffect(effect) { effect.deps.forEach((dep: any) => { dep.delete(effect) }) }
写在最后
本章讲解了如何实现readonly以及stop,主要是思路的实现,可能没有测试不是很直观,之后将jest测试也加入进来,通过白盒测试,从TDD(测试驱动开发)的思想更加直观的来理解vue3的源码