对于希望提升自己前端架构能力,写出更优雅代码的前端同学,
vue源码绝对是你成功路上的垫脚石,但由于尤大实现的vue源码库有许多对于一些边缘情况的判断逻辑,
所有这篇文章希望和大家一起从头实现一个简单版的vue源码库 mini-vue(感谢 阿崔cxr 的 mini-vue)
github源码地址 如果觉着还不错欢迎Star~
实现runner功能
effect再接受用户传入的fn之后会将fn返回let runner=effect(fn)
我们称这个返回值为runner
,在执行runner()
的时候会将fn的返回值作为runner的返回值返回fn的返回值=runner()
1、在src>reactive>test>effect.test.ts中编写runner测试用例
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
//...other code
it('runner', () => {
let bar = 1
let runner = effect(() => {
bar++
return 'bar'
})
//验证用户传入的fn执行
expect(bar).toBe(2)
let r = runner()
//验证在执行runner的时候会再次执行用户传入的fn
expect(bar).toBe(3)
//验证runner的返回值等于fn的返回值
expect(r).toBe('bar')
})
});
从测试用例可以看出我们返回的runner
其实就是我们ReactiveEffect
的run
方法
class ReactiveEffect {
//...other code
run() {
activeEffect = this;//用于存储依赖
this._fn();
}
}
2、在src>reactive>effect.ts中将run方法返回
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
let runner = _effect.run.bind(_effect)// bind用于处理run中的this指向
return runner
}
3、在ReactiveEffect的run方法中将fn的返回值返回
class ReactiveEffect {
//...other code
run() {
activeEffect = this;//用于存储依赖
return this._fn(); //将fn的返回值返回
}
}
4、执行一下effect测试用例
yarn test effect.spec.ts
实现scheduler功能
effect允许传入第二个参数scheduler
,当传入scheduler之后,effect初始化
会执行fn,而当fn中的reactive值执行set操作
时会执行我们传入的scheduler
1、在src>reactive>test>effect.test.ts中编写scheduler测试用例
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
//...other code
it('scheduler', () => {
let dummy
let info = reactive({ age: 18 })
//jest 模拟传入的函数
const scheduler = jest.fn(() => {
dummy++
})
//将scheduler作为第二个参数传入
effect(() => {
dummy = info.age
}, { scheduler })
//验证第一次是scheduler不被调用
expect(scheduler).not.toHaveBeenCalled()
//验证调用传入的fn
expect(dummy).toBe(18)
//执行set操作
info.age = 20
//验证执行set操作时会执行传入的scheduler
expect(dummy).toBe(19)
expect(scheduler).toBeCalledTimes(1)
})
});
2、在src>reactive>effect.ts中添加传入参数options将options传入ReactiveEffect
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
let runner = _effect.run.bind(_effect)
return runner
}
class ReactiveEffect {
private _fn: any;
public scheduler: any//添加一个scheduler属性
constructor(fn, scheduler?) {
//将传入的scheduler存储起来
this.scheduler = scheduler
this._fn = fn;
}
run() {
activeEffect = this;//用于存储依赖
return this._fn();
}
}
2、在src>reactive>effect.ts 在trigger是判断effect是否存在scheduler
,如果存在执行scheduler否则执行run
export function trigger(target, key) {
//...other code
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
3、执行一下effect测试用例
yarn test effect.spec.ts
实现stop功能
effect文件会暴露一个stop方法,如果在stop方法中传入runner,则之后reavtive的set操作不会触发fn的执行,只在运行runner的时执行fn
实现思路:
reactive执行set操作时执行fn的实现原理
(1)在reactive的get操作用将effect存储起来
get(target, key) {
const res = Reflect.get(target, key);
track(target, key);//存储effect
return res;
}
function track(target, key) {
//other code
//将activeEffectc存储起来
dep.add(activeEffect);
}
(2)在set时执行所有effect
set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
function trigger(target, key) {
//...other code
//在set中执行effect的run方法
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
根据上述原因可知,要实现stop功能只需要将当前的effect从依赖中删除即可。
1、在src>reactive>test>effect.test.ts中编写stop测试用例
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
//...other code
it('stop', () => {
let dummy
let info = reactive({ age: 18 })
let runner = effect(() => {
dummy = info.age
})
expect(dummy).toBe(18)
//将runner传入stop后,更新reactive不会触发fn
stop(runner)
info.age = 20
expect(dummy).toBe(18)
//再次执行runner的时候会执行fn
runner()
expect(dummy).toBe(20)
})
});
2、在src>reactive>test>effect.ts中将effect挂载到runner上
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
let runner: any = _effect.run.bind(_effect)
//将effect挂载到runner上,以便在stop函数中使用
runner._effect = _effect
return runner
}
export const stop = (runner: any) => {
runner._effect.stop()
}
2、在src>reactive>test>effect.ts中反向存储track中的依赖以便在stop中将依赖删除
class ReactiveEffect {
//other code
//用于反向存储track中的依赖
//由于可能是多个依赖所以用[]在存储
public deps = []
//这样在调用stop时可以将track中存储的对应依赖删除
stop() {
this.deps.forEach((dep: any) => {
dep.delete(this)
})
}
}
export function track(target, key) {
// other code
//如果当前没有触发的依赖则直接返回否则将activeEffect与dep进行存储
if (!activeEffect) return
dep.add(activeEffect);
activeEffect.deps.push(dep)
}
3、执行一下effect测试用例
yarn test effect.spec.ts
4、优化代码 对于这个stop中的代码我们可以重构一下,让他更具有语义化
class ReactiveEffect {
//...other code
stop() {
this.deps.forEach((dep: any) => {
dep.delete(this)
})
}
}
class ReactiveEffect {
//...other code
stop() {
cleanUpEffect(this);
}
}
function cleanUpEffect(effect: any) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
}
由于第一次调用stop会将track中的依赖删除,但之后的get操作会将依赖再次添加上去,为了防止这种清空我们可以添加一个active来做判断
class ReactiveEffect {
private active = true //为防止cleanUpEffect重复调用
stop() {
if (this.active) {
cleanUpEffect(this);
}
this.active = false
}
}
5、再次执行测试
yarn test effect.spec.ts
实现onStop功能
effect还可以接收一个onStop函数,当用户调用stop功能之后会回到这个传入的onStop函数
1、在src>reactive>test>effect.test.ts中编写onStop测试用例
it('onStop', () => {
let dummy
let info = reactive({ age: 18 })
//创建一个onStop函数
const onStop = jest.fn()
let runner = effect(() => {
dummy = info.age
// 将onStop作为第二个参数传入
}, { onStop })
expect(dummy).toBe(18)
//执行stop之后调用onStop
stop(runner)
expect(onStop).toHaveBeenCalledTimes(1)
})
2、在src>reactive>test>effect.ts中将传入的参数赋值给effect
export function effect(fn, options: any = {}) {
//...other code
Object.assign(_effect, options)
}
2、在src>reactive>test>effect.ts中的RactiveEffect添加这个onStop的处理逻辑
class ReactiveEffect {
//...other code
private onStop?: () => void
stop() {
if (this.active) {
cleanUpEffect(this);
//判断是否存在onStop,有的话在执行cleanUpEffect之后执行
if (this.onStop) {
this.onStop()
}
}
this.active = false
}
}
3、执行测试
yarn test effect.spec.ts
4、优化代码 对于第二步的处理我们可以让代码更加语义化 在src下创建一个shard文件夹和index文件 重写我们的Object.assign
export function effect(fn, options: any = {}) {
//...other code
//Object.assign(_effect, options)
extend(_effect, options)
}
4、再次执行测试
yarn test effect.spec.ts