@mini-jackz-vue/reactivity
v2.2.1
Published
```javascript 1.选用TS+jest(测试框架) 2.测试用例驱动功能代码实现 ```
Downloads
5
Readme
🚀 Welcome to 响应系统的源码简版 -ZSM reactivity# 1.项目结构介绍
1.选用TS+jest(测试框架)
2.测试用例驱动功能代码实现
前置知识点 ts (接口 类型 枚举 泛型 谓词签名(is) 断◊言签名(as)等)
vue核心三件 - 响应系统 渲染 编译
关于WeakMap以及js位运算
2.实现 reactivity
实现最基础的reactive
查看 Vue3 API 文档中的响应性 API 部分,找到reactive
的介绍
reactive返回对象的响应式副本
const obj = reactive({ count: 0 })
响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的 proxy 是不等于原始对象的。建议只使用响应式 proxy,避免依赖原始对象。
类型声明
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
在实现reactive
之前,首先在src/reactivity/__tests__
目录下创建reactive
的测试文件reactive.spec.ts
,并添加以下测试代码:
describe('reactivity/reactive', () => {
it('Object', () => {
const original = { foo: 1 }
// reactive 返回对象的响应式副本
const observed = reactive(original)
// observed !== original
expect(observed).not.toBe(original)
// observed 的 property 的值与 original 的相等
expect(observed.foo).toBe(1)
})
})
为了通过以上测试,在src/reactivity/src
目录下创建reactive.ts
文件,在其中实现并导出reactive
:
export function reactive(raw) {
// 返回 Proxy 的实例
return new Proxy(raw, {
// 对原始对象的 property 的 get 和 set 进行代理
get(target, key) {
// TODO: 收集依赖
return Reflect.get(target, key)
},
set(target, key, value) {
// TODO: 触发依赖
return Reflect.set(target, key, value)
}
})
}
实现最基础的effect
effect
接受一个函数作为参数,在程序运行时会执行该函数。若该函数中使用了响应式对象的 property,当该 property 的值更新时,会再次执行该函数
在实现effect
之前,首先在src/reactivity/__tests__
目录下创建effect
的测试文件effect.spec.ts
,并添加以下测试代码:
describe('effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
// 创建 mock 函数
const fnSpy = jest.fn(() => {})
effect(fnSpy)
// 当程序执行时,传入的函数会被执行
expect(fnSpy).toHaveBeenCalledTimes(1)
})
it('should observe basic properties', () => {
let dummy
// 创建响应式对象
const counter = reactive({ num: 0 })
// 在传入的函数中使用了响应式对象的 property
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
// 当该 property 的值更新时,会再次执行该函数
counter.num = 7
expect(dummy).toBe(7)
})
})
为了通过以上测试,在src/reactivity/src
目录下创建effect.ts
文件,在其中实现一个不完全的effect
并导出,在实现过程中抽离出一个ReactiveEffect
类,对相关操作进行封装:
// 抽离出一个 ReactiveEffect 类,对相关操作进行封装
class ReactiveEffect {
private _fn: any
constructor(fn) {
// 将传入的函数赋值给实例的私有 property _fn
this._fn = fn
}
// 执行传入的函数
run() {
this._fn()
}
}
// 接受一个函数作为参数
export function effect(fn) {
// 利用传入的函数创建 ReactiveEffect 类的实例
const _effect: ReactiveEffect = new ReactiveEffect(fn)
// 调用 ReactiveEffect 实例的 run 方法,执行传入的函数
_effect.run()
}
这样就实现了一个不完全的effect
,即能够在程序运行时执行传入的函数。之后,在reactive
返回的 Proxy 的实例的 get 中收集依赖,在 set 中触发依赖
export function reactive(raw) {
// 返回 Proxy 的实例
return new Proxy(raw, {
// 对原始对象的 get 进行代理
get(target, key) {
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
return res
},
// 对原始对象的 set 进行代理
set(target, key, value) {
const res = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key)
return res
}
})
}
在effect.ts
文件中实现并导出track
和trigger
函数,在实现过程中使用了一个全局的WeakMap
类型的变量targetsMap
,用于保存程序运行中的所有依赖,以及一个全局的变量activeEffect
,用于保存正在执行的ReactiveEffect
类的实例
class ReactiveEffect {
/* 其他代码 */
run() {
// 调用 run 方法时,用全局变量 activeEffect 保存当前实例
activeEffect = this
this._fn()
}
}
/**
* 用于保存程序运行中的所有依赖
* key 为响应式对象
* value 为 Map 的实例,用于保存该响应式对象的所有依赖
*/
const targetsMap = new WeakMap()
// 用于保存正在执行的 ReactiveEffect 类的实例
let activeEffect: ReactiveEffect
// 用于收集依赖
export function track(target, key) {
// 获取当前响应式对象对应的 Map 实例,若为 undefined 则进行初始化并保存到 targetsMap 中
/**
* 用于保存当前响应式对象的所有依赖
* key 为响应式对象的 property
* value 为 Set 的实例,用于保存与该 property 相关的 ReactiveEffect 类的实例
*/
let depsMap: Map<any, Set<ReactiveEffect>> | undefined =
targetsMap.get(target)
if (!depsMap) {
depsMap = new Map<any, Set<ReactiveEffect>>()
targetsMap.set(target, depsMap)
}
// 获取当前 property 对应的 Set 实例,若为 undefined 则进行初始化并保存到 depsMap 中
/**
* 用于保存与当前 property 相关的函数
* value 为与该 property 相关的 ReactiveEffect 类的实例
*/
let dep: Set<ReactiveEffect> | undefined = depsMap.get(key)
if (!dep) {
dep = new Set<ReactiveEffect>()
depsMap.set(key, dep)
}
// 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回
if (dep.has(activeEffect!)) {
return
}
// 将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中
dep.add(activeEffect)
}
// 用于触发依赖
export function trigger(target, key) {
// 获取当前响应式对象对应的 Map 实例
const depsMap: Map<any, Set<ReactiveEffect>> = targetsMap.get(target)
// 获取当前 property 对应的 Set 实例
const dep: Set<ReactiveEffect> = depsMap.get(key)!
// 遍历 dep,调用每一个 ReactiveEffect 类的实例的 run 方法
for (const reactiveEffect of dep) {
reactiveEffect.run()
}
}
完善effect
——返回runner
effect
执行会返回一个函数,用一个变量runner
接受该函数,调用runner
时会再次执行传入的函数,同时返回该函数的返回值。
在effect
的测试文件effect.spec.ts
中添加以下测试代码:
describe('effect', () => {
/* 其他测试代码 */
it('should return a function to be called manually', () => {
let foo = 0
// 用一个变量 runner 接受 effect 执行返回的函数
const runner = effect(() => {
foo++
return 'foo'
})
expect(foo).toBe(1)
// 调用 runner 时会再次执行传入的函数
const res = runner()
expect(foo).toBe(2)
// runner 执行返回该函数的返回值
expect(res).toBe('foo')
})
})
为了通过以上测试,需要对effect
的实现进行完善。首先,effect
执行返回_effect.run
,并将其this
指向指定为_effect
,其次run
方法执行返回传入的函数执行的结果:
class ReactiveEffect {
/* 其他代码 */
run() {
activeEffect = this
// 返回传入的函数执行的结果
return this._fn()
}
}
export function effect(fn) {
const _effect: ReactiveEffect = new ReactiveEffect(fn)
_effect.run()
// 返回 _effect.run,并将其 this 指向指定为 _effect
return _effect.run.bind(_effect)
}
完善effect
——接受scheduler
effect
接受一个对象作为第二个参数,该对象中可以包括一个scheduler
方法。用一个变量runner
接受effect
执行返回的函数,程序运行时会首先执行传入的函数,而不会调用scheduler
方法,之后当传入的函数依赖的响应式对象的 property 的值更新时,会调用scheduler
方法而不会执行该函数,只有当调用runner
时才会执行该函数。
在effect
的测试文件effect.spec.ts
中添加以下测试代码:
describe('effect', () => {
/* 其他测试代码 */
it('scheduler', () => {
let dummy
let run: number
// 创建 mock 函数
const scheduler = jest.fn(() => {
run++
})
const obj = reactive({ foo: 1 })
const runner = effect(
() => {
dummy = obj.foo
},
{ scheduler }
)
// 程序运行时会首先执行传入的函数,而不会调用 scheduler 方法
expect(scheduler).not.toHaveBeenCalled()
expect(dummy).toBe(1)
// 当传入的函数依赖的响应式对象的 property 的值更新时,会调用 scheduler 方法而不会执行传入的函数
obj.foo++
expect(scheduler).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
// 只有当调用 runner 时才会执行传入的函数
runner()
expect(scheduler).toHaveBeenCalledTimes(1)
expect(dummy).toBe(2)
})
})
为了通过以上测试,需要对effect
的实现和trigger
函数进行完善。
class ReactiveEffect {
/* 其他代码 */
// 构造函数接受可选的第二个参数,保存为实例的公共变量 scheduler
constructor(fn, public scheduler?) {
this._fn = fn
}
}
// 接受一个函数作为第一个参数,接受一个对象作为第二个参数
export function effect(fn, options: any = {}) {
// 利用传入的函数创建 ReactiveEffect 类的实例,并将 scheduler 方法传给 ReactiveEffect 类的构造函数
const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler)
/* 其他代码 */
}
export function trigger(target, key) {
/* 其他代码 */
/**
* 遍历 dep,判断每一个 ReactiveEffect 类的实例的 scheduler property 是否存在
* 若不为 undefined 则调用 scheduler 方法,否则调用 run 方法
*/
for (const reactiveEffect of dep) {
if (reactiveEffect.scheduler) {
reactiveEffect.scheduler()
} else {
reactiveEffect.run()
}
}
}
完善effect
——stop
stop
接受effect
执行返回的函数作为参数。用一个变量runner
接受effect
执行返回的函数,调用stop
并传入runner
后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数,只有当调用runner
时才会恢复执行该函数。
在effect
的测试文件effect.spec.ts
中添加以下测试代码:
describe('effect', () => {
/* 其他测试代码 */
it('stop', () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
obj.prop = 2;
expect(dummy).toBe(2);
// 调用 stop 后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数
stop(runner);
obj.prop = 3;
expect(dummy).toBe(2);
obj.prop++;
expect(dummy).toBe(4);
// 只有当调用`runner`时才会恢复执行该函数
runner();
expect(dummy).toBe(4);
});
})
为了通过以上测试,需要对effect
的实现进行完善,实现并导出stop
:
// 用于记录是否应该收集依赖,防止调用 stop 后触发响应式对象的 property 的 get 时收集依赖
let shouldTrack: boolean = false
class ReactiveEffect {
/* 其他代码 */
// 用于保存与当前实例相关的响应式对象的 property 对应的 Set 实例
deps: Array<Set<ReactiveEffect>> = []
// 用于记录当前实例状态,为 true 时未调用 stop 方法,否则已调用,防止重复调用 stop 方法
active: boolean = true
// 用于执行传入的函数
run() {
// 若已调用 stop 方法则直接返回传入的函数执行的结果
if (!this.active) {
return this._fn()
}
// 应该收集依赖
shouldTrack = true
// 调用 run 方法时,用全局变量 activeEffect 保存当前实例
activeEffect = this
const res = this._fn()
// 重置
shouldTrack = false
// 返回传入的函数执行的结果
return res
}
// 用于停止传入的函数的执行
stop() {
if (this.active) {
cleanupEffect(this)
this.active = false
}
}
}
// 用于将传入的 ReactiveEffect 类的实例从与该实例相关的响应式对象的 property 对应的 Set 实例中删除
function cleanupEffect(effect: ReactiveEffect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
}
export function effect(fn, options: any = {}) {
/* 其他代码 */
// 用一个变量 runner 保存将 _effect.run 的 this 指向指定为 _effect 的结果
const runner: any = _effect.run.bind(_effect)
// 将 _effect 赋值给 runner 的 effect property
runner.effect = _effect
// 返回 runner
return runner
}
export function track(target, key) {
// 若不应该收集依赖则直接返回
if (!shouldTrack || activeEffect === undefined) {
return
}
/* 其他代码 */
dep.add(activeEffect!)
// 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
activeEffect?.deps.push(dep)
}
// 用于停止传入的函数的执行
export function stop(runner) {
// 调用 runner 的 effect property 的 stop 方法
runner.effect.stop()
}
完善effect
——接受onStop
effect
接受一个对象作为第二个参数,该对象中还可以包括一个onStop
方法。用一个变量runner
接受effect
执行返回的函数,调用stop
并传入runner
时,会执行onStop
方法。
在effect
的测试文件effect.spec.ts
中添加以下测试代码
describe('effect', () => {
/* 其他测试代码 */
it('events: onStop', () => {
// 创建 mock 函数
const onStop = jest.fn()
const runner = effect(() => {}, {
onStop
})
// 调用 stop 时,会执行 onStop 方法
stop(runner)
expect(onStop).toHaveBeenCalled()
})
})
为了通过以上测试,需要对effect
的实现进行完善。
class ReactiveEffect {
/* 其他代码 */
// 用于保存当前实例的 onStop 方法
onStop?: () => void
stop() {
if (this.active) {
cleanupEffect(this)
// 在调用 stop 方法时,调用 onStop 方法
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
export function effect(fn, options: any = {}) {
const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler)
// 将 onStop 方法挂载到 ReactiveEffect 类的实例上
_effect.onStop = options.onStop
/* 其他代码 */
}
effect
接受一个对象作为第二个参数,该对象中可以包括多个属性和方法,在effect
的实现中若依次挂载到ReactiveEffect
类的实例上将会十分繁琐,因此可以使用Object.assign
方法,同时为了提高代码的可读性,可以为其设置别名。在src/shared
目录下创建index.ts
文件,并添加以下代码:
// 为 Object.assign 方法创建别名
export const extend = Object.assign
利用Object.assign
方法对之前的实现做简单优化:
增加lazy懒属性不立即调用副作用函数:
export function effect(fn, options: any = {}) {
const _effect: ReactiveEffect = new ReactiveEffect(fn)
// 将第二个参数即 options 对象的属性和方法挂载到 ReactiveEffect 类的实例上
extend(_effect, options)
if (!options || !options.lazy) {
_effect.run();
}
/* 其他代码 */
}
实现最基础的readonly
查看 Vue3 API 文档中的响应性 API 部分,找到readonly
的介绍:
接受一个对象(响应式或纯对象)或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。
onst original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用于响应性追踪
console.log(copy.count)
})
// 变更 original 会触发依赖于副本的侦听器
original.count++
// 变更副本将失败并导致警告
copy.count++ // 警告!
在实现readonly
之前,首先在src/reactivity/__tests__
目录下创建readonly
的测试文件readonly.spec.ts
,并添加以下测试代码:
describe('reactivity/readonly', () => {
it('should make values readonly', () => {
const original = { foo: 1 }
// 创建 readonly 响应式对象
const wrapped = readonly(original)
console.warn = jest.fn()
// readonly 响应式对象与原始对象不相等
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
// readonly 响应式对象的 property 是只读的
wrapped.foo = 2
expect(wrapped.foo).toBe(1)
// 修改 readonly 响应式对象的 property 的值时会调用 console.warn 发出警告
expect(console.warn).toBeCalled()
})
})
为了通过以上测试,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出readonly
:
export function readonly(raw) {
// 返回 Proxy 的实例
return new Proxy(raw, {
// 对原始对象的 get 进行代理
get(target, key) {
const res = Reflect.get(target, key)
return res
},
// 对原始对象的 set 进行代理
set() {
// TODO: 警告!
return true
}
})
}
reactive
和readonly
的实现中有较多重复,需要对其中的代码进行优化,抽离重复代码,提高可读性。在src/reactivity/src
目录下创建baseHandlers.ts
文件,将与创建用于构造 Proxy 的 handlers 相关的代码抽离到其中,并抽离出工具函数和使用全局变量进行缓存:
// 对 get 和 set 进行缓存,防止重复调用工具函数
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
// 用于生成 get 函数的工具函数
function createGetter(isReadonly = false) {
return function (target, key) {
const res = Reflect.get(target, key)
// 利用 reactive 进行响应式转换时才进行依赖收集
if (!isReadonly) {
// 收集依赖
track(target, key)
}
return res
}
}
// 用于生成 set 函数的工具函数
function createSetter() {
return function (target, key, value) {
const res = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key)
return res
}
}
// reactive 对应的 handlers
export const mutableHandlers = {
get,
set
}
// readonly 对应的 handlers
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// 调用 console.warn 发出警告
console.warn(
`Set operation on key "${key}" failed: target is readonly.`,
target
)
return true
}
// reactive.ts 之后对reactive和readonly的实现进行优化,抽离出工具函数:
export function reactive(raw) {
return createReactiveObject(raw, mutableHandlers)
}
export function readonly(raw) {
return createReactiveObject(raw, readonlyHandlers)
}
// 用于创建 Proxy 实例的工具函数
function createReactiveObject(raw, baseHandlers) {
// 返回 Proxy 的实例
return new Proxy(raw, baseHandlers)
}
实现isReactive
、isReadonly
和isProxy
查看 Vue3 API 文档中的响应性 API 部分,找到isProxy
、isReactive
和isReadonly
的介绍:
isProxy检查对象是否是由reactive
或readonly
创建的 proxy。
isReactive检查对象是否是由reactive
创建的响应式代理。
mport { reactive, isReactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
console.log(isReactive(state)) // -> true
}
}
如果该代理是readonly
创建的,但包裹了由reactive
创建的另一个代理,它也会返回true
。
import { reactive, isReactive, readonly } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
// 从普通对象创建的只读 proxy
const plain = readonly({
name: 'Mary'
})
console.log(isReactive(plain)) // -> false
// 从响应式 proxy 创建的只读 proxy
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
}
}
isReadonly检查对象是否是由readonly
创建的只读代理。
① 实现isReactive
在实现isReactive
之前,首先在reactive
的测试文件reactive.spec.ts
中增加关于isReactive
的测试代码
describe('reactivity/reactive', () => {
it('Object', () => {
const original = { foo: 1 }
const observed = reactive(original)
expect(observed).not.toBe(original)
// 对响应式对象调用 isReactive 返回 true
expect(isReactive(observed)).toBe(true)
// 对普通对象调用 isReactive 返回 false
expect(isReactive(original)).toBe(false)
expect(observed.foo).toBe(1)
})
})
为了通过以上测试,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出isReactive
:
// 用于检查对象是否是由 reactive 创建的响应式对象
export function isReactive(value): boolean {
// 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive
return !!value['__v_isReactive']
}
同时,还需要对src/reactivity/src
目录下的baseHandlers.ts
文件中的createGetter
工具函数做相应修改:
function createGetter(isReadonly = false) {
return function (target, key) {
// 当 property 名为 __v_isReactive 时,表明正在调用 isReactive,直接返回 !isReadonly
if (key === '__v_isReactive') {
return !isReadonly
}
/* 其他代码 */
}
}
② 实现isReadonly
在实现isReadonly
之前,首先在readonly
的测试文件readonly.spec.ts
中增加关于isReadonly
的测试代码
describe('reactivity/readonly', () => {
it('should make values readonly', () => {
const original = { foo: 1 }
const wrapped = readonly(original)
console.warn = jest.fn()
expect(wrapped).not.toBe(original)
// 对 readonly 响应式对象调用 isReactive 返回 false
expect(isReactive(wrapped)).toBe(false)
// 对 readonly 响应式对象调用 isReadonly 返回 true
expect(isReadonly(wrapped)).toBe(true)
// 对普通对象调用 isReactive 返回 false
expect(isReactive(original)).toBe(false)
// 对普通对象调用 isReadonly 返回 false
expect(isReadonly(original)).toBe(false)
expect(wrapped.foo).toBe(1)
wrapped.foo = 2
expect(wrapped.foo).toBe(1)
expect(console.warn).toBeCalled()
})
})
为了通过以上测试,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出isReadonly
:
// 用于检查对象是否是由 readonly 创建的 readonly 响应式对象
export function isReadonly(value): boolean {
// 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive
return !!value['__v_isReadonly']
}
同时,还需要对src/reactivity/src
目录下的baseHandlers.ts
文件中的createGetter
工具函数做相应修改:
function createGetter(isReadonly = false) {
return function (target, key) {
// 当 property 名为 __v_isReactive 时,表明正在调用 isReactive,直接返回 !isReadonly
if (key === '__v_isReactive') {
return !isReadonly
}
// 当 property 名为 __v_isReadonly 时,表明正在调用 isReadonly,直接返回 isReadonly
else if (key === '__v_isReadonly') {
return isReadonly
}
/* 其他代码 */
}
}
③ 实现isProxy
在实现isProxy
之前,首先分别在reactive
的测试文件reactive.spec.ts
和readonly
的测试文件readonly.spec.ts
中增加关于isProxy
的测试代码:
// reactive.spec.ts
describe('reactivity/reactive', () => {
it('Object', () => {
const original = { foo: 1 }
const observed = reactive(original)
expect(observed).not.toBe(original)
expect(isReactive(observed)).toBe(true)
expect(isReactive(original)).toBe(false)
// 对响应式对象调用 isProxy 返回 true
expect(isProxy(observed)).toBe(true)
// 对普通对象调用 isProxy 返回 false
expect(isProxy(original)).toBe(false)
expect(observed.foo).toBe(1)
})
})
// readonly.spec.ts
describe('reactivity/readonly', () => {
it('should make values readonly', () => {
const original = { foo: 1 }
const wrapped = readonly(original)
console.warn = jest.fn()
expect(wrapped).not.toBe(original)
expect(isReactive(wrapped)).toBe(false)
expect(isReadonly(wrapped)).toBe(true)
expect(isReactive(original)).toBe(false)
expect(isReadonly(original)).toBe(false)
// 对 readonly 响应式对象调用 isProxy 返回 true
expect(isProxy(wrapped)).toBe(true)
// 对普通对象调用 isProxy 返回 false
expect(isProxy(original)).toBe(false)
expect(wrapped.foo).toBe(1)
wrapped.foo = 2
expect(wrapped.foo).toBe(1)
expect(console.warn).toBeCalled()
})
})
为了通过以上测试,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出isProxy
:
// 用于检查对象是否是由 reactive 或 readonly 创建的响应式对象
export function isProxy(value): boolean {
// 利用 isReactive 和 isReadonly 进行判断
return isReactive(value) || isReadonly(value)
}
④ 优化代码
isReactive
和isReadonly
的实现中使用到的特殊 property 的名为字符串,需要对其进行优化,创建并导出枚举类型ReactiveFlags
用于保存这两个字符串:
//reactive.ts
// 用于保存 isReactive 和 isReadonly 中使用的特殊 property 的名
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
// baseHandlers.ts
function createGetter(isReadonly = false) {
return function (target, key) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
/* 其他代码 */
}
}
// reactive.ts
export function isReactive(value): boolean {
return !!value[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value): boolean {
return !!value[ReactiveFlags.IS_READONLY]
}
完善reactive
和readonly
——响应式转换嵌套对象
reactive
和readonly
的响应式转换是“深层”的,会影响所有嵌套的 property,即嵌套的 property 也应该是响应式的。
分别在reactive
的测试文件reactive.spec.ts
和readonly
的测试文件readonly.spec.ts
中添加以下测试代码:
// reactive.spec.ts
describe('reactivity/reactive', () => {
it('nested reactives', () => {
const original = { foo: { bar: 1 } }
const observed = reactive(original)
// 嵌套对象是响应式的
expect(isReactive(observed.foo)).toBe(true)
})
})
// readonly.spec.ts
describe('reactivity/readonly', () => {
it('should make nested values readonly', () => {
const original = { foo: { bar: 1 } }
const wrapped = readonly(original)
// 嵌套对象是响应式的
expect(isReadonly(wrapped.foo)).toBe(true)
})
})
为了通过以上测试,需要对reactive
和readonly
的实现进行完善,对src/reactivity/src
目录下的baseHandlers.ts
文件中的createGetter
工具函数做如下修改
function createGetter(isReadonly = false) {
return function (target, key) {
/* 其他代码 */
const res = Reflect.get(target, key)
if (!isReadonly) {
track(target, key)
}
// 若 property 的值为对象,则利用 reactive 和 readonly 进行响应式转换
if (typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
由于可能会多次使用到,因此可以将判断一个变量是否为对象抽离成一个isObject
函数。在src/shared
目录下的index.ts
文件中添加以下代码:
// 用于判断一个变量是否为对象
export const isObject = value => typeof value === 'object' && value !== null
之后利用isObject
函数完善createGetter
工具函数:
function createGetter(isReadonly = false) {
return function (target, key) {
/* 其他代码 */
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
/* 其他代码 */
}
}
实现shallowReactive
和shallowReadonly
查看 Vue3 API 文档中的响应性 API 部分,找到shallowReactive
和shallowReadonly
的介绍:
shallowReactive创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换(暴露原始值)
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
与reactive
不同,任何使用ref
的 property 都不会被代理自动解包。
shallowReadonly 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(暴露原始值)。
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用
与readonly
不同,任何使用ref
的 property 都不会被代理自动解包
在实现shallowReactive
和shallowReadonly
之前,首先在src/reactivity/__tests__
目录下分别创建shallowReactive
和shallowReadonly
的测试文件shallowReactive.spec.ts
和shallowReadonly.spec.ts
,分别添加以下测试代码:
// shallowReactive.spec.ts
describe('shallowReactive', () => {
test('should not make non-reactive properties reactive', () => {
const props = shallowReactive({ n: { foo: 1 } })
expect(isReactive(props.n)).toBe(false)
})
})
// shallowReadonly.spec.ts
describe('reactivity/shallowReadonly', () => {
test('should not make non-reactive properties reactive', () => {
const props = shallowReadonly({ n: { foo: 1 } })
expect(isReactive(props.n)).toBe(false)
})
})
为了通过以上测试,同时根据之前优化代码的思路,首先对src/reactivity/src
目录下的baseHandlers.ts
文件中的createGetter
工具函数做如下修改:
function createGetter(isReadonly = false, shallow = false) {
return function (target, key) {
/* 其他代码 */
const res = Reflect.get(target, key)
// 利用 reactive 和 shallowReactive 进行响应式转换时才进行依赖收集
if (!isReadonly) {
// 收集依赖
track(target, key)
}
// 若利用 shallowReactive 和 shallowReadonly 进行响应式转换则直接返回
if (shallow) {
return res
}
/* 其他代码 */
}
}
之后,在src/reactivity/src
目录下的baseHandlers.ts
文件中分别构建shallowRreactive
和shallowReadonly
对应的 handlers,二者分别是由mutableHandlers
和readonlyHandlers
替换 get property 得到的:
// shallowRreactive 对应的 handlers 是由 mutableHandlers 替换 get property 得到的
export const shallowHandlers = extend({}, mutableHandlers, {
get: shallowGet
})
// shallowReadonly 对应的 handlers 是由 readonlyHandlers 替换 get property 得到的
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
get: shallowReadonlyGet
})
最后,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出shallowRreactive
和shallowReadonly
:
export function shallowReactive(raw) {
return createReactiveObject(raw, shallowHandlers)
}
export function shallowReadonly(raw) {
return createReactiveObject(raw, shallowReadonlyHandlers)
}
处理几个effect的特例(暂不涉及数组)
1.target对应已注册的proxy无需重新注册
// 例子
let test={num:1}
let a1=reactive(test)
let a2=reactive(test)
export const reactiveMap = new WeakMap<Target, any>();
export const readonlyMap = new WeakMap<Target, any>();
export const shallowReactiveMap = new WeakMap<Target, any>();
export const shallowReadonlyMap = new WeakMap<Target, any>();
export function reactive(target) {
// 如果目标对象是一个只读的响应数据,则直接返回目标对象
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, reactiveMap);
}
export function readonly(target) {
return createReactiveObject(target, true, readonlyHandlers, readonlyMap);
}
export function shallowReactive(target) {
return createReactiveObject(target, false, shallowHandlers, shallowReactiveMap);
}
export function shallowReadonly(target) {
return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyMap);
}
// 用于创建 Proxy 实例的工具函数
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
// 不是对象直接返回
return target;
}
// 查看当前代理对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
// proxyMap 是一个全局的缓存WeakMap
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 返回 Proxy 的实例
const proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
2.如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回实现
// 例子
let test=reactive(readonly({num:1})) // =====> readonly({num:1})
let test01=readonly(reactive({num:1})) //readonly( reaceive({num1}))
// 函数 createReactiveObject里
// 如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回
if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target;
}
//新增枚举
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
RAW = '__v_raw'
}
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean; // target 是否是响应式
[ReactiveFlags.IS_READONLY]?: boolean; // target 是否是只读
[ReactiveFlags.RAW]?: any; // 表示 proxy 对应的源数据,target 已经是 proxy 对象时会有该属性
}
// baseHandles.ts
// 用于生成 get 函数的工具函数
function createGetter(isReadonly = false, shallow = false) {
return function (target, key, receiver) {
// ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
} else if (
// 如果key是raw receiver 指向调用者,则直接返回目标对象。
// 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者
// 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target;
}
const res = Reflect.get(target, key, receiver);
// 利用 reactive 进行响应式转换时才进行依赖收集
if (!isReadonly) {
// 收集依赖
track(target, key);
}
if (shallow) {
return res;
}
// 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
3.嵌套effect,外层副作用函数失效
4.同时收集触发依赖造成死循环
5.增加deferStop 防止内部停止不了当前副作用
// 例子 嵌套effect
let test=reactive({num:1})
let test2=reactive({num2:1})
effect(()=>{
effect(()=>{
console.log(test2.num2)
})
console.log(test.num)
})
// 收集触发依赖死循环
effect(()=>{
test.num++
})
// 内部停止不了当前副作用
let runner=effect(()=>{
if(test.num>3){
stop(runner)
}
})
// 抽离出一个 ReactiveEffect 类,对相关操作进行封装
export class ReactiveEffect {
private _fn: any;
// 用于保存与当前实例相关的响应式对象的 property 对应的 Set 实例
deps: Array<Set<ReactiveEffect>> = [];
// 用于记录当前实例状态,为 true 时未调用 stop 方法,否则已调用,防止重复调用 stop 方法
active: boolean = true;
parent: ReactiveEffect | undefined = undefined;
// 防止内部调用 无法stop该副作用
private deferStop?: boolean;
constructor(fn, public scheduler?) {
// 将传入的函数赋值给实例的私有 property _fn
this._fn = fn;
}
// 执行传入的函数
run() {
// 若已调用 stop 方法则直接返回传入的函数执行的结果
if (!this.active) {
return this._fn();
}
let parent: ReactiveEffect | undefined = activeEffect;
// 防止内嵌产生导致外层依赖无法收集
let lastShouldTrack = shouldTrack;
while (parent) {
// 处理副作用函数get 和 set同时操作造成内存溢出
if (parent === this) {
return;
}
parent = parent.parent;
}
// debugger;
try {
this.parent = activeEffect;
// 应该收集依赖
shouldTrack = true;
// 调用 run 方法时,用全局变量 activeEffect 保存当前实例
activeEffect = this as any;
// 执行前清除依赖
cleanupEffect(this);
//执行副作用函数 触发收集依赖
const res = this._fn();
// 返回传入的函数执行的结果
return res;
} finally {
// 储存上一个副作用函数的实例
activeEffect = this.parent;
// 重置
shouldTrack = lastShouldTrack;
this.parent = undefined;
// 清除依赖
if (this.deferStop) {
this.stop();
}
}
}
onStop?: () => void;
// 用于停止传入的函数的执行
stop() {
// 防止内部调用 最后副作用函数完成时依赖未完全清除时 导致清除无效
if (activeEffect == this) {
this.deferStop = true;
} else if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
ref
查看 Vue3 API 文档中的响应性 API 部分,找到ref
的介绍。
ref 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。
示例:
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
如果将对象分配为 ref 值,则通过reactive
函数使该对象具有高度的响应式。
类型声明:
interface Ref<T> {
value: T
}
function ref<T>(value: T): Ref<T>
有时我们可能需要为 ref 的内部值指定复杂类型。想要简洁地做到这一点,我们可以在调用 ref 覆盖默认推断时传递一个泛型参数
const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>
foo.value = 123 // ok!
如果泛型的类型未知,建议将 ref 转换为Ref<T>
:
function useState<State extends string>(initial: State) {
const state = ref(initial) as Ref<State> // state.value -> State extends string
return state
}
① 实现最基础的ref
在实现ref
之前,首先在src/reactivity/__tests__
目录下创建ref
的测试文件ref.spec.ts
,并添加以下测试代码:
describe('reactivity/ref', () => {
it('should hold a value', () => {
// 创建 ref 对象
const a = ref(1)
// ref 对象的 value property 的值等于传入的值
expect(a.value).toBe(1)
// ref 对象的 value property 的值是可变的
a.value = 2
expect(a.value).toBe(2)
})
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)
// ref 对象是响应式的
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// ref 对象的 value property 的 set 具有缓存,不会重复触发依赖
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
})
为了通过以上测试,在src/reactivity/src
目录下创建ref.ts
文件,在其中实现一个不完全的ref
并导出,在实现过程中利用Ref
接口的实现类,对操作进行封装
// ref 对象的接口
interface Ref {
value
}
// Ref 接口的实现类,对操作进行封装
class RefImpl {
private _value
constructor(value) {
// 将传入的值赋值给实例的私有 property _value
this._value = value
}
// value property 的 get 返回私有 property _value 的值
get value() {
// TODO: 收集依赖
// 返回实例的私有 property _value 的值
return this._value
}
// value property 的 set 修改私有 property _value 的值
set value(newVal) {
// TODO: 触发依赖
// 对 set 的值进行处理,将结果赋值给实例的私有 property _value
this._value = newVal
}
}
export function ref(value?: unknown): Ref {
// 返回 RefImpl 类的实例,即 ref 对象
return new RefImpl(value);
}
这样就实现了一个不完全的ref
,即能够将传入的值转为 ref 对象。之后,从src/reactivity/src
目录下的effect.ts
文件中的track
和trigger
函数中抽离并导出isTracking
、trackEffects
和triggerEffects
函数:
export function track(target, key) {
// 若不应该收集依赖则直接返回
if (!isTracking()) {
return
}
/* 其他代码 */
trackEffects(dep)
}
// 用于判断是否应该收集依赖
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
// 用于将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中, 同时将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
export function trackEffects(dep) {
// 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回
if (dep.has(activeEffect!)) {
return;
}
// 将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中
dep.add(activeEffect!);
// 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
activeEffect!.deps.push(dep);
}
export function trigger(target, key) {
/* 其他代码 */
triggerEffects(dep)
}
// 用于遍历 dep,调用每一个 ReactiveEffect 类的实例的 scheduler 方法或 run 方法
export function triggerEffects(dep) {
for (const reactiveEffect of dep) {
if (reactiveEffect.scheduler) {
reactiveEffect.scheduler()
} else {
reactiveEffect.run()
}
}
}
之后,在RefImpl
类中创建私有 property dep
用于保存与当前 ref 对象相关的依赖,在 value property 的 get 中收集依赖,在 set 中触发依赖:
class RefImpl {
private _value
// 用于保存与当前 ref 对象相关的依赖
private dep
constructor(value) {
this._value = value
this.dep = new Set()
}
get value() {
if (isTracking()) {
// 收集依赖
trackEffects(this.dep)
}
return this._value
}
set value(newVal) {
// 若 set 的值与之前相同则直接返回
if (!hasChanged(newVal, this._value)) {
return
}
this._value = newVal
// 触发依赖
triggerEffects(this.dep)
}
}
// share.js
export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue);
② 完善ref
若传入的值是一个对象,需要利用reactive
对该对象进行响应式转换。
在ref
的测试文件ref.spec.ts
中添加以下测试代码:
describe('reactivity/ref', () => {
/* 其他测试代码 */
it('should make nested properties reactive', () => {
const a = ref({
count: 1
})
let dummy
effect(() => {
dummy = a.value.count
})
expect(dummy).toBe(1)
// ref 对象的 value property 的是一个响应式对象
a.value.count = 2
expect(dummy).toBe(2)
})
})
为了通过以上测试,需要对ref
的实现进行完善。首先,在src/reactivity/src
目录下的reactive.ts
文件中实现并导出toReactive
函数:
// 用于对值进行处理,若为对象则利用 reactive 进行响应式处理,否则直接返回
export const toReactive = value => (isObject(value) ? reactive(value) : value)
之后,在RefImpl
类中增加私有 property _rawValue
用于保存用于保存传入的值和 set 的值,并在赋值给实例的私有 property _value
之前利用toReactive
函数对值进行处理:
class RefImpl {
// 用于保存传入的值和 set 的值
private _rawValue
private _value
private dep
constructor(value) {
// 将传入的值赋值给实例的私有 property _rawValue
this._rawValue = value
// 对传入的值进行处理,将结果赋值给实例的私有 property _value
this._value = toReactive(value)
this.dep = new Set()
}
get value() {
if (isTracking()) {
trackEffects(this.dep)
}
return this._value
}
set value(newVal) {
// 若 set 的值与之前不同则修改并触发依赖
if (hasChanged(newVal, this._rawValue)) {
// 将 set 的值赋值给实例的私有 property _rawValue
this._rawValue = newVal
// 对 set 的值进行处理,将结果赋值给实例的私有 property _value
this._value = toReactive(newVal)
// 触发依赖
triggerEffects(this.dep)
}
}
}
实现isRef
和unref
查看 Vue3 API 文档中的响应性 API 部分,找到isRef
和unRef
的介绍。
isRef检查值是否为一个 ref 对象。
unref如果参数是一个 ref,则返回内部值,否则返回参数本身。这是val = isRef(val) ? val.value : val
的语法糖函数。
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x) // unwrapped 现在一定是数字类型
}
在实现isRef
和unRef
之前,首先在ref
的测试文件ref.spec.ts
中增加关于isRef
和unRef
的测试代码:
describe('reactivity/ref', () => {
it('isRef', () => {
expect(isRef(ref(1))).toBe(true)
expect(isRef(reactive({ foo: 1 }))).toBe(false)
expect(isRef(0)).toBe(false)
expect(isRef({ bar: 0 })).toBe(false)
})
it('unref', () => {
expect(unref(1)).toBe(1)
expect(unref(ref(1))).toBe(1)
})
})
为了通过以上测试,首先在RefImpl
类中增加共有 property __v_isRef
用于标志实例是一个 ref 对象,之后,在src/reactivity/src
目录下的ref.ts
文件中实现并导出isRef
和unRef
:
class RefImpl {
// 用于保存传入的值和 set 的值
private _rawValue
private _value
// 用于保存与当前 ref 对象相关的依赖
private dep
// 用于标志实例是一个 ref 对象
public __v_isRef = true
}
// 用于判断一个值是否是 ref 对象
export function isRef(value): boolean {
return !!value.__v_isRef
}
// 用于获取 ref 对象的 value property 的值
export function unref(ref) {
return isRef(ref) ? ref.value : ref
}
实现proxyRefs
函数
proxyRefs
函数接受一个对象作为参数,返回一个对该对象的 get 和 set 进行代理的 Proxy 的实例proxy
,若该对象的某个 property 的值是一个 ref 对象,则可直接通过获取proxy
的相应 property 的值获取该 ref 对象的传入的值,直接修改proxy
的相应 property 的值修改该 ref 对象的传入的值或替换该 ref 对象。
在实现proxyRefs
函数之前,首先在ref
的测试文件ref.spec.ts
中增加关于proxyRefs
函数的测试代码:
describe('reactivity/ref', () => {
it('proxyRefs', () => {
const obj = {
foo: ref(1),
bar: 'baz'
}
const proxyObj = proxyRefs(obj)
expect(proxyObj.foo).toBe(1)
expect(proxyObj.bar).toBe('baz')
proxyObj.foo = 2
expect(proxyObj.foo).toBe(2)
proxyObj.foo = ref(3)
expect(proxyObj.foo).toBe(3)
})
})
为了通过以上测试,在src/reactivity/src
目录下的ref.ts
文件中实现并导出proxyRefs
函数。
export function proxyRefs(objectWithRefs) {
// 返回 Proxy 的实例
return new Proxy(objectWithRefs, {
// 对传入的对象的 property 的 get 和 set 进行代理
get: function (target, key) {
// 获取传入的对象的 property 的值,再调用 unref 进行处理
return unref(Reflect.get(target, key))
},
set: function (target, key, value) {
const oldValue = target[key]
// 若传入的对象的 property 的值是一个 ref 对象,而 set 的值不是一个 ref 对象,则修改该 ref 对象的值,否则直接修改 property 的值
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value)
}
}
})
}
实现computed
查看 Vue3 API 文档中的响应性 API 部分,找到computed
的介绍。
computed接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
类型声明:
// 只读的
function computed<T>(
getter: () => T,
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
effect: ReactiveEffect
target: any
type: OperationTypes
key: string | symbol | undefined
}
① 实现最基础的computed
在实现computed
之前,在src/reactivity/__tests__
目录下创建computed
的测试文件computed.spec.ts
,并添加以下测试代码:
describe('reactivity/computed', () => {
it('should return updated value', () => {
const value = reactive({ foo: 1 })
// 接受一个 getter 函数创建只读响应式 ref 对象,
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(1)
value.foo = 2
expect(cValue.value).toBe(2)
})
})
为了通过以上测试,在src/reactivity/src
目录下创建computed.ts
文件,在其中实现一个最基础的computed
并导出,在实现过程中利用Ref
接口的实现类,对操作进行封装,同时利用了effect
的实现中抽离出的ReactiveEffect
类,因此需要将src/reactivity/src
目录下的effect.ts
文件中的ReactiveEffect
类导出:
// effect.ts
export class ReactiveEffect {
/* 具体实现 */
}
// computed.ts
// Ref 接口的实现类
class ComputedImpl {
// 用于保存 ReactiveEffect 类的实例
private _effect: ReactiveEffect
constructor(getter) {
// 利用 getter 函数创建 ReactiveEffect 类的实例
this._effect = new ReactiveEffect(getter)
}
// value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值
get value() {
return this._effect.run()
}
}
export function computed(getter) {
// 返回 RefImpl 类的实例,即 ref 对象
return new ComputedImpl(getter)
}
② 完善computed
computed
会懒执行 getter 函数,同时响应式 ref 对象的 value property 的 get 具有缓存。
处理穿参若为{ get(){ },set(){ } }时
在computed
的测试文件computed.spec.ts
中添加以下测试代码:
describe('reactivity/computed', () => {
it('should compute lazily', () => {
const value = reactive({ foo: 1 })
const getter = jest.fn(() => value.foo)
const cValue = computed(getter)
// 在获取 ref 对象的 value property 的值时才执行 getter
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1)
// 若依赖的响应式对象的 property 的值没有更新,则再次获取 ref 对象的 value property 的值不会重复执行 getter
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// 修改依赖的响应式对象的 property 的值时不会执行 getter
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)
// 在依赖的响应式对象的 property 的值没有更新后,获取 ref 对象的 value property 的值再次执行 getter
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(2)
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
})
为了通过以上测试,需要对computed的实现进行完善。
import { effect } from './effect';
import { isFunction } from '../shared/index';
// Ref 接口的实现类
class ComputedImpl {
private _runner;
// 用于保存 getter 函数的执行结果
private _value;
// 用于记录是否 不使用缓存
private _dirty = true;
private _setter?;
constructor(getter, setter) {
this._setter = setter;
// 利用 getter 函数和一个方法创建 ReactiveEffect 类的实例
this._runner = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
// 用于关闭缓存
this._dirty = true;
}
}
});
}
// value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值
get value() {
if (this._dirty) {
// 调用 ReactiveEffect 类的实例的 run 方法,即执行 getter 函数,将结果赋值给 _value property
this._value = this._runner.effect.run();
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}
/* 处理参数为 {
get(){},
set(newVal){
....
}
} */
export function computed(getterOrOption) {
let getter, setter;
if (isFunction(getterOrOption)) {
getter = getterOrOption;
setter = () => {
console.warn('computed is readonly');
};
} else {
getter = getterOrOption.get;
setter = getterOrOption.set;
}
// 返回 RefImpl 类的实例,即 ref 对象
return new ComputedImpl(getter, setter);
}
// ../shared/index (新增判断是否为函数)
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'