实现mini-vue3

实现mini-vue3

初始化项目

yarn init -y

vue3源码采用的是monorerpo的管理方式,我们这就简单点的方式

创建包

集成typescript

注意如果没有安装typescript需要先安装typescript

npx tsc --init

集成jest

yarn add jest @types/jest --dev

注意安装之后还是不能识别spec.ts文件

需要在ts.config文件中配置

配置测试脚本命令

配置ts.config

解决jest不兼容esmodule的问题

配置一下babel

https://jestjs.io/docs/getting-started

安装插件jest

实现收集依赖

先创建effect.spec.js

describe('effect',()=>{it("enter",()=>{constaf=reactive({age:1})/** * 所谓的收集依赖就是将effect下面的回调函数fn get的时候先放在一个容器中 * set的时候在执行所有被收集起来的fn */letnewAgeeffect(()=>{newAge=af.age+1})expect(newAge).toBe(2)af.age++expect(newAge).toBe(3)})})

配置ts.config使其兼容es6

reactive.spec.ts

import{reactive}from'../reactive'describe("reactive",()=>{it("enter",()=>{constobj={a:1}constproxyObj=reactive(obj)expect(proxyObj).not.toBe(obj)expect(proxyObj.a).toBe(1)})})

下面代码完成上面测试

reactive.ts

exportfunctionreactive(raw){returnnewProxy(raw,{get(target,key){constres=Reflect.get(target,key)// 依赖收集returnres},set(target,key,value){constres=Reflect.set(target,key,value)returnres}})}

effect.ts

letactiveEffectclassReactiveEffect{fn:anyconstructor(fn){this.fn=fn}run(){activeEffect=thisthis.fn()}}/** * 依赖收集 收集的就是ReactiveEffect的实例 * */exportfunctioneffect(fn){// fnconst_effect=newReactiveEffect(fn)_effect.run()}// target->key->dep//收集依赖lettargetMap=newMap()exportfunctiontrack(target,key){letdepsMap=targetMap.get(target)if(!depsMap){depsMap=newMap()targetMap.set(target,depsMap)}letdep=depsMap.get(key)if(!dep){dep=newSet()depsMap.set(key,dep)}dep.add(activeEffect)}//触发set 更新exportfunctiontrigger(target,key){letdepsMap=targetMap.get(target)letdep=depsMap.get(key)for(leteffectofdep){effect.run()}}

reactive.ts 添加量函数

完成和effect相关的功能

runner

一句话概括这个功能:effect函数会有一个返回一个可执行函数,并且这个函数返回值是fn return出来的值

测试用例

it("runner",()=>{leta=1construnner=effect(()=>{a++return'a'})expect(a).toBe(2)constr=runner()expect(a).toBe(3)expect(r).toBe('a')})

在effect 函数中return

在run方法中return

scheduler

scheduler 就是 effect能传入的第二个参数,而且这个参数应该是个函数 一开始不会被调用

等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了

测试用例

it("scheduler",()=>{letdummyletrun:anyconstscheduler=jest.fn(()=>{run=runner})constobj=reactive({foo:1})construnner=effect(()=>{dummy=obj.foo},{scheduler})// scheduler 就是 effect能传入的第二个参数,而且这个参数应该是个函数 一开始不会被调用expect(scheduler).not.toHaveBeenCalled()expect(dummy).toBe(1)// 等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了obj.foo++expect(scheduler).toHaveBeenCalledTimes(1)expect(dummy).toBe(1)// 调用runnerrun()expect(dummy).toBe(2)})

stop

调用stop方法的runner函数 会有一次让响应式更新失效的情况

it("stop",()=>{letdummy;constobj=reactive({props:1})construnner=effect(()=>{dummy=obj.prop})obj.prop=2expect(dummy).toBe(2)stop(runner)obj.prop=3//可以看到没有立即更新为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.ts

import{readonly}from'../reactive'describe("",()=>{it("happy path",()=>{/** * 只读属性 */constoriginal={foo:1,bar:{a:2}}constwrapped=readonly(original)expect(wrapped).not.toBe(original)expect(wrapped.foo).toBe(1)})//当改变属性 触发set时发出警告it('warn',()=>{console.warn=jest.fn();constuser=readonly({age:10})user.age=11expect(console.warn).toBeCalled()})})

创建一个baseHandler.ts文件 ,对reactive.ts里面的代码进行重构,

import{track,trigger}from'./effect'/** * 为节约内存,所以全局环境下存储高阶函数的返回值 */constget=createGetter()constset=createSetter()constreadonlyGet=createGetter(true)functioncreateGetter(isReadonly=false){returnfunctionget(target,key){constres=Reflect.get(target,key)// 依赖收集if(!isReadonly){track(target,key)}returnres}}functioncreateSetter(){returnfunctionset(target,key,value){constres=Reflect.set(target,key,value)trigger(target,key)returnres}}exportconstmutableHandlers={get:get,set:set}exportconstreadonlyHandlers={get:readonlyGet,set(target,key,value){console.warn('只读');returntrue}}

reactive.ts

import{track,trigger}from'./effect'import{mutableHandlers,readonlyHandlers}from'./baseHandlers'exportfunctionreactive(raw){returncreateActiveObject(raw,mutableHandlers)}exportfunctionreadonly(raw){returncreateActiveObject(raw,readonlyHandlers)}functioncreateActiveObject(raw,baseHandlers){returnnewProxy(raw,baseHandlers)}
实现isReactive和isReadonly

测试用例

解决

深层reactive

it("recursion reactives",()=>{constoriginal={nested:{foo:1},array:[{bar:2}]}constobserved=reactive(original)expect(isReactive(observed.nested)).toBe(true)expect(isReactive(observed.array)).toBe(true)expect(isReactive(observed.array[0])).toBe(true)})

shallowReadonly

shallowReadonly.spec.ts

import{isReadonly,shallowReadonly}from'../reactive';describe("shallowReadonly",()=>{it("no recursion",()=>{// reactive值作用于对象的表层constprops=shallowReadonly({n:{foo:1}})expect(isReadonly(props)).toBe(true)expect(isReadonly(props.n)).toBe(false)})it('warn',()=>{console.warn=jest.fn();constuser=shallowReadonly({age:10})user.age=11expect(console.warn).toBeCalled()})})

reactive.ts

baseHandlers.ts

isProxy

reactive.ts

ref

import{effect}from'../effect'import{ref}from'../ref'describe("ref",()=>{it("ref init",()=>{consta=ref(1)expect(a.value).toBe(1)})it("be reactive",()=>{consta=ref(1)letdummy;letcalls=0effect(()=>{calls++dummy=a.value})expect(calls).toBe(1)expect(dummy).toBe(1)a.value=2expect(calls).toBe(2)expect(dummy).toBe(2)// same value should not triggera.value=2expect(calls).toBe(2)expect(dummy).toBe(2)})it("should be recursion",()=>{consta=ref({count:1,})letdummyeffect(()=>{dummy=a.value.count})expect(dummy).toBe(1)a.value.count=2expect(dummy).toBe(2)})})

ref.ts

import { 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) }