01、集成 jest 测试环境
bash
初始化tsconfig.json 文件
npx tsc --init
编写一个 ts 函数,和编写一个测试用例,并让其通过,使用jest
这个库。
安装 jest
bash
yarn add --dev jest
因为jest
默认使用的是 commonjs 规范,所以我们需要使用babel
来进行转换,
bash
yarn add --dev babel-jest @babel/core @babel/preset-env
使用ts
bash
yarn add --dev @babel/preset-typescript
yarn add --dev @types/jest
在项目下面新建babel.config.js
js
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};
编写一个ts
函数
关闭强制检查any
tsconfig.json
ts
"noImplicitAny": false
"types": ["jest"],
编写一个 index.ts
代码
ts
export function add(a, b) {
return a + b;
}
编写index.spec.ts
ts
import { add } from "..";
it("init", () => {
expect(add(1, 4)).toBe(5);
});
添加 script
脚本
json
"scripts": {
"test": "jest"
},
然后在命令行执行yarn test
, 通过则表示测测试环境搭建成功了。
02、实现effect&reactive 依赖收集&触发更新
- 怎么实现依赖收集呢?
其实就是用一个容器,把effect
里面传递的这个fn
函数收集起来,就可以了。
- 怎么拿到当前正在执行的依赖信息呢?
在这里我们使用一个全局变量activeEffect
在运行的时候来记录它。然后在别的地方使用。
-
用一个
类
来抽象出effect
叫做EffectEffect
. -
在
get
里面进行依赖收集track
, 在set
里面进行trigger
目标实现下面的这两个测试用例
reactive
ts
import { reactive } from "../reactive";
describe("reactive", () => {
test("Object", () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
// get
expect(observed.foo).toBe(1);
// has
expect("foo" in observed).toBe(true);
// ownKeys
expect(Object.keys(observed)).toEqual(["foo"]);
});
});
effect.spec.ts
ts
import { reactive } from "../reactive";
import { effect } from "../effect";
describe("effect", () => {
it("should observed basic properties", () => {
let dummy;
const counter = reactive({ num: 0 });
// init setup
effect(() => {
dummy = counter.num;
});
expect(dummy).toBe(0);
// update
counter.num = 10;
expect(dummy).toBe(10);
});
});
好接下来我们实现reactive.ts
ts
import { track, trigger } from "./effect";
export function reactive(raw) {
return new Proxy(raw, {
get(target, key, receiver) {
// 解释一下 receiver 这个参数, Proxy 或者继承 Proxy 的对象
// 也就是代理后产生的新proxy对象
let res = Reflect.get(target, key, receiver);
//先读,再依赖收集
track(target, key);
return res;
},
set(target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver);
// 先设置,再触发更新
trigger(target, key);
return result;
},
});
}
effect.ts
要点
- 用
ReactiveEffect
包装依赖函数 - 存储依赖新的的
targetMap
Map变量,vue
使用的WeakMap
- 全局变量
activeEffect
用于在依赖函数执行前记录依赖,方便后面再track
函数中使用,当依赖函数执行完成之后还必须的重置为null
保证activeEffect
只是在依赖韩式执行的时候有用
。因为js是单线程的特性。
ts
import { add } from "../index";
// 记录正在执行的effect
let activeEffect: any = null;
// 包装依赖信息
class ReactiveEffect {
private _fn: any;
constructor(fn: any) {
this._fn = fn;
}
run() {
activeEffect = this;
this._fn();
activeEffect = null;
}
}
// 存储依赖信息 target key fn1, fn2
const targetMap = new Map();
export function track(target, key) {
let depsMap = targetMap.get(target);
// 根据 target 取出 target对象的依赖
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 在根据 key 取出 key 的依赖
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 把依赖添加到 dep 的 set 中
dep.add(activeEffect);
}
// 找出依赖信息依次执行
export function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
for (const effect of dep) {
effect.run();
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
// 调用effect 传递的这个函数
_effect.run();
}
好啦,我们完美的通过了两个测试用例
03、实现effect返回runner
单侧
ts
it("effect has return runner function", () => {
let foo = 10;
const runner = effect(() => {
foo++;
return "foo";
});
// 第一次执行
expect(foo).toBe(11);
// 验证 runner
const r = runner();
expect(foo).toBe(12);
expect(r).toBe("foo");
});
假设现在我们有以下代码,
js
import { reactive, effect } from "../../dist/guide-mini-vue.esm.js";
let obj1 = reactive({ a: 100, b: 200 });
let obj2 = reactive({ c: 1 });
// a 属性在 四个 effect 中用了
// c 属性 在一个 effect 中使用了
a 属性有在四个 effect 中使用了,所以有四个
effect(() => {
// 访问属性的时候,触发各自的依赖收集,但是共同拥有一个 ReactiveEffect
// 所以 track 函数就会走两次 所以 deps 里面就有两个 set
console.log(obj1.a);
console.log(obj2.c);
});
effect(() => {
console.log(obj1.a);
});
set 关系
deps 关系
同理我们可以推测,a
属性依赖 Set
的第一个 ReactiveEffect
和 c
属性 依赖 set
的第一个 ReactiveEffect
是同一个对象
这里再来画一张图来整理一下targetMap
与 ReactiveEffect
之间的关系。
所以到这里是不是更加清楚 ReactEffect
中 deps
的作用了?
收集当前副作用函数所有的依赖集合 。一个属性的依赖是一个 Set
,所有的依赖集合就是[Set1, set2]
04、实现effect的scheduler
单侧
ts
it("scheduler", () => {
// 功能的描述
// 1. 通过 effect 的第二个对象参数,传入一个 scheduler 函数
// 2. effect 第一次执行的时候 还是会执行 fn 函数
// 3. 当响应式对象 set 的时候不会 执行 fn 而是执行 scheduler 函数
// 4. 在执行 scheduler 函数的时候,我们记录一下 runner , 并调用 runner 函数,那么 fn 函数会再次执行的。
let dummy: any;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{
scheduler,
}
);
// 第一次执行 fn 的时候 scheduler 不会执行
expect(scheduler).not.toHaveBeenCalled();
// 第一次调用的时候 fn 会执行
expect(dummy).toBe(1);
// 当调用 set 的时候, 触发的是 scheduler 函数的执行
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
expect(dummy).toBe(1);
// manually run
run();
expect(dummy).toBe(2);
// 如何实现呢?
// 首先给 effect 添加第二个参数
// 其次 当响应式数据 set 的时候,检测如果有scheduler 则执行 scheduler 函数, 不再触发更新
});
05、实现effect的stop
先写两个单侧
ts
it("stop", () => {
let dummy: any;
const obj = reactive({ foo: 1 });
const runner = effect(() => {
dummy = obj.foo;
});
obj.foo = 2;
expect(dummy).toBe(2);
stop(runner);
obj.foo = 3;
expect(dummy).toBe(2);
});
// 调用stop 之后的回调函数
it("onStop", () => {
const obj = reactive({ foo: 1 });
const onStop = jest.fn();
let dummy: any;
const runner = effect(
() => {
dummy = obj.foo;
},
{
onStop,
}
);
stop(runner);
// 判断 onStop 是否被调用?
expect(onStop).toBeCalledTimes(1);
});
shared/index.ts
ts
export const extend = Object.assign;
记录要点:
activeEffect
只在执行effect
函数的时候才会有,假如只是属性的访问触发的get
,则是没有activeEffect
的。- 让
ReaciveEffect
对象也记住依赖函数 (dep) - 调用
stop
的事情清空里面的dep依赖就好了。
06、实现readonly功能
ts
it("readonly", () => {
let original = { foo: 1 };
console.warn = jest.fn();
const obj = readonly(original);
expect(obj).not.toBe(original);
expect(obj.foo).toBe(1);
// set
obj.foo = 2;
expect(console.warn).toBeCalled();
});
记录要点:
- 是只读的就不需要
set
触发依赖,那么同样也不需要进行set
里面的track
依赖收集。
reactive.ts
ts
export function readonly(raw: any) {
return new Proxy(raw, readonlyHandler);
}
baseHandlers.ts
ts
import { track, trigger } from "./effect";
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
//先读,再依赖收集
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter(isReadonly = false) {
return function set(target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver);
// 先设置,再触发更新
if (!isReadonly) {
trigger(target, key);
}
return result;
};
}
const get = createGetter();
const set = createSetter();
const readonlyGetter = createGetter(true);
export const reactiveHandler = {
get,
set,
};
export const readonlyHandler = {
get: readonlyGetter,
set(target, key, value, receiver) {
console.warn(`不能修改 ${String(key)},因为他是readonly的`);
return true;
},
};
07、实现isReactive和isReadonly
记录要点:
- 在调用
isReactive
函数的时候,随意访问一下,被代理对象上面的一个属性,都会触发get
方法,我们利用这一点,在函数内部访问不同的key
在get
里面做判断,并根据baseHandle
函数的参数isReadonly
进行区分,是否是readonly
的。
ts
it("test isReactive", () => {
const original = { foo: 1 };
const obj = reactive(original);
expect(original).not.toBe(obj);
expect(obj.foo).toBe(1);
expect(isReactive(original)).toBe(false);
expect(isReactive(obj)).toBe(true);
});
it("test isReadonly", () => {
const original = { foo: 1 };
const obj = readonly(original);
expect(original).not.toBe(obj);
expect(obj.foo).toBe(1);
expect(isReadonly(obj)).toBe(true);
expect(isReadonly(original)).toBe(false);
});
reactive.ts
ts
export function isReactive(value: any) {
// 两个 !! 是将 假值转化为false
return !!value[ReactiveFlag.IS_REACTIVE_FLAG];
}
export function isReadonly(value: any) {
return !!value[ReactiveFlag.IS_READONLY_FLAG];
}
basehandle.ts
ts
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
if (key === ReactiveFlag.IS_REACTIVE_FLAG) {
return !isReadonly;
} else if (key === ReactiveFlag.IS_READONLY_FLAG) {
return isReadonly;
}
//先读,再依赖收集
if (!isReadonly) {
track(target, key);
}
return res;
};
}
08、优化stop功能
记录要点:
1.添加一个变量shouldTrack
用来控制是否需要收集依赖?因为如果执行了obj.foo++
,则不仅会触发set
也会触发get
操作。shouldTrack
默认是关闭的(false)
在执行effect
函数之前开启,在依赖函数执行完成之后重置为false
。
- 并且在收集依赖的函数(
track
)中进行判断,如果shouldTrack = false
,则直接return
掉。
ts
it("enhanced stop", () => {
let dummy: any;
const obj = reactive({ foo: 1 });
const runner = effect(() => {
dummy = obj.foo;
});
stop(runner); // 这里我们明明
obj.foo++;
expect(dummy).toBe(1);
});
effect.ts
ts
run() {
// 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
// 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
// 这里就需要控制了
// 执行 fn 但是不收集依赖
if (!this.active) {
return this._fn();
}
// 可以进行收集依赖了
shouldTrack = true;
// 记录全局正在执行的依赖
activeEffect = this;
let r = this._fn();
//重置
shouldTrack = false;
// activeEffect = null; //???????
return r;
}
09、实现 reactive 和 readonly 嵌套对象转换功能
记录要点:如果通过Reflect.get()得到的是一个对象,则需要递归代理结果,并返回。
ts
it("deep reactive", () => {
const original = {
foo: {
bar: 1,
},
array: [{ bar: 2 }],
};
const obj = reactive(original);
// 检测里面的 对象是不是一个 reactive对象
expect(isReactive(obj.foo)).toBe(true);
expect(isReactive(obj.array)).toBe(true);
expect(isReactive(obj.array[0])).toBe(true);
});
ts
it("deep readonly", () => {
const original = {
foo: {
bar: 1,
},
array: [
{
var: 10,
},
],
};
let obj = readonly(original);
expect(isReadonly(obj.foo)).toBe(true);
expect(isReadonly(obj.array)).toBe(true);
expect(isReadonly(obj.array[0])).toBe(true);
});
baseHandler.ts
ts
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
......
//如果得到的是对象,那么还需要递归
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
10、实现 shallowReadonly 功能
记录要点:
- 被他代理的对象是浅层的,并且不能被
set
,所以我们要创建一个shallowReadonlyGet
ts
it("shallowReadonly", () => {
const original = {
n: {
foo: 1,
},
};
const obj = shallowReadonly(original);
expect(isReadonly(obj)).toBe(true);
expect(isReadonly(obj.n)).toBe(false);
});
实现
ts
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
.....
// 如果是shallow 的则直接 return
if (shallow) {
return res;
}
......
};
}
11、实现 isProxy
要点总结:
- 只需要判断传进来的对象是否符合
isReactive
或者isReadonly
即可
ts
it("isProxy", () => {
const original = {
foo: 1,
};
const obj = reactive(original);
const readonlyObj = readonly({ value: 100 });
expect(isProxy(obj)).toBe(true);
expect(isProxy(readonlyObj)).toBe(true);
});
实现
ts
export function isProxy(raw: any) {
return isReactive(raw) || isReadonly(raw);
}
12、实现ref
注意要点:
- 在
ref
里面必须有一个key
对应一个deps
。 - 当
ref
接收的是一个对象时候,它内部会调用reactive
进一步处理。 ref
的参数一般都是一个单值,单值的话,我们就没办法使用Proxy
来进行代理了,所以作者就使用了一个类,在内部实现get value
和set value
,这也就是为什么单值需要.value
的原因了。
js
describe("ref", () => {
it("should create a ref", () => {
const foo = ref(1);
expect(foo.value).toBe(1);
});
it("ref can effect", () => {
const foo = ref(1);
let dummy = 0;
effect(() => {
dummy = foo.value;
});
expect(dummy).toBe(1);
foo.value = 2;
expect(dummy).toBe(2);
foo.value = 2;
expect(dummy).toBe(2);
});
it("should support properties reactive", () => {
const foo = ref({
bar: 1,
});
let dummy = 0;
effect(() => {
dummy = foo.value.bar;
});
expect(dummy).toBe(1);
foo.value.bar = 2;
expect(dummy).toBe(2);
foo.value.bar = 2;
expect(dummy).toBe(2);
});
});
实现
js
import { track, trackEffects, triggerEffects } from "./effect";
import { isObject } from "../shared/index";
import { reactive } from "./reactive";
class RefImpl {
private _value: any;
// 依赖函数存放的位置是在 ref 的 deps 属性上
private deps: Set<any> = new Set();
constructor(value) {
// 在初始化 ref 的时候要判断是不是一个object
this._value = isObject(value) ? reactive(value) : value;
}
get value() {
// 收集依赖
trackEffects(this.deps);
return this._value;
}
set value(newValue) {
this._value = isObject(newValue) ? reactive(newValue) : newValue;
// 触发依赖
triggerEffects(this.deps);
}
}
export function ref(value: any) {
return new RefImpl(value);
}
13、实现isRef 和 unRef
要点记录:
- 给
ref
类上面加个表示就可以区分实例对象是不是ref
类型的
这两个功能很简单
js
it("isRef", () => {
const foo = ref(1);
expect(isRef(2)).toBe(false);
expect(isRef(foo)).toBe(true);
});
it("unRef", () => {
const foo = ref(1);
expect(unRef(foo)).toBe(1);
expect(unRef(1)).toBe(1);
});
实现:
js
export function isRef(ref) {
return !!ref.__v_isRef;
}
export function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
14、实现proxyRefs 功能
要点记录
- 如果原来的值是一个
ref
那么重新赋值的时候,就要改原来值的.value
js
it("proxyRefs", () => {
const user = {
age: ref(10),
name: "张三",
};
const proxyUser = proxyRefs(user);
expect(user.age.value).toBe(10);
// 如果是 ref 则会自动的返回 ref 的 value
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("张三");
// 设置值,也分两种情况
// 设置的值不是 ref
proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
// 设置的是 ref
proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});
实现
js
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unRef(Reflect.get(target, key));
},
set(target, key, value) {
// 如果原来的值是一个ref 那么重新赋值的时候,就要改原来值的 .value
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
},
});
}
15、实现computed功能
特性:
- 调用
ComputedRefImpl
, 同时将getter
函数传递给ReactiveEffect(getter)
, 对其进行依赖收集。 - computed 属性是有缓存的, 只有在访问
.value
属性的时候,才会调用effect.run()
- 因为缓存功能的存在,所以在内部需要收集
getter
函数的依赖信息,当发现有依赖信息变化之后调用schduler
函数,重新将dirty
变量重置为true
- 当用户调用
.value = xxx
的时候就会再次触发scheduler
函数,重新设置dirty = true
的值。 - 下一次再调用
.value
的时候,检测dirty
变量发现是"脏的",则就再需要重新调用getter
函数,获取最新的值。从而达到既可以缓存,又可以在依赖的值发生变化的时候,下一次取的时候拿到最新的值。
js
describe("computed", () => {
it("happy path", () => {
const user = reactive({
age: 1,
});
const age = computed(() => {
return user.age;
});
expect(age.value).toBe(1);
});
it("should computed lazily", () => {
// 在没访问.value 之前, getter函数是不会被调用的
const user = reactive({
age: 1,
});
const getter = jest.fn(() => {
return user.age;
});
const value = computed(getter);
// lazy 延迟执行
expect(getter).not.toHaveBeenCalled();
// 访问.value属性,触发 getter函数执行
expect(value.value).toBe(1);
expect(getter).toBeCalledTimes(1);
// 重新赋值, getter 函数还是值调用一次
user.age = 2;
expect(getter).toBeCalledTimes(1);
// 访问 .value 属性
expect(value.value).toBe(2);
expect(getter).toBeCalledTimes(2);
// 测试缓存 效果
value.value;
expect(getter).toBeCalledTimes(2);
});
});
代码实现
ts
import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
private _getter: any;
private _dirty: any = true; // 默认值是 true 表示不脏的
private _value: any;
private _effect: ReactiveEffect;
constructor(getter) {
this._getter = getter;
// 第一次不会执行 scheduler 函数 ,当 响应式数据被 set 的时候, 不会触发 effect 函数, 而是执行 scheduler 函数
this._effect = new ReactiveEffect(getter, () => {
// set 的时候把 标记脏不脏的放开 ,
if (!this._dirty) {
this._dirty = true;
}
});
}
get value() {
// 用一个变量来标记是否 读取过 computed 的值
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}