实现mini-vue3
初始化项目yarn init -yvue3源码采用的是monorerpo的管理方式我们这就简单点的方式创建包集成typescript注意如果没有安装typescript需要先安装typescriptnpx tsc --init集成jestyarn add jest types/jest --dev注意安装之后还是不能识别spec.ts文件需要在ts.config文件中配置配置测试脚本命令配置ts.config解决jest不兼容esmodule的问题配置一下babelhttps://jestjs.io/docs/getting-started安装插件jest实现收集依赖先创建effect.spec.jsdescribe(effect,(){it(enter,(){constafreactive({age:1})/** * 所谓的收集依赖就是将effect下面的回调函数fn get的时候先放在一个容器中 * set的时候在执行所有被收集起来的fn */letnewAgeeffect((){newAgeaf.age1})expect(newAge).toBe(2)af.ageexpect(newAge).toBe(3)})})配置ts.config使其兼容es6reactive.spec.tsimport{reactive}from../reactivedescribe(reactive,(){it(enter,(){constobj{a:1}constproxyObjreactive(obj)expect(proxyObj).not.toBe(obj)expect(proxyObj.a).toBe(1)})})下面代码完成上面测试reactive.tsexportfunctionreactive(raw){returnnewProxy(raw,{get(target,key){constresReflect.get(target,key)// 依赖收集returnres},set(target,key,value){constresReflect.set(target,key,value)returnres}})}effect.tsletactiveEffectclassReactiveEffect{fn:anyconstructor(fn){this.fnfn}run(){activeEffectthisthis.fn()}}/** * 依赖收集 收集的就是ReactiveEffect的实例 * */exportfunctioneffect(fn){// fnconst_effectnewReactiveEffect(fn)_effect.run()}// target-key-dep//收集依赖lettargetMapnewMap()exportfunctiontrack(target,key){letdepsMaptargetMap.get(target)if(!depsMap){depsMapnewMap()targetMap.set(target,depsMap)}letdepdepsMap.get(key)if(!dep){depnewSet()depsMap.set(key,dep)}dep.add(activeEffect)}//触发set 更新exportfunctiontrigger(target,key){letdepsMaptargetMap.get(target)letdepdepsMap.get(key)for(leteffectofdep){effect.run()}}reactive.ts 添加量函数完成和effect相关的功能runner一句话概括这个功能effect函数会有一个返回一个可执行函数并且这个函数返回值是fn return出来的值测试用例it(runner,(){leta1construnnereffect((){areturna})expect(a).toBe(2)constrrunner()expect(a).toBe(3)expect(r).toBe(a)})在effect 函数中return在run方法中returnschedulerscheduler 就是 effect能传入的第二个参数而且这个参数应该是个函数 一开始不会被调用等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了测试用例it(scheduler,(){letdummyletrun:anyconstschedulerjest.fn((){runrunner})constobjreactive({foo:1})construnnereffect((){dummyobj.foo},{scheduler})// scheduler 就是 effect能传入的第二个参数而且这个参数应该是个函数 一开始不会被调用expect(scheduler).not.toHaveBeenCalled()expect(dummy).toBe(1)// 等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了obj.fooexpect(scheduler).toHaveBeenCalledTimes(1)expect(dummy).toBe(1)// 调用runnerrun()expect(dummy).toBe(2)})stop调用stop方法的runner函数 会有一次让响应式更新失效的情况it(stop,(){letdummy;constobjreactive({props:1})construnnereffect((){dummyobj.prop})obj.prop2expect(dummy).toBe(2)stop(runner)obj.prop3//可以看到没有立即更新为3 因为上面的runner调用了stop方法expect(dummy).toBe(2)runner()expect(dummy).toBe(3)})思路让当前key 对应的dep 里面的依赖 被删除第三集:优化下stop完成readonly相关功能优化stop功能obj.prop 测试在以前的stop测试不会通过原因就是obj.prop 的等价操作就是 obj.prop obj.prop 1。 其中在获取obj.prop的时候会再一次触发get收集依赖 所以上面的stop删除就相当于白删除了配置launch.json{ version: 0.2.0, configurations: [ { // 调试名称 name: Jest, type: node, // 启动类型 分为launch(启动) 和 attach(附加)两种 request: launch, // 设置运行时可执行文件路径默认是node可以是其他的执行程序如npm、yarn runtimeExecutable: yarn, // 传递给程序的参数 args: [ jest, ], // 指定程序启动调试的目录 cwd: ${workspaceRoot}, sourceMaps: false, // 如果设置为std则进程stdout / stderr的输出将显示在调试控制台中而不是侦听调试端口上的输出 outputCapture: std, }, ] }解决readonly创建文件readonly.spec.tsimport{readonly}from../reactivedescribe(,(){it(happy path,(){/** * 只读属性 */constoriginal{foo:1,bar:{a:2}}constwrappedreadonly(original)expect(wrapped).not.toBe(original)expect(wrapped.foo).toBe(1)})//当改变属性 触发set时发出警告it(warn,(){console.warnjest.fn();constuserreadonly({age:10})user.age11expect(console.warn).toBeCalled()})})创建一个baseHandler.ts文件 对reactive.ts里面的代码进行重构import{track,trigger}from./effect/** * 为节约内存所以全局环境下存储高阶函数的返回值 */constgetcreateGetter()constsetcreateSetter()constreadonlyGetcreateGetter(true)functioncreateGetter(isReadonlyfalse){returnfunctionget(target,key){constresReflect.get(target,key)// 依赖收集if(!isReadonly){track(target,key)}returnres}}functioncreateSetter(){returnfunctionset(target,key,value){constresReflect.set(target,key,value)trigger(target,key)returnres}}exportconstmutableHandlers{get:get,set:set}exportconstreadonlyHandlers{get:readonlyGet,set(target,key,value){console.warn(只读);returntrue}}reactive.tsimport{track,trigger}from./effectimport{mutableHandlers,readonlyHandlers}from./baseHandlersexportfunctionreactive(raw){returncreateActiveObject(raw,mutableHandlers)}exportfunctionreadonly(raw){returncreateActiveObject(raw,readonlyHandlers)}functioncreateActiveObject(raw,baseHandlers){returnnewProxy(raw,baseHandlers)}实现isReactive和isReadonly测试用例解决深层reactiveit(recursion reactives,(){constoriginal{nested:{foo:1},array:[{bar:2}]}constobservedreactive(original)expect(isReactive(observed.nested)).toBe(true)expect(isReactive(observed.array)).toBe(true)expect(isReactive(observed.array[0])).toBe(true)})shallowReadonlyshallowReadonly.spec.tsimport{isReadonly,shallowReadonly}from../reactive;describe(shallowReadonly,(){it(no recursion,(){// reactive值作用于对象的表层constpropsshallowReadonly({n:{foo:1}})expect(isReadonly(props)).toBe(true)expect(isReadonly(props.n)).toBe(false)})it(warn,(){console.warnjest.fn();constusershallowReadonly({age:10})user.age11expect(console.warn).toBeCalled()})})reactive.tsbaseHandlers.tsisProxyreactive.tsrefimport{effect}from../effectimport{ref}from../refdescribe(ref,(){it(ref init,(){constaref(1)expect(a.value).toBe(1)})it(be reactive,(){constaref(1)letdummy;letcalls0effect((){callsdummya.value})expect(calls).toBe(1)expect(dummy).toBe(1)a.value2expect(calls).toBe(2)expect(dummy).toBe(2)// same value should not triggera.value2expect(calls).toBe(2)expect(dummy).toBe(2)})it(should be recursion,(){constaref({count:1,})letdummyeffect((){dummya.value.count})expect(dummy).toBe(1)a.value.count2expect(dummy).toBe(2)})})ref.tsimport { isObjet } from ../utils import { isTracking, trackEffects, triggerEffects } from ./effect import { reactive } from ./reactive class RefImpl { private _value: any // ref和reactive的区别就是ref只有一个value所以dep只需单独一个不需要reactive的复杂对应关系 public dep private _rawValue: any constructor(value) { // _rawValue的声明 是因为害怕this._value进行reactive后变成proxy // 从而不方便下面set的比较 this._rawValue value this._value isObjet(value) ? reactive(value) : value this.dep new Set() } get value() { if (isTracking()) { trackEffects(this.dep) } return this._value } set value(newValue) { if (!Object.is(newValue, this._rawValue)) { this._rawValue newValue // 注意是先修改value的值然后进行trigger this._value isObjet(newValue) ? reactive(newValue) : newValue triggerEffects(this.dep) } } } export function ref(value) { return new RefImpl(value) }