前情提要:上一期我们实现了基本的Effect和Reactive。
- Effect的作用是立即执行回调函数,并且设置activeEffect。
- Reactive类在getter中实现了依赖收集,将activeEffect加入dep;在setter中实现了依赖触发,遍历dep中的每一个effect,并执行run/scheduler方法。
- Effect函数需要返回runner,runner即是ReactiveEffect.run方法,执行runner能拿到回调函数的返回值。
- Effect函数可以传入一个options对象,其中包括Scheduler和onStop,它们都是回调函数。Scheduler的作用是延迟回调被执行的时间。
- Stop函数需要传入runner作为参数,在runner中保存effect属性,在effect中保存deps属性,然后将effect从每个dep中移除。Stop之后,即使修改了响应式对象的属性,effect也不会被触发。
这一次我们来实现Readonly,isReactive/isReadonly 以及优化Stop。
readonly,isReative / isReadonly
和reactive类似,都需要使用proxy,不同之处在于,readonly的属性只能访问不能修改。因此在实现上,getter中不做依赖收集,setter执行会抛出警告。
describe('readonly', () => {
it('happy path', () => {
const user = { age: 10 };
const readonlyUser = readonly(user);
expect(readonlyUser).not.toBe(user);
expect(readonlyUser.age).toBe(10);
expect(isReadonly(user)).toBe(false);
expect(isReadonly(readonlyUser)).toBe(true);
});
it('should call console.warn when set', () => {
console.warn = jest.fn();
const user = { age: 10 };
const readonlyUser = readonly(user);
readonlyUser.age++;
// 产生一次警告
expect(console.warn).toHaveBeenCalled();
});
});
此外,对代码稍加优化,把向proxy传入getter和setter的操作封装了起来,把new Proxy()封装成语义化更好的createReactiveObject函数。
isReactive/isReadonly实现的逻辑,就是去访问特定的属性,然后在getter中将该操作拦截下来,得到一个返回值。如果是原始对象,不会进入getter,那么会得到undefined,需要用!!转为false。
enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
export function reactive(raw) {
return createReactiveObject(raw, mutableHandlers);
}
export function readonly(raw) {
return createReactiveObject(raw, readonlyHandlers);
}
function createReactiveObject(target, baseHandlers) {
return new Proxy(target, baseHandlers);
}
export function isReactive(value) {
// 当传入原始对象时,直接访问该属性会得到undefined,需转换成false
return !!value[ReactiveFlags.IS_REACTIVE];
}
export function isReadonly(value) {
return !!value[ReactiveFlags.IS_READONLY];
}
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
function createGetter(isReadonly = false) {
return function (target, key) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
}
// 依赖收集
if (!isReadonly) {
track(target, key);
}
return Reflect.get(target, key);
};
}
function createSetter() {
return function (target, key, value) {
// 依赖触发
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
};
}
const mutableHandlers = {
get,
set
};
const readonlyHandlers = {
get: readonlyGet,
set: (target, _key, _value) => {
console.warn('warn: set attribute on readonly object', target);
return true;
}
};
优化Stop
之前Stop其实还存在bug,如果执行obj.prop = 3,dummy的值确实还是2。如果obj.prop++,dummy的值就会变成3,为什么会这样呢?
it('stop', () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
return 1;
});
obj.prop = 2;
expect(dummy).toBe(2);
stop(runner);
// obj.prop = 3
// 换成++以后,就不能通过了
obj.prop++;
expect(dummy).toBe(2);
// stopped effect should still be manually callable
const result = runner(); // 确保执行runner不会收集依赖
obj.prop++;
expect(result).toBe(1);
expect(dummy).toBe(3); // dummy的值不应该变成4
});
因为++操作的本质是先get,再set,即obj.prop = obj.prop + 1,在get中又执行了track,把effect添加进了dep中,set的时候又触发到该effect。
所以我们有如下几个目标:
- 使得默认情况下getter不收集依赖,只有当运行effect.run()时才track。
- 如果已经stop过,肯定无法通过setter -> trigger去执行回调了,但是可以拿到runner,通过runner()的方式,依然能够执行回调,而runner会调用effect.run(),可能引发track。所以run方法中需要添加一些逻辑,当检测到已stop时,避免track。
设置全局变量shouldTrack。
通过active属性判断是否已经stop,一般情况下在执行回调之前,先将shouldTrack设置为true,从而让回调在执行的过程中能收集依赖。如果已经stop,则直接执行回调(上一次run的最后已经将shouldTrack设置为false),这样runner()就不会触发依赖收集了。
run() {
// 通过
if (!this.active) {
return this._fn();
}
activeEffect = this;
// 在执行回调前,先设置shouldTrack
// 确保本次回调执行的过程中,如果访问了响应式对象的属性,依然能够track
shouldTrack = true;
const res = this._fn();
// 回调执行完毕,重置shouldTrack,使得默认情况下getter不收集依赖
shouldTrack = false;
return res;
}
getter依然会触发,但是可能不会track:
function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export function track(target, key) {
if (!isTracking()) return;
// ...
}