前言
注:本文过长过干,观看前请备足水分。and,虽然是ts写的,但是并不关心类型,所有的类型注解基本只是为了防报错。
在手写Vue3核心代码之前,作者完全不知道TDD(测试驱动开发)是啥,在完成下文代码之前补习了一下TDD的基本思想以及一款测试框架——jest。所以在阅读之前,最好希望你对jest框架有基本的认知(阅读官方文档半小时能入门),以及明白为何在完成功能之前需要先写测试代码——这有助于减少调试的耗时,细致拆分的测试文件能够将bug扼杀在摇篮之中。
同时,不仅仅是完成代码,为了保证代码的可读性还会对完成功能后的代码进行重构,当然作者的重构能力仅限参考,清谨慎阅读😀。
reactivity的核心流程
以一段reactive和effect的配合使用的代码来说明响应式的核心流程——依赖收集以及依赖触发。
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
console.log(nextAge) //11
//update
user.age++
console.log(nextAge) //12
这里演示了一个最基本功能的数据响应式案例,effect函数和reactive函数都是reactivity模块中的核心API,reactive函数通过Proxy代理传入的对象,在getter和setter阶段会分别执行依赖收集以及依赖的触发的工作。而effect函数接收一个回调函数,初始默认执行一次,并在回调中对应的依赖更新时再次执行。
当然reactivity还包含其他响应式API如ref等,这里先实现最基本的reactive和effect模块。
reactive和effect的实现
分别测试reactive和effect的逻辑,单元测试(jest) 代码如下:
//effect.spec.ts
//effect测试了reactive的响应式
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的代理中的get和set
//reactive.ts
export function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
//收集依赖 在effect中实现track,因为要获取activeEffect对象
//...
return Reflect.get(target, key)
},
set(target, key, value) {
//触发依赖 在effect中实现trigger
//...
return Reflect.set(target, key, value)
}
})
}
在这里拆分并测试一下reactive函数的逻辑,是可以通过的
//reactive.spec.ts
//测试reactive的代理功能
describe('reactive', () => {
it('happy path', () => {
const obj = { a: 1 }
const reactiveObj = reactive(obj)
expect(reactiveObj).not.toBe(obj)
expect(reactiveObj.a).toBe(1)
}));
第二步,再get和set中分别收集依赖(track)和收集依赖(trigger),这两个函数的实现放在了effect.ts文件中,因为需要利用外部作用域和effect函数共用activeEffect对象(当前活跃的ReactiveEffect对象),这个点后面就知道为什么了。
//effect.ts中的track和trigger的实现
let activeEffect: ReactiveEffect
const targetsMap = new Map
export function track(target, key) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let deps = depsMap.get(key)
//将现在的effect对象放入deps
deps.add(activeEffect)
}
export function trigger(target, key, value) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let deps = depsMap.get(key)
//通知依赖
for (let dep of deps) {
dep.run()
}
}
第三步,编写effect函数的功能——执行传入的函数,并且能够配合track和trigger实现依赖的触发与收集
class ReactiveEffect {
fn: Function
constructor(fn) {
this.fn = fn
}
run() {
this.fn()
}
}
export function effect(fn: Function) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn)
activeEffect = _effect
_effect.run()
}
这里不是直接在effect函数中执行fn,而是定义了一个ReactiveEffect对象,这个对象其实是观察者模式中的观察者(Observer),观察者上面定义了run函数,在主体(Subjcet)——也就是之前reactive所代理对象的某个key值发生改变时,会通知所有对应的观察者——即track和trigger中的deps集合中所包含的对象执行其run函数。
完善effect的功能
effect返回runner
effect的返回值runner是一个函数,且具有和传入的函数相同的功能和返回值。测试代码如下:
//测试返回runner的功能
it('return runner', () => {
let foo = 1
let fn = effect(() => {
foo++
return 'foo'
})
expect(foo).toBe(2)
const n = fn()
expect(foo).toBe(3)
expect(n).toBe('foo')
});
})
实现也很简单,修改后的代码如下:
class ReactiveEffect {
private fn: Function
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
export function effect(fn: Function) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn)
_effect.run()
//bind是因为run函数内部有this值的指向问题
return _effect.run.bind(_effect)
}
scheduler
effect的第二个参数是可选参数——配置对象options,其中的scheduler是一个函数对象,如果有scheduler的话,effect只会在初始化时执行fn,在之后的触发更新阶段都会执行scheduler。测试代码如下:
//测试scheduler功能
it('scheduler ', () => {
let run: any
let scheduler = function () {
run = runner
}
let foo = reactive({ bar: 1 })
let bee
let runner = effect(() => {
bee = foo.bar + 1
return 'runner'
}, {
scheduler: scheduler
})
expect(bee).toBe(2)
//trigger,执行的不是fn,而是scheduler
foo.bar++//foo.bar == 2 ,没有执行fn,所以bee还是2 ,执行了scheduler,所以run可以使用
expect(bee).toBe(2)
const n = run()//执行run,即执行fn函数,此时bee == foo.bar +1 ==3,且n为fn的返回值
expect(bee).toBe(3)
expect(n).toBe('runner')
});
实现如下,只需要改变effect的二个参数为可选参数,以及改写ReactiveEffect对象的构造函数并向外暴露scheduler,在trigger中判断并调用即可。
//effect第二个参数为可选参数
export function effect(fn: Function, options?: any) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn, options)
_effect.run()
return _effect.run.bind(_effect)
}
//向外暴露scheduler
class ReactiveEffect {
private fn: Function
public scheduler
constructor(fn, options={}) {
this.fn = fn
this.scheduler = options.scheduler
}
run() {
activeEffect = this
this.fn()
return this.fn
}
}
//trigger中调用
export function trigger(target, key, value) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let deps = depsMap.get(key)
//通知依赖
for (let dep of deps) {
if (dep.scheduler) dep.scheduler()
else dep.run()
}
}
stop和onStop
effect向外暴露一个stop函数,通过 stop(runner)(runner是effect的返回值,即effect对象上的run方法)取消触发effect对应的依赖,再次执行runner则又可以恢复其依赖的触发。
而onStop是effect的配置选项,会在用户调用 stop(runner)时调用对应effect对象的onStop回调,这一部分的测试代码如下,这部分遇到了一些插曲,分享一些bug的解决过程。
//测试stop和onStop
it('stop and onStop', () => {
let foo = reactive({ bar: 1 })
let dummy
let str = ''
let onStop = jest.fn(() => { str += 'stop!' })
let runner = effect(() => {
dummy = foo.bar
}, { onStop })
expect(dummy).toBe(1)
stop(runner)
foo.bar = 1
//foo.bar++ 如果是这种写法,测试不通过,问题后续再讲
expect(dummy).toBe(1)//dummy失去了响应性
expect(onStop).toBeCalledTimes(1)//onStop被调用
runner()
expect(dummy).toBe(2)//dummy恢复响应性
});
首先,实现stop部分——向外暴露一个stop函数,接受runner,该如何清除依赖呢?我们的做法是通过在收集依赖的同时反向记录依赖所存在的位置:即观察者也会收集主体的dep。
//trigger部分的修改实现
export function track(target, key) {
//......
//反向记录,每个Effect对象都会记录自己所在的deps
activeEffect.deps.add(dep)
}
class ReactiveEffect {
//...
public deps: Set<any>
}
通过类内部访问这个deps属性是最好的,但是stop函数只有runner参数,因此我们将对应的ReactiveEffect对象挂载到runner上
export function stop(runner) {
runner._effect.stop()
}
export function effect(fn: Function, options?: any) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn, options)
effect.run()
const runner:any = _effect.run.bind(_effect)
runner._effect = _effect
return runner
}
之后再类内部实现stop方法如下,而onStop只需要在里面调用即可
class ReactiveEffect {
private fn: Function
public scheduler?
public onStop?: Function
public deps: Set<any>
constructor(fn, options?) {
this.fn = fn
this.scheduler = options.scheduler
this.onStop = options.onStop
this.deps = new Set
}
//...
stop() {
//找到deps并一一删除
this.deps.forEach(dep => dep.delete(this))
//执行onStop回调
this.onStop && this.onStop()
}
}
最后测试一哈,通过!但是在这有作者在测试中发现的一个小问题可以优化:
一点小bug
如下,只是改动了foo.bar的赋值方式,就发现测试不通过,并不是stop不起作用,原因在于foo.bar++等同于 foo.bar = foo.bar + 1的操作,会再次触发一次get和set,此时的activeEffect还是之前的effect,所以又执行了依赖收集和触发。
//测试stop和onStop
it('stop and onStop', () => {
let foo = reactive({ bar: 1 })
let dummy
let str = ''
let onStop = jest.fn(() => { str += 'stop!' })
let runner = effect(() => {
dummy = foo.bar
}, { onStop })
expect(dummy).toBe(1)
stop(runner)
foo.bar++//foo.bar++ 如果是这种写法,测试不通过
expect(dummy).toBe(1)//dummy失去了响应性
expect(onStop).toBeCalledTimes(1)//onStop被调用
runner()
expect(dummy).toBe(2)//dummy恢复响应性
});
如何解决
可以在stop中清除活动对象activeEffect并在收集依赖时判断activeEffect的存在性。当然也有另外的解决方式(官方做法)——在ReactiveEffect实例对象上添加是否需要被tracking的标志isTracking,并在执行runner时置为true,在stop后被置为false,在收集依赖时判断该标志即可。感兴趣的同学可以自行尝试。
export function track(target, key) {
//...
//当前如果没有活动对象
if (!activeEffect) return
//...
}
class ReactiveEffect {
//...
stop() {
//找到deps并一一删除
this.deps.forEach(dep => dep.delete(this))
//清空活动对象
activeEffect = null
//执行onStop回调
this.onStop && this.onStop()
}
}
最后对代码进行了一些重构小优化,完整代码如下:
import { extend } from "./shared/extend"
class ReactiveEffect {
private fn: Function
public scheduler?
public onStop?: Function
public deps: Set<any> = new Set
active: boolean = true
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
this.active = true
return this.fn()
}
stop() {
if (this.active) {
cleanEffect(this)
//执行onStop回调
this.onStop && this.onStop()
this.active = false
}
}
}
function cleanEffect(effect) {
//找到deps并一一删除
effect.deps.forEach(dep => dep.delete(effect))
//清空活动对象
activeEffect = null
}
export function effect(fn: Function, options?: any) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn)
//将options的属性赋予_effect
extend(_effect, options)
_effect.run()
const runner: any = _effect.run.bind(_effect)
runner._effect = _effect
return runner
}
let activeEffect: ReactiveEffect | null
const targetsMap = new Map
export function track(target, key) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let dep = depsMap.get(key)
//当前如果没有活动对象
if (!activeEffect) return
//将现在的effect对象放入deps
dep.add(activeEffect)
//反向记录,每个Effect对象都会记录自己所在的deps
activeEffect.deps.add(dep)
}
export function trigger(target, key, value) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let dep = depsMap.get(key)
//通知依赖
for (let effect of dep) {
if (effect.scheduler) effect.scheduler()
else effect.run()
}
}
export function stop(runner) {
runner._effect.stop()
}
实现readonly
readonly方法可以将一个普通对象或者响应式对象变为readonly对象,测试代码如下:
import { readonly } from "../reactive";
describe('readonly', () => {
it('happy path', () => {
const raw = { foo: 1 }
let dummy = readonly(raw)
expect(dummy).not.toBe(raw)
expect(dummy.foo).toBe(1)
console.warn = jest.fn()
dummy.foo = 2
expect(console.warn).toBeCalledTimes(1)
});
});
实现如下,和reactive的实现逻辑基本一致,但是不需要触发和收集依赖(因为不会改变对象中的值)。
export function readonly(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key)
return res
},
set(target, key, value) {
//抛出警告
console.warn(`${target}为readonly对象,不能对${key.toString()}做修改`)
}
})
}
重构&优化
到这里测试是可以通过的,但是代码结构显然有些臃肿,readonly和reactive的重合部分过多,且缺乏可读性。我们可以首先将两者的get和set抽离出去,定义两个高阶函数createGetter和createSetter函数,通过传入的isReadonly标识将readonly和reactive的逻辑分离
import { track, trigger } from "./effect"
export function reactive(obj) {
return new Proxy(obj, {
get: createGetter(),
set: createSetter()
})
}
export function readonly(obj) {
return new Proxy(obj, {
get: createGetter(true),
set: createSetter(true)
})
}
function createGetter(isReadonly: boolean = false) {
return function get(target, key) {
const res = Reflect.get(target, key)
if (!isReadonly) {
//收集依赖
//track函数传入target和key,通过targetsMap找到target对应的依赖depsMap,再根据key设置对应的deps的fn
track(target, key)
}
return res
}
}
function createSetter(isReadonly: boolean = false) {
return function set(target, key, value) {
if (!isReadonly) {
const res = Reflect.set(target, key, value);
//触发依赖
//同样是通过targetsMap和depsMap,依次触发deps中的回调函数
trigger(target, key, value);
return res
}
else {
//抛出警告
console.warn(`${target}为readonly对象,不能对${key.toString()}做修改`);
return true
}
};
}
上面的代码还可以进一步抽离,可以看出reactive和readonly实现差别在于Proxy的第二个参数handlers上,定义reactive和eadonly对应的handlers为mutableHandlers和readonlyHandlers,将它们放到一个文件——baseHandlers.ts内。Vue3的源码也是这样的处理,对于不同类型的handlers作了语义上的划分。
//reactive.ts
import { mutableHandlers, reaonlyHandlers } from "./baseHandlers"
export function reactive(obj) {
return new Proxy(obj, mutableHandlers)
}
export function readonly(obj) {
return new Proxy(obj, reaonlyHandlers)
}
//baseHandlers.ts
import { track, trigger } from "./effect";
export const reaonlyHandlers = {
get: createGetter(true),
set: createSetter(true)
}
export const mutableHandlers = {
get: createGetter(),
set: createSetter()
}
export function createGetter(isReadonly: boolean = false) {
return function get(target, key) {
//...
};
}
export function createSetter(isReadonly: boolean = false) {
return function set(target, key, value) {
//...
};
}
最后再增加一点可读性和小优化(不用每次get和set操作都要createGetter和createSetter)捏
//reactive.ts
import { mutableHandlers, reaonlyHandlers } from "./baseHandlers"
export function reactive(obj) {
return createActiveObject(obj, mutableHandlers)
}
export function readonly(obj) {
return createActiveObject(obj, reaonlyHandlers)
}
function createActiveObject(obj: any, handlers) {
return new Proxy(obj, handlers)
}
//baseHandlers.ts
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const readonlySet = createSetter(true)
export const reaonlyHandlers = {
get,
set
}
export const mutableHandlers = {
readonlyGet,
readonlySet
}
重构之后别忘了测试一下之前全部的测试代码捏( ̄▽ ̄)*。
实现isReactive,isReadonly和isProxy
先写单元测试:
describe('isReactive', () => {
it('happy path', () => {
const foo = reactive({ bar: 1 })
const dummy = {}
expect(isReactive(foo)).toBe(true)
expect(isReactive(dummy)).toBe(false)
});
})
describe('isReadonly', () => {
it('happy path', () => {
const foo = readonly({ bar: 1 })
const dummy = {}
expect(isReadonly(foo)).toBe(true)
expect(isReadonly(dummy)).toBe(false)
});
})
describe('isProxy', () => {
it('happy path', () => {
const dummy = { bar: 1 }
const foo = reactive(dummy)
const bee = readonly(dummy)
expect(isProxy(foo)).toBe(true)
expect(isProxy(bee)).toBe(true)
expect(isProxy(dummy)).toBe(false)
});
});
isReactive、isReadonly和isProxy用来识别对象的类型,实现的方式非常巧妙。以isReactive举例,通过访问被检测对象的特定属性,以及在reactive中对应的get handler中处理响应式对象访被问特定属性时根据isReadonly标识符返回值,即可实现对reactive对象的判断。
isReadonly和isProxy方法实现也基本一致,完整代码如下:
//reactive.ts
//创建了一个枚举对象
export const enum ReactiveFlags {
IS_REACTIVE = '_v_isReactive',
IS_READONLY = '_v_isReadonly',
IS_PROXY = '_v_isProxy'
}
export function isReactive(target) {
return Boolean(target[ReactiveFlags.IS_REACTIVE])
}
export function isReadonly(target) {
return Boolean(target[ReactiveFlags.IS_READONLY])
}
export function isProxy(target) {
return Boolean(target[ReactiveFlags.IS_PROXY])
}
export function createGetter(isReadonly: boolean = false) {
return function get(target, key) {
if (key == ReactiveFlags.IS_REACTIVE) return !isReadonly
else if (key == ReactiveFlags.IS_READONLY) return isReadonly
else if (key == ReactiveFlags.IS_PROXY) return true
//...
};
}
实现reactive和readonly的深层嵌套逻辑
我们知道reactive和readonly代理的嵌套对象内的深层对象也是reactive/readonly类型的,测试代码如下:
//reactive
test('nested reactive', () => {
const original = {
nested: {
foo: 1
},
arr: [{ bar: 1 }]
}
const observed = reactive(original)
expect(isReactive(observed.nested)).toBe(true)
expect(isReactive(observed.arr[0])).toBe(true)
})
//readonly
test('nested reactive', () => {
const original = {
nested: {
foo: 1
},
arr: [{ bar: 1 }]
}
const observed = readonly(original)
expect(isReadonly(observed.nested)).toBe(true)
expect(isReadonly(observed.arr[0])).toBe(true)
observed.nested.foo = 2
expect(observed.nested.foo).not.toBe(2)
})
实现的话只需要在get中递归返回使每个对象类型的属性reactive化或readonly化对象即可:
function createGetter(isReadonly: boolean = false) {
return function get(target, key) {
//...
if (isObject(res)) return isReadonly ? readonly(res) : reactive(res)
//...
};
}
function isObject(obj){
return obj != null && typeof obj == 'object'
}
实现shallowReactive和shallowReadonly
前面实现了reactive和readonly的深层嵌套转换功能,现在来实现只会转换浅层对象的shallowReactive和shallowReadonly,单测如下,把上次单测的断言结果取反即可:
//reactive
test('nested reactive', () => {
const original = {
nested: {
foo: 1
},
arr: [{ bar: 1 }]
}
const observed = reactive(original)
expect(isReactive(observed.nested)).toBe(false)
expect(isReactive(observed.arr[0])).toBe(false)
})
//readonly
test('nested readonly', () => {
const original = {
nested: {
foo: 1
},
arr: [{ bar: 1 }]
}
const observed = readonly(original)
expect(isReadonly(observed.nested)).toBe(false)
expect(isReadonly(observed.arr[0])).toBe(false)
observed.nested.foo = 2
expect(observed.nested.foo).toBe(2)
})
同readonly的做法一样,在创建shallowHandlers的get时,定义一个shallow标识符,根据标识符决定是否深层递归收集依赖。
function createGetter(isReadonly: boolean = false, isShallow: boolean = false) {
return function get(target, key) {
if (key == ReactiveFlags.IS_REACTIVE) return !isReadonly
else if (key == ReactiveFlags.IS_READONLY) return isReadonly
const res = Reflect.get(target, key);
//先判断是否会深层递归,再判断readonly
if (!isShallow && isObject(res)) return isReadonly ? readonly(res) : reactive(res)
if (!isReadonly) {
//收集依赖
//track函数传入target和key,通过targetsMap找到target对应的依赖depsMap,再根据key设置对应的deps的fn
track(target, key);
}
return res;
};
}
之后根据createGetter创建shallowReactiveGet和shallowReadonlyGet,再利用之前定义好的extend(Object.assign的语义化赋值)重写mutableHandlers和readonlyHandlers的get,得到最终的shallowReactiveHandlers和shallowReadonlyHandlers:
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const readonlySet = createSetter(true)
const shallowReactiveGet = createGetter(false, true)
const shallowReadonlyGet = createGetter(true, true)
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set: readonlySet
}
export const shallowReactiveHandlers = extend({}, mutableHandlers, { get: shallowReactiveGet })
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet })
然后再导出shallowReactive和shallowReadonly方法:
export function shallowReactive(obj) {
return createActiveObject(obj, shallowReactiveHandlers)
}
export function shallowReadonly(obj) {
return createActiveObject(obj, shallowReadonlyHandlers)
}
测试通过,别忘了把之前所有的单元测试再测一遍噢( •̀ ω •́ )y
实现ref
ref是reactivity模块中一个重要的api,它会将普通值封装成一个响应式对象,同value属性来调用值,如果传入的是一个对象,则会调用reactivity方法进行封装。
测试代码分为三个部分:happy path、与effect配合实现响应式、处理对象
describe('ref', () => {
test('happy path', () => {
const foo = ref(1)
expect(foo.value).toBe(1)
})
test('ref make value reactive', () => {
const foo = ref(1)
let dummy
let fn = jest.fn(() => {
dummy = foo.value
})
effect(fn)
expect(dummy).toBe(1)
expect(fn).toBeCalledTimes(1)
foo.value = 2
expect(dummy).toBe(2)
expect(fn).toBeCalledTimes(2)
//赋旧值不触发依赖
foo.value = 2
expect(dummy).toBe(2)
expect(fn).toBeCalledTimes(2)
})
test('make nested properties reactive', () => {
const foo = ref({ bar: 1 })
let dummy
effect(() => {
dummy = foo.value.bar
})
foo.value.bar++
expect(dummy).toBe(2)
})
});
首先实现happy path,实现ref函数返回一个具有value值的对象,我们可以定义一个类RefImpl(ref接口)来实现ref的各个逻辑。返回一个实例,实例上具有value的get和set方法:
class RefImpl {
private _value
constructor(value) {
this._value = value
}
get value() {
return this._value
}
set value(newValue) {
this._value = newValue
}
}
export function ref(value) {
return new RefImpl(value)
}
接下来实现ref的响应式功能,这部分和reactive的实现类似,被观察主体Subject是RefImpl实例对象,上面应该有一个dep属性用来记录所有的依赖,以及在value被get时收集依赖、被set时触发依赖。并且收集依赖和促发依赖的逻辑在track和trigger中已经实现,将其抽离成两个函数trackEffect和triggerEffect:
//effect.ts
export function trackEffect(dep) {
//当前如果没有活动对象
if (!activeEffect) return
//将现在的effect对象放入deps
dep.add(activeEffect)
//反向记录,每个Effect对象都会记录自己所在的deps
activeEffect.deps.add(dep)
}
export function triggerEffect(dep) {
//通知依赖
for (let effect of dep) {
if (effect.scheduler) effect.scheduler()
else effect.run()
}
}
别忘了在set时检查新旧值是否不同,完善后的ref部分代码如下:
import { trackEffect, triggerEffect } from "./reactivity/effect"
class RefImpl {
private _value
private dep: Set<any>
constructor(value) {
this._value = value
this.dep = new Set
}
get value() {
//收集依赖
trackEffect(this.dep)
return this._value
}
set value(newValue) {
//判断是新值还是旧值
if (Object.is(newValue, this._value)) return
this._value = newValue
//触发依赖
triggerEffect(this.dep)
}
}
export function ref(value) {
return new RefImpl(value)
}
最后ref在处理对象时会调用reactive方法,只需要在构造RefImpl实例时判断即可。但是由于引入了reactive代理对象,所以在value为对象的情况下,新旧value比较时,旧的value已经变成Proxy对象,因此直接比较普通对象和代理对象是不合理的。我们的处理如下:在RefImpl实例上添加rawValue属性,记录旧对象的源对象。
import { trackEffect, triggerEffect } from "./reactivity/effect"
import { reactive } from "./reactivity/reactive"
import { isObject } from "./reactivity/shared/isObject"
class RefImpl {
private _value
private dep: Set<any>
private rawValue
constructor(value) {
//判断是普通值还是对象
this._value = isObject(value) ? reactive(value) : value
this.rawValue = value
this.dep = new Set
}
get value() {
//收集依赖
trackEffect(this.dep)
return this._value
}
set value(newValue) {
//判断是新值还是旧值
if (Object.is(newValue, this.rawValue)) return
this._value = newValue
//触发依赖
triggerEffect(this.dep)
}
}
export function ref(value) {
return new RefImpl(value)
}
到这里所有的单元测试都是可以通过的,我们来对这部分代码做一些重构工作。可以看到构造函数和set中根据传入值来修改value值的逻辑是复用的,将这部分抽离成createRefValue函数。以及将判断新旧值是否相同抽离成更有语义化的表达hasChanged函数,并将其放入shared文件夹中供之后使用。
//effect.ts
function createRefValue(ref, value) {
//判断是普通值还是对象
ref._value = isObject(value) ? reactive(value) : value
}
//shared/hasChanged.ts
export const hasChanged = function (A, B) {
return Object.is(A, B)
}
重构完成之后别忘了重新测试一下噢(●'◡'●)
一些思考:到这里大家就发现,对于普通值,如果将ref解构赋予另外的变量,则该变量不会是响应式的(没有通过实例属性访问时不会触发依赖),而如果value值是对象,解构出的value依然具有响应性(此时是一个reactive对象)。这也是我们在开发时经常遇到的情况。
test('deconstruct', () => {
const foo = ref({ bar: 1 })
const bee = ref(1)
let dummy
const fn = jest.fn(() => {
dummy = foo.value.bar
dummy = bee.value + 1
})
effect(fn)
expect(fn).toBeCalledTimes(1)
let a = foo.value
let b = bee.value
a.bar = 2
expect(fn).toBeCalledTimes(2)
b = 3
expect(fn).toBeCalledTimes(2)
})
实现isRef和unRef
实现isRef只需要在RefImpl中定义一个_v_isRef属性即可。而unRef则是 isRef(obj)?:obj.value:obj的语法糖实现,用来对ref对象解包,测试和代码实现如下:
//ref.spec.ts
test('isRef and unRef', () => {
const foo = ref(1)
const bar = reactive({ a: 1 })
expect(isRef(foo)).toBe(true)
expect(isRef(bar)).toBe(false)
expect(unRef(foo)).toBe(1)
})
//ref.ts
class RefImpl {
//...
readonly _v_isRef = true
//...
}
export function isRef(obj) {
return Boolean(obj._v_isRef)
}
export function unRef(obj) {
return isRef(obj) ? obj.value : obj
}
实现proxyRefs
proxyRefs用来对含有ref对象的对象做解包处理,达到不用.value就能读取ref对象值的效果。使用场景主要是在template中使用setup返回的ref对象。单元测试如下:
test('proxyRefs', () => {
const foo = { name: 'foo', age: ref(18) }
const dummy = proxyRefs(foo)
expect(foo.age.value).toBe(18)
expect(dummy.age).toBe(18)
dummy.age = 19
expect(dummy.age).toBe(19)
expect(foo.age.value).toBe(19)
dummy.age = ref(100)
expect(dummy.age).toBe(100)
expect(foo.age.value).toBe(100)
})
proxyRefs是对一个对象中含有的ref对象进行浅层解包,使用proxy对该对象进行代理,在get时对对象属性进行unRef解包,在set时对ref对象的value值修改或者对整个属性进行替换。将该handlers抽离为一个独立的handlers
export function proxyRefs(objWithRefs) {
return new Proxy(objWithRefs, objWithRefsHandlers)
}
export const objWithRefsHandlers = {
get(target, key) {
return unRef(Reflect.get(target, key))
},
set(target, key, value) {
return isRef(target[key]) && !isRef(value)
? target[key].value = value
: Reflect.set(target, key, value)
}
}
实现computed
computed也是响应式的一个核心功能,先将它的功能测试点列出:
- computed默认接受一个回调函数getter,返回封装了该函数返回值的对象,类似于ref对象,用.value可以调用该返回值。
- computed具有缓存的效果,多次读取computed对象的value会读取缓存值,而不是重复执行getter的所有行为。
- computed的行为是lazily地,只有在读取computed对象的value时才会执行getter。
测试代码如下,copy了官方的一部分测试代码。
describe('computed', () => {
it('should return updated value', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
})
it('should compute lazily', () => {
const value = reactive({})
const getter = jest.fn(() => value.foo)
const cValue = computed(getter)
// lazy
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(undefined)
expect(getter).toHaveBeenCalledTimes(1)
// should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// should not compute until needed
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)
// now it should compute
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(2)
// should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
});
computed函数的实现如下:向外暴露computed函数,返回一个ComputedRefImpl实例,在类中定义获取value的get方法。为了实现缓存的效果,添加一个状态变量dirty(为false代表没有被污染,需要执行getter来污染)来判断当前是否直接读取缓存的value。
同时,也需要在合适的时候(getter中的响应式对象触发依赖时)改变这个状态变量。因此我们需要effect的介入,但是由于直接使用effect函数会初始化一次以及后续需要在触发依赖时控制变量dirty(传入的getter无法做到,需要自定义一个scheduler),这是我们不需要的,因此直接用更底层的ReactiveEffect对象以及scheduler来实现。
import { ReactiveEffect } from './effect'
import { extend } from './shared/extend'
class ComputedRefImpl {
private _value
private dirty = false
private effect: ReactiveEffect
constructor(getter) {
this.effect = new ReactiveEffect(getter)
const scheduler = () => {
this.dirty = false
}
extend(this.effect, { scheduler })
}
get value() {
if (!this.dirty) {
this._value = this.effect.run()
this.dirty = true
}
return this._value
}
}
export function computed(getter) {
return new ComputedRefImpl(getter)
}
computed部分的代码量不多,但是涉及到的知识点还是比较多,这部分值得多多思考。