实现mini-vue3
Posted _阿锋丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现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", () =>
const af = reactive(
age: 1
)
/**
* 所谓的收集依赖就是将effect下面的回调函数fn get的时候先放在一个容器中
* set的时候在执行所有被收集起来的fn
*/
let newAge
effect(() =>
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", () =>
const obj = a: 1
const proxyObj = reactive(obj)
expect(proxyObj).not.toBe(obj)
expect(proxyObj.a).toBe(1)
)
)
下面代码完成上面测试
reactive.ts
export function reactive(raw)
return new Proxy(raw,
get(target, key)
const res = Reflect.get(target, key)
// 依赖收集
return res
,
set(target, key, value)
const res = Reflect.set(target, key, value)
return res
)
effect.ts
let activeEffect
class ReactiveEffect
fn: any
constructor(fn)
this.fn = fn
run()
activeEffect = this
this.fn()
/**
* 依赖收集 收集的就是ReactiveEffect的实例
*
*/
export function effect(fn)
// fn
const _effect = new ReactiveEffect(fn)
_effect.run()
// target->key->dep
//收集依赖
let targetMap = new Map()
export function track(target, key)
let depsMap = targetMap.get(target)
if (!depsMap)
depsMap = new Map()
targetMap.set(target, depsMap)
let dep = depsMap.get(key)
if (!dep)
dep = new Set()
depsMap.set(key, dep)
dep.add(activeEffect)
//触发set 更新
export function trigger(target, key)
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (let effect of dep)
effect.run()
reactive.ts 添加量函数
完成和effect相关的功能
runner
一句话概括这个功能:effect函数会有一个返回一个可执行函数,并且这个函数返回值是fn return出来的值
测试用例
it("runner", () =>
let a = 1
const runner = effect(() =>
a++
return 'a'
)
expect(a).toBe(2)
const r = runner()
expect(a).toBe(3)
expect(r).toBe('a')
)
在effect 函数中return
在run方法中return
scheduler
scheduler 就是 effect能传入的第二个参数,而且这个参数应该是个函数 一开始不会被调用
等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了
测试用例
it("scheduler", () =>
let dummy
let run: any
const scheduler = jest.fn(() =>
run = runner
)
const obj = reactive( foo: 1 )
const runner = 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)
// 调用runner
run()
expect(dummy).toBe(2)
)
stop
调用stop方法的runner函数 会有一次让响应式更新失效的情况
it("stop",()=>
let dummy;
const obj = reactive(props:1)
const runner = effect(()=>
dummy = obj.prop
)
obj.prop = 2
expect(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", () =>
/**
* 只读属性
*/
const original = foo: 1, bar: a: 2
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
)
//当改变属性 触发set时发出警告
it('warn', () =>
console.warn = jest.fn();
const user = readonly(
age: 10
)
user.age = 11
expect(console.warn).toBeCalled()
)
)
创建一个baseHandler.ts文件 ,对reactive.ts里面的代码进行重构,
import track, trigger from './effect'
/**
* 为节约内存,所以全局环境下存储高阶函数的返回值
*/
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
function createGetter(isReadonly = false)
return function get(target, key)
const res = Reflect.get(target, key)
// 依赖收集
if (!isReadonly)
track(target, key)
return res
function createSetter()
return function set(target, key, value)
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
export const mutableHandlers =
get: get,
set: set
export const readonlyHandlers =
get: readonlyGet,
set(target, key, value)
console.warn('只读');
return true
reactive.ts
import track, trigger from './effect'
import mutableHandlers, readonlyHandlers from './baseHandlers'
export function reactive(raw)
return createActiveObject(raw, mutableHandlers)
export function readonly(raw)
return createActiveObject(raw, readonlyHandlers)
function createActiveObject(raw, baseHandlers)
return new Proxy(raw, baseHandlers)
实现isReactive和isReadonly
测试用例
解决
深层reactive
it("recursion reactives", () =>
const original =
nested:
foo: 1
,
array: [ bar: 2 ]
const observed = 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值作用于对象的表层
const props = shallowReadonly( n: foo: 1 )
expect(isReadonly(props)).toBe(true)
expect(isReadonly(props.n)).toBe(false)
)
it('warn', () =>
console.warn = jest.fn();
const user = shallowReadonly(
age: 10
)
user.age = 11
expect(console.warn).toBeCalled()
)
)
reactive.ts
baseHandlers.ts
isProxy
reactive.ts
ref
import effect from '../effect'
import ref from '../ref'
describe("ref", () =>
it("ref init", () =>
const a = ref(1)
expect(a.value).toBe(1)
)
it("be reactive", () =>
const a = ref(1)
let dummy;
let calls = 0
effect(() =>
calls++
dummy = a.value
)
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
)
it("should be recursion", () =>
const a = ref(
count: 1,
)
let dummy
effect(() =>
dummy = a.value.count
)
expect(dummy).toBe(1)
a.value.count = 2
expect(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)
以上是关于实现mini-vue3的主要内容,如果未能解决你的问题,请参考以下文章
青训营Pro 前端框架设计理念 - Vue3动机 - 手写实现mini-vue
青训营Pro 前端框架设计理念 - Vue3动机 - 手写实现mini-vue