mini-vue
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbHL7vMa-1689576017084)(https://hexo-img.obs.cn-east-3.myhuaweicloud.com/llf/%E6%9C%AA%E5%91%BD%E5%90%8D%E7%BB%98%E5%9B%BE.drawio.png)]
TDD开发流程
TDD(Test-Drive Development)
TDD的开发流程通常包括以下几个步骤:
- 编写测试用例:根据需求编写测试用例,测试用例应该覆盖尽可能多的场景,包括正常情况和异常情况。
- 运行测试用例:运行测试用例,所有测试用例都应该失败,因为此时还没有编写相关的代码。
- 编写代码:编写代码来满足测试用例的要求,代码应该尽可能简单、清晰、可读、可维护,同时也要考虑性能和安全等方面。
- 运行测试用例:运行测试用例,所有测试用例都应该通过,否则需要继续修改代码。
- 重构代码:对代码进行重构,使其更加简洁、优雅、可读、可维护,同时保证测试用例依然能够通过。
- 重复以上步骤:重复以上步骤,直到所有需求都被满足,测试用例覆盖的场景足够多,代码质量达到了预期的要求。
目录
- 1-16:reactivity
- 17-?:runtime-core
1.Jest单元测试环境配置
配置jest
添加好开发环境下的tsc支持
yarn add typescript --dev
npx tsc --init
添加测试依赖
yarn add jest --dev
yarn add @type/jest --dev
测试
it('init', () => {
expect(true).toBe(true);
})
在package.json中添加命令"test":"jest"
yarn test
配置jest的ts支持和babel支持
https://www.jestjs.cn/docs/getting-started
2. effect的实现
什么是effect?
在Vue3中,
effect
是一个全新的响应式API,用于追踪响应式数据的变化并触发副作用。
effect
函数接受一个函数作为参数,并返回一个响应式的函数。在
effect
函数内部可以访问和操作响应式数据,在响应式数据发生变化时,
effect
会自动重新运行,以触发相应的副作用。
effect
函数的基本用法如下:
import { effect } from 'vue';
const count = ref(0);
const stop = effect(() => {
console.log(count.value);
});
count.value = 1; // 输出:1
count.value = 2; // 输出:2
stop(); // 停止effect函数的运行
在上面的例子中,我们使用
effect
函数创建了一个响应式函数,
当
count
的值发生变化时,
effect
函数会自动重新运行,并输出最新的
count
值
。我们还可以通过调用
stop
函数来停止
effect
函数的运行。
effect
函数还支持配置选项,例如
lazy
、
scheduler
、
onTrack
和
onTrigger
等,可以用于优化性能或实现高级的响应式功能。例如,
lazy
选项可以延迟
effect
函数的运行直到它被访问,
scheduler
选项可以用于控制
effect
函数的运行时机,
onTrack
和
onTrigger
选项可以用于在
effect
函数运行前和运行后触发钩子函数。
目标测试
import { reactive } from "../reactive";
import { effect } from "../effect";
describe('effect', () => {
it('happy path', () => {
const user = reactive({
age: 10
})
let nextAge;
effect(() => {
nextAge = user.age + 1;
})
expect(nextAge).toBe(11);
//update
user.age++;
expect(nextAge).toBe(12);
})
})
需要reactive和effect的实现才能通过这个单元测试,因此需要先实现reactive
代码实现
effect.ts
class ReactiveEffect {
private _fn: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
activeEffect = this;
this._fn();
}
}
//track方法用于依赖收集
const targetMap = new Map()
export function track(target: any, key: any) {
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
// 将fn存入,因为使用的是set,在多次进行get时,fn不会重复
dep.add(activeEffect)
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
effect.run();
}
}
// 用于记录当前run的effect,以便track进行获取
let activeEffect: any;
// 接收到这个fn需要进行封装,不能丢掉
export function effect(fn: Function) {
const _effect = new ReactiveEffect(fn)
_effect.run();
}
将effect相关的track和trigger放在reactive的get和set方法中,即使有些reactive对象不使用effect,也会不断地调用track和trigger方法,这样的调用是浪费资源的,可能可以改进。
3.reactive的实现
什么是reactive?
在Vue3中,
reactive
是一个全新的响应式API,用于将一个普通的JavaScript对象转换为
响应式的对象
。
reactive
函数接受一个普通的JavaScript对象作为参数,并返回一个响应式的
Proxy对象
。在响应式对象的属性被读取或修改时,Vue会追踪这些属性的依赖关系,并在发生变化时自动触发更新。
reactive
函数的基本用法如下:
import { reactive } from 'vue';
const state = reactive({
count: 0
});
console.log(state.count); // 输出:0
state.count++;
console.log(state.count); // 输出:1
在上面的例子中,我们使用
reactive
函数创建了一个响应式对象
state
,该对象包含一个属性
count
,初始值为0。我们可以像使用普通对象一样访问和修改
state
对象的属性,但是Vue会追踪这些属性的依赖关系,并在属性发生变化时自动触发更新。
reactive
函数还支持嵌套对象和数组,可以创建复杂的响应式数据结构。
const state = reactive({
todos: [
{ id: 1, text: 'Learn Vue', done: true },
{ id: 2, text: 'Build an app', done: false }
],
filter: 'all'
});
console.log(state.todos[0].text); // 输出:Learn Vue
state.todos.push({ id: 3, text: 'Test app', done: false });
console.log(state.todos.length); // 输出:3
目标测试
import { reactive } from "../reactive";
describe('reactive', () => {
it('happy path', () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
})
})
代码实现
reactive.ts
import { track, trigger } from "./effect";
// 核心是通过Proxy进行代理,最重要是知道什么时候该set,什么时候该get
// 依赖的收集和触发是effect的关键
// 使用Proxy记得在tsconfig.json中添加 lib": ["DOM"]
export function reactive(raw: any) {
return new Proxy(raw, {
// target指当前对象,key指用户访问的key
// target:{foo:1},key:foo
get(target, key) {
const res = Reflect.get(target, key)
// ***依赖收集***
track(target, key)
return res
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
// ***触发依赖***
trigger(target, key);
return res
}
})
}
4. effect返回runner方法
什么是runner方法?
在Vue 3中,
effect
函数返回一个函数,通常称为“runner函数”。这个函数的作用是执行副作用代码并建立响应式依赖关系。
具体来说,
effect
函数接受一个函数作为参数,这个函数通常包含一些具有副作用的代码,例如对DOM进行操作、发起网络请求等。当调用
effect
函数时,它会立即执行这个函数,并收集所有响应式依赖关系,然后返回这个“runner函数”。
当响应式数据发生变化时,Vue会自动重新执行这个“runner函数”,并重新收集响应式依赖关系。这样就可以自动更新与响应式数据相关的DOM、计算属性等内容。
目标测试
it('should return runner when call effect', () => {
// effect 返回runner
let foo = 10
const runner = effect(() => {
foo++;
return 'foo'
})
expect(foo).toBe(11)
const r = runner()
expect(foo).toBe(12)
expect(r).toBe('foo')
})
代码实现
effect.ts
class ReactiveEffect {
private _fn: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
activeEffect = this;
return this._fn();
}
}
//track方法用于依赖收集
const targetMap = new Map()
export function track(target: any, key: any) {
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
// 将fn存入,因为使用的是set,在多次进行get时,fn不会重复
dep.add(activeEffect)
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
effect.run();
}
}
// 用于记录当前run的effect,以便track进行获取
let activeEffect: any;
// 接收到这个fn需要进行封装,不能丢掉
export function effect(fn: Function) {
const _effect = new ReactiveEffect(fn)
_effect.run();
// 返回run方法作为runner方法,同时bind当前effect处理run方法中this的指向问题
return _effect.run.bind(_effect)
}
5.effect中scheduler功能的实现
什么是scheduler?
在Vue 3中,
effect
函数接受一个可选的
scheduler
函数作为第二个参数。这个
scheduler
函数的作用是
控制何时运行
effect
函数
。
具体来说,**当
effect
函数中的响应式数据发生变化时,Vue会先调用这个
scheduler
函数,而不是立即执行
effect
函数。**要注意的是,effect函数初始化时不会调用scheduler函数。
scheduler
函数接受一个
job
函数作为参数,这个
job
函数就是
effect
函数中包含副作用代码的函数。
scheduler
函数可以自由地决定何时运行
job
函数,例如可以将它放入队列中,以便在下一次事件循环中运行。
通过
scheduler
函数,我们可以控制
effect
函数何时运行,以及如何处理响应式更新。这可以帮助我们优化性能,减少不必要的更新操作。
需要注意的是,如果
scheduler
函数返回一个函数,那么这个函数将会在
effect
函数被停止时执行。这可以用来执行一些清理操作,例如取消定时器或清除事件监听器等。
需要注意的是,
scheduler
函数不是必须的,如果不传入
scheduler
函数,则默认使用Vue内置的
scheduler
函数来执行
effect
函数。这个内置
scheduler
函数会立即执行
effect
函数,不会延迟到下一次事件循环中。
目标测试
it("scheduler", () => {
// 1. 通过effect的第二个参数给定的一个scheduler的fn
// 2. effect方法第一次执行时,还会执行fn
// 3. 当响应式对象set update不会执行fn而是执行scheduler
// 4. 当执行runner的时候,会再次执行fn
let dummy;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
})
const obj = reactive({ foo: 1 })
const runner = effect(() => {
dummy = obj.foo
}, { scheduler })
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1)
// should be called on first trigger
obj.foo++
expect(scheduler).toHaveBeenCalledTimes(1);
// should not run fn yet
expect(dummy).toBe(1)
// manually run
run();
// should have run
expect(dummy).toBe(2)
})
代码实现
effect.ts
class ReactiveEffect {
private _fn: Function
constructor(fn: Function, public scheduler?: any) {
this._fn = fn
}
run() {
activeEffect = this;
return this._fn();
}
}
//track方法用于依赖收集
const targetMap = new Map()
export function track(target: any, key: any) {
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
// 将fn存入,因为使用的是set,在多次进行get时,fn不会重复
dep.add(activeEffect)
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
// scheduler方法来控制触发,可以减少不必要的更新,提高性能
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
// 用于记录当前run的effect,以便track进行获取
let activeEffect: any;
// 接收到这个fn需要进行封装
export function effect(fn: Function, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run();
// 返回run方法作为runner方法,同时bind当前effect处理run方法中this的指向问题
return _effect.run.bind(_effect)
}
6.effect中stop功能的实现
什么是stop?
在Vue 3中,
effect
函数返回一个函数,也就是我们通常称为“runner函数”。这个“runner函数”具有一个名为
stop
的方法,可以用于停止
effect
函数的响应式依赖关系的收集和更新。
调用
stop
方法后,与
effect
函数相关的响应式数据将不再被追踪,不会触发
effect
函数的更新操作。这可以用于手动停止与某个组件或某段代码相关的响应式依赖关系的更新,以避免不必要的性能损耗。
stop
方法不会立即停止
effect
函数的响应式依赖关系的收集和更新,而是将其标记为“停止”。直到下一次响应式数据发生变化时,才会真正停止响应式依赖关系的更新。这是因为Vue 3的响应式系统是基于异步更新的,需要等待下一次事件循环才能更新。
此外,调用
stop
方法后,与
effect
函数相关的响应式数据仍然保持着响应式特性,可以继续被其他
effect
函数追踪和更新。只有与当前
effect
函数相关的响应式依赖关系被停止了。
目标测试
it('stop', () => {
let dummy;
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
stop(runner)
obj.prop = 3
expect(dummy).toBe(2)
//stopped effect should still be manually callable
runner()
expect(dummy).toBe(3)
})
it('onStop', () => {
const obj = reactive({ foo: 1 })
const onStop = jest.fn();
let dummy;
const runner = effect(() => {
dummy = obj.foo
}, { onStop, })
stop(runner)
expect(onStop).toBeCalledTimes(1);
})
代码实现
import { extend } from "../shared"
class ReactiveEffect {
private _fn: Function
public deps: Array<Object> = []
public active: Boolean = true
public onStop?: () => void
public scheduler?: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
activeEffect = this;
return this._fn();
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect: ReactiveEffect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
}
//track方法用于依赖收集
const targetMap = new Map()
export function track(target: any, key: any) {
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
if (!activeEffect) return
// 将fn存入,因为使用的是set,在多次进行get时,fn不会重复
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
// scheduler方法来控制触发,可以减少不必要的更新,提高性能
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
// 用于记录当前run的effect,以便track进行获取
let activeEffect: ReactiveEffect;
// 接收到这个fn需要进行封装
export function effect(fn: Function, options: any = {}) {
const _effect = new ReactiveEffect(fn)
// Object.assign()代替_effect.onStop = options.onStop等等,因为后续会有其他的参数,Object.assign()已经在shared中封装
// 已有scheduler, onStop
extend(_effect, options)
_effect.run();
const runner: any = _effect.run.bind(_effect)
runner.effect = _effect
// 返回run方法作为runner方法,同时bind当前effect处理run方法中this的指向问题
return runner
}
export function stop(runner: any) {
runner.effect.stop()
}
7.实现readonly功能
什么是readonly?
在Vue 3中,
readonly
是一个函数,可以用来创建一个只读的响应式对象。只读的响应式对象是指该对象的属性值不能被修改,仅能被访问和使用。
readonly
函数接受一个对象作为参数,返回一个只读的响应式代理对象。这个只读的响应式代理对象具有与原对象相同的属性和方法,但是属性值不能被修改。
只读的响应式对象适用于那些需要保护数据不被修改的场景,例如在组件中使用
props
传递数据时,可以使用
readonly
来确保这些数据不被修改。
需要注意的是,
只读的响应式对象并不适用于嵌套对象
。如果只读的响应式对象中包含其他对象,那么这些对象的属性值仍然可以被修改。如果需要对嵌套对象进行只读保护,可以使用递归的方式对嵌套对象进行处理。
目标测试
import { readonly } from "../reactive"
describe("readonly", () => {
it("happy path", () => {
// not set
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
})
it("warn when call set", () => {
console.warn = jest.fn()
const user = readonly({ age: 10 })
user.age = 11
expect(console.warn).toBeCalled()
})
})
代码实现(已重构)
baseHandlers.ts
import { track, trigger } from "./effect"
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
function createGetter(isReadonly = false) {
return function get(target: any, key: any) {
const res = Reflect.get(target, key)
if (!isReadonly) {
track(target, key)
}
return res
}
}
function createSetter() {
return function get(target: any, key: any, value: any) {
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
}
}
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set(target: any, key: any, value: any) {
console.warn(`key:${key} set failure, because target is readonly`, target)
return true;
}
}
reactive.ts
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
// 核心是通过Proxy进行代理,最重要是知道什么时候该set,什么时候该get
// 依赖的收集和触发是effect的关键
// 使用Proxy记得在tsconfig.json中添加 lib": ["DOM"]
// set和get已经重构至baseHandlers
export function reactive(raw: any) {
return createActiveObject(raw, mutableHandlers)
}
// readonly
export function readonly(raw: any) {
return createActiveObject(raw, readonlyHandlers)
}
function createActiveObject(raw: any, baseHandlers: any) {
return new Proxy(raw, baseHandlers)
}
8.实现isReactive和isReadonly功能
什么是isReactive和isReadonly?
在Vue 3中,可以使用
isReactive
和
isReadonly
函数来判断一个对象是否是响应式对象和只读对象。
isReactive
函数接受一个对象作为参数,返回一个布尔值,表示这个对象是否是响应式对象。如果对象是响应式对象,则返回
true
,否则返回
false
。
isReadonly
函数接受一个对象作为参数,返回一个布尔值,表示这个对象是否是只读对象。如果对象是只读对象,则返回
true
,否则返回
false
。
目标测试
import { isReadonly, readonly } from "../reactive"
describe("readonly", () => {
it("happy path", () => {
// not set
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
// 测试isReadonly
expect(isReadonly(wrapped)).toBe(true)
expect(isReadonly(original)).toBe(false)
})
it("warn when call set", () => {
console.warn = jest.fn()
const user = readonly({ age: 10 })
user.age = 11
expect(console.warn).toBeCalled()
})
})
import { reactive, isReactive } from "../reactive";
describe('reactive', () => {
it('happy path', () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
// 测试isReactive
expect(isReactive(observed)).toBe(true)
expect(isReactive(original)).toBe(false)
})
})
代码实现
reactive.ts
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly"
}
// 核心是通过Proxy进行代理,最重要是知道什么时候该set,什么时候该get
// 依赖的收集和触发是effect的关键
// 使用Proxy记得在tsconfig.json中添加 lib": ["DOM"]
// set和get已经重构至baseHandlers
export function reactive(raw: any) {
return createActiveObject(raw, mutableHandlers)
}
// readonly
export function readonly(raw: any) {
return createActiveObject(raw, readonlyHandlers)
}
export function isReactive(value: any) {
return !!value[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value: any) {
return !!value[ReactiveFlags.IS_READONLY]
}
function createActiveObject(raw: any, baseHandlers: any) {
return new Proxy(raw, baseHandlers)
}
baseHandlers.ts
import { track, trigger } from "./effect"
import { ReactiveFlags } from "./reactive"
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
function createGetter(isReadonly = false) {
return function get(target: any, key: any) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
const res = Reflect.get(target, key)
if (!isReadonly) {
track(target, key)
}
return res
}
}
function createSetter() {
return function get(target: any, key: any, value: any) {
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
}
}
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set(target: any, key: any, value: any) {
console.warn(`key:${key} set failure, because target is readonly`, target)
return true;
}
}
9.优化effect中的stop功能
当前问题
在stop(runner)之后,如果使用obj.prop++,即ob j.prop = obj.prop + 1。触发了get和set,触发get时又会重新收集依赖,从而导致stop方法无效。
目标测试
it('stop', () => {
let dummy;
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
stop(runner)
obj.prop++
expect(dummy).toBe(2)
//stopped effect should still be manually callable
runner()
expect(dummy).toBe(3)
})
代码实现
effect.ts
主要是添加了shouldTrack进行控制
import { extend } from "../shared"
// 用于记录当前run的effect,以便track进行获取
let activeEffect: ReactiveEffect;
//track方法用于依赖收集
const targetMap = new Map()
// 控制track
let shouldTrack = false;
class ReactiveEffect {
private _fn: Function
public deps: Array<Object> = []
public active: Boolean = true
public onStop?: () => void
public scheduler?: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
if (!this.active) {
return this._fn();
}
shouldTrack = true
activeEffect = this;
const result = this._fn()
shouldTrack = false
return result
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect: ReactiveEffect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
effect.deps.length = 0
}
export function track(target: any, key: any) {
// 如果是stop的对象,则不需要track
if (!isTracking()) return
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
// 将fn存入,因为使用的是set,在多次进行get时,fn不会重复
if (dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
function isTracking() {
return shouldTrack && activeEffect !== undefined
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
// scheduler方法来控制触发,可以减少不必要的更新,提高性能
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
// 接收到这个fn需要进行封装
export function effect(fn: Function, options: any = {}) {
const _effect = new ReactiveEffect(fn)
// Object.assign()代替_effect.onStop = options.onStop等等,因为后续会有其他的参数,Object.assign()已经在shared中封装
// 已有scheduler, onStop
extend(_effect, options)
_effect.run();
const runner: any = _effect.run.bind(_effect)
runner.effect = _effect
// 返回run方法作为runner方法,同时bind当前effect处理run方法中this的指向问题
return runner
}
export function stop(runner: any) {
runner.effect.stop()
}
10.实现reactive和readonly嵌套对象转换功能
目标测试
test("nested reactive", () => {
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)
})
it("happy path", () => {
// not set
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
expect(isReadonly(wrapped)).toBe(true)
expect(isReadonly(wrapped.bar)).toBe(true)
expect(isReadonly(original)).toBe(false)
})
代码实现
baseHandlers.ts
添加isObject(val)判断
function createGetter(isReadonly = false) {
return function get(target: any, key: any) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
const res = Reflect.get(target, key)
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
track(target, key)
}
return res
}
}
export const isObject = (val: any) => {
return val !== null && typeof val === "object"
}
11.实现shallowReadonly功能
什么是shallowReadonly?
在 Vue 3 中,
shallowReadonly
是一个函数,可以将一个对象转换为只读的代理对象。与
readonly
不同的是,
shallowReadonly
只会将对象的第一层属性转换为只读的代理对象,而不会递归转换嵌套对象的属性。这意味着,如果对象的属性仍然是对象,那么这些属性不会被转换为只读的代理对象。
const obj = {
name: 'Vue',
version: 3,
author: {
name: 'Evan You',
company: 'Vue.js'
}
}
//如果将 obj 转换为只读的代理对象,可以使用 shallowReadonly:
import { shallowReadonly } from 'vue'
const readonlyObj = shallowReadonly(obj)
//此时,readonlyObj 是一个只读的代理对象,可以访问 name 和 version 属性,但是 author 属性仍然是可写的对象。也就是说,以下代码是有效的:
readonlyObj.name // 'Vue'
readonlyObj.version // 3
readonlyObj.author.name = 'John Doe'
readonlyObj.author.name // 'John Doe'
//但是,以下代码是无效的:
readonlyObj.author = {} // 抛出 TypeError
//因为 author 属性仍然是原始对象的引用,而 readonlyObj 不能修改原始对象。
目标测试
import { isReadonly, shallowReadonly } from "../reactive"
describe("shallowReadonly", () => {
test("should not make non-reactive properties reactive", () => {
const props = shallowReadonly({ n: { foo: 1 } })
expect(isReadonly(props)).toBe(true)
expect(isReadonly(props.n)).toBe(false)
})
})
代码实现
reactive.ts
export function shallowReadonly(raw: any) {
return createActiveObject(raw, shallowReadonlyHandlers)
}
baseHandlers.ts
补充isShallow的判断以及shallowReadonlyGet的创建
import { extend, isObject } from "../shared"
import { track, trigger } from "./effect"
import { ReactiveFlags, reactive, readonly } from "./reactive"
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)
function createGetter(isReadonly = false, isShallow = false) {
return function get(target: any, key: any) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
const res = Reflect.get(target, key)
if (isShallow) {
return res
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
track(target, key)
}
return res
}
}
function createSetter() {
return function get(target: any, key: any, value: any) {
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
}
}
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set(target: any, key: any, value: any) {
console.warn(`key:${key} set failure, because target is readonly`, target)
return true;
}
}
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet })
12.实现isProxy功能
在 Vue 3 中,
isProxy
是一个函数,用于检查一个对象是否是一个响应式代理对象。
import { reactive, isProxy } from 'vue'
const obj = reactive({
name: 'Vue',
version: 3
})
const proxyObj = new Proxy(obj, {})
console.log(isProxy(obj)) // true
console.log(isProxy(proxyObj)) // true
console.log(isProxy({})) // false
目标测试
import { isReadonly, readonly, isProxy } from "../reactive"
describe("readonly", () => {
it("happy path", () => {
// not set
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
expect(isReadonly(wrapped)).toBe(true)
expect(isReadonly(wrapped.bar)).toBe(true)
expect(isReadonly(original)).toBe(false)
expect(isProxy(wrapped)).toBe(true)
expect(isProxy(original)).toBe(false)
})
it("warn when call set", () => {
console.warn = jest.fn()
const user = readonly({ age: 10 })
user.age = 11
expect(console.warn).toBeCalled()
})
})
import { reactive, isReactive, isProxy } from "../reactive";
describe('reactive', () => {
it('happy path', () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
expect(isReactive(observed)).toBe(true)
expect(isReactive(original)).toBe(false)
expect(isProxy(observed)).toBe(true)
expect(isProxy(original)).toBe(false)
})
test("nested reactive", () => {
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)
})
})
代码实现
reactive.ts
export function isProxy(value: any) {
return isReactive(value) || isReadonly(value)
}
无法判断原生Proxy对象是不是Proxy,但Proxy上也没有能直接判断的方法,这是个值得解决的问题
13.实现ref功能
在 Vue 3 中,
ref
是一个函数,用于将一个普通的 JavaScript 值转换为响应式数据。如果要将一个对象转换为响应式数据,可以使用
reactive
函数。
目标测试
三个测试,可以分步逐一实现
import { effect } from '../effect';
import { ref } from '../ref'
describe("ref", () => {
it("happy path", () => {
const a = ref(1);
expect(a.value).toBe(1)
})
it("should 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 make nested properties reactive", () => {
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 { hasChanged, isObject } from "../shared";
import { trackEffects, triggerEffect, isTracking } from "./effect";
import { reactive } from "./reactive";
class RefImpl {
private _value: any;
private _rawValue: any;
public dep;
constructor(value: any) {
this._rawValue = value
this._value = convert(value)
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue: any) {
// 相等则直接返回
if (!hasChanged(this._rawValue, newValue)) return
this._value = convert(newValue)
this._rawValue = newValue
triggerEffect(this.dep)
}
}
// ref处理的是单值,但传入的value是对象时,需要转换为reactive对象
function convert(value: any) {
return isObject(value) ? reactive(value) : value;
}
function trackRefValue(ref: RefImpl) {
if (isTracking()) {
trackEffects(ref.dep)
}
}
export function ref(value: any) {
return new RefImpl(value)
}
effect.ts
重构以适用ref
import { extend } from "../shared"
// 用于记录当前run的effect,以便track进行获取
let activeEffect: ReactiveEffect;
//track方法用于依赖收集
const targetMap = new Map()
// 控制track
let shouldTrack = false;
class ReactiveEffect {
private _fn: Function
public deps: any = []
public active: Boolean = true
public onStop?: () => void
public scheduler?: Function
constructor(fn: Function) {
this._fn = fn
}
run() {
if (!this.active) {
return this._fn();
}
shouldTrack = true
activeEffect = this;
const result = this._fn()
shouldTrack = false
return result
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect: ReactiveEffect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
effect.deps.length = 0
}
export function track(target: any, key: any) {
// 如果是stop的对象,则不需要track
if (!isTracking()) return
//target -> key -> dep
let depsMap = targetMap.get(target)
// 初始化时,depsMap是没有的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key)
// 初始化时,dep也是没有的
if (!dep) {
dep = new Set();
depsMap.set(key, dep)
}
trackEffects(dep)
}
export function trackEffects(dep: any) {
if (dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
// trigger方法用于依赖相关的effect触发
export function trigger(target: any, key: any) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
triggerEffect(dep)
}
export function triggerEffect(dep: any) {
for (const effect of dep) {
// scheduler方法来控制触发,可以减少不必要的更新,提高性能
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run();
}
}
}
// 接收到这个fn需要进行封装
export function effect(fn: Function, options: any = {}) {
const _effect = new ReactiveEffect(fn)
// Object.assign()代替_effect.onStop = options.onStop等等,因为后续会有其他的参数,Object.assign()已经在shared中封装
// 已有scheduler, onStop
extend(_effect, options)
_effect.run();
const runner: any = _effect.run.bind(_effect)
runner.effect = _effect
// 返回run方法作为runner方法,同时bind当前effect处理run方法中this的指向问题
return runner
}
export function stop(runner: any) {
runner.effect.stop()
}
14.实现isRef和unRef功能
isRef
是一个函数,用于检查一个变量是否是
Ref
对象
unRef
是一个函数,用于获取
Ref
对象的值。如果传入的值不是
Ref
对象,则直接返回该值。
目标测试
it("isRef", () => {
const a = ref(1)
const user = reactive({
age: 1
})
expect(isRef(a)).toBe(true);
expect(isRef(1)).toBe(false);
expect(isRef(user)).toBe(false)
})
it("unRef", () => {
const a = ref(1)
expect(unRef(a)).toBe(1);
expect(unRef(1)).toBe(1);
})
代码实现
ref.ts
import { hasChanged, isObject } from "../shared";
import { trackEffects, triggerEffect, isTracking } from "./effect";
import { reactive } from "./reactive";
class RefImpl {
private _value: any;
private _rawValue: any;
public dep;
public __v_isRef = true;
constructor(value: any) {
this._rawValue = value
this._value = convert(value)
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue: any) {
// 相等则直接返回
if (!hasChanged(this._rawValue, newValue)) return
this._value = convert(newValue)
this._rawValue = newValue
triggerEffect(this.dep)
}
}
// ref处理的是单值,但传入的value是对象时,需要转换为reactive对象
function convert(value: any) {
return isObject(value) ? reactive(value) : value;
}
function trackRefValue(ref: RefImpl) {
if (isTracking()) {
trackEffects(ref.dep)
}
}
export function ref(value: any) {
return new RefImpl(value)
}
export function isRef(ref: any) {
return !!ref.__v_isRef
}
export function unRef(ref: any) {
return isRef(ref) ? ref._value : ref
}
15.实现proxyRefs功能
什么是proxyRefs?
通常情况下,当你使用
ref
、
reactive
、
computed
等函数创建响应式对象时,会返回一个普通的 JavaScript 对象。这样的对象虽然拥有响应式能力,但是无法像普通对象一样直接访问属性和方法。例如ref对象需要使用.value来获取属性
proxyRefs
的作用就是将
普通的响应式对象
转换为一个
响应式代理对象
,这个代理对象具有以下特点:
- 可以直接访问对象的属性和方法,就像访问普通的 JavaScript 对象一样。
- 在访问属性和方法时,会自动进行依赖收集和触发更新。
- 可以通过解构赋值等方式进行属性的提取和组合。
目标测试
it("proxyRef", () => {
const user = {
age: ref(10),
name: "xiaoming"
}
const proxyUser = proxyRefs(user)
// test get
expect(user.age.value).toBe(10)
expect(proxyUser.age).toBe(10)
expect(proxyUser.name).toBe("xiaoming");
// test set value
proxyUser.age = 20;
expect(proxyUser.age).toBe(20)
expect(user.age.value).toBe(20)
// test set ref
proxyUser.age = ref(10)
expect(proxyUser.age).toBe(10)
expect(user.age.value).toBe(10)
})
代码实现
ref.ts
export function proxyRefs(objectWithRefs: any) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unRef(Reflect.get(target, key))
},
set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value)
} else {
return Reflect.set(target, key, value)
}
}
})
}
16.实现computed计算属性
什么是computed计算属性?
使用
computed
函数可以将一个计算属性定义为一个函数,这个函数的返回值就是计算属性的值。当计算属性所依赖的数据发生变化时,计算属性的值也会自动更新。
计算属性的值是惰性求值的,也就是说,只有在访问计算属性时才会进行计算。如果计算属性所依赖的数据没有变化,那么计算属性的值也不会重新计算。
目标测试
import { computed } from "../computed"
import { reactive } from "../reactive"
describe("computed", () => {
it("happy path", () => {
const user = reactive({
age: 1
})
const age = computed(() => {
return user.age
})
expect(age.value).toBe(1)
})
it("should compute lazily", () => {
const value = reactive({
foo: 1
})
const getter = jest.fn(() => {
return value.foo
})
const cValue = computed(getter)
// lazy
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1)
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(1)
// should not compute until needed
value.foo = 2;
expect(getter).toHaveBeenCalledTimes(1)
// now it should compute
expect(cValue.value).toBe(2)
expect(getter).toHaveBeenCalledTimes(2)
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(2)
})
})
代码实现
computed.ts
computed的实现很巧妙,结合了effect的用法,用一个dirty标识实现了缓存
import { ReactiveEffect } from "./effect";
class computedRefImpl {
private _getter;
private _dirty = true;
private _value: any;
private _effect: any;
constructor(getter: any) {
this._getter = getter
this._effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
}
})
}
get value() {
if (this._dirty) {
this._dirty = false
this._value = this._effect.run()
}
return this._value;
}
}
export function computed(getter: any) {
return new computedRefImpl(getter)
}