👏🏻欢迎来到 CoderStan 的手写 Mini-Vue3 专栏,和我一起✍🏻实现自己的 Mini-Vue3。这是专栏中的第二篇文章,通过这篇文章,你将能够彻底搞懂 Vue3 的 reactivity 中主要功能的实现原理。(🙏🏻感谢 阿崔cxr 的 mini-vue)
有不足的地方欢迎大家评论留下意见或建议,如果觉得还不错还请👍🏻支持一下,想看其他部分的文章可以关注我或者关注我的手写 Mini-Vue3专栏,想看逐行注释的源码欢迎访问 GitHub 仓库,也请⭐支持一下。
3. 实现 reactivity
3.1 实现最基础的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)
}
})
}
执行yarn test reactive
命令运行reactive
的测试,可以看到测试通过,这样就完成了reactive
最基础的实现。
3.2 实现最基础的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()
}
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就完成了effect
最基础的实现。
3.3 完善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)
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
3.4 完善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()
}
}
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
3.5 完善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(2)
// 只有当调用`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()
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就实现了stop
,进一步完善了effect
的实现。
3.6 完善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
/* 其他代码 */
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
effect
接受一个对象作为第二个参数,该对象中可以包括多个属性和方法,在effect
的实现中若依次挂载到ReactiveEffect
类的实例上将会十分繁琐,因此可以使用Object.assign
方法,同时为了提高代码的可读性,可以为其设置别名。在src/shared
目录下创建index.ts
文件,并添加以下代码:
// 为 Object.assign 方法创建别名
export const extend = Object.assign
利用Object.assign
方法对之前的实现做简单优化:
export function effect(fn, options: any = {}) {
const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler)
// 将第二个参数即 options 对象的属性和方法挂载到 ReactiveEffect 类的实例上
extend(_effect, options)
/* 其他代码 */
}
3.7 实现最基础的readonly
查看 Vue3 API 文档中的响应性 API 部分,找到readonly
的介绍:
readonly
接受一个对象(响应式或纯对象)或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。
const 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
}
})
}
执行yarn test readonly
命令运行readonly
的测试,可以看到测试通过,这样就完成了readonly
最基础的实现。
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
和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)
}
3.8 实现isReactive
、isReadonly
和isProxy
查看 Vue3 API 文档中的响应性 API 部分,找到isProxy
、isReactive
和isReadonly
的介绍:
isProxy
检查对象是否是由
reactive
或readonly
创建的 proxy。
isReactive
检查对象是否是由
reactive
创建的响应式代理。
import { 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
}
/* 其他代码 */
}
}
执行yarn test reactive
命令运行reactive
的测试,可以看到测试通过,这样就实现了isReactive
。
② 实现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
}
/* 其他代码 */
}
}
执行yarn test readonly
命令运行readonly
的测试,可以看到测试通过,这样就实现了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)
}
分别执行yarn test reactive
和yarn test readonly
命令运行reactive
和readonly
的测试,可以看到测试均通过,这样就实现了isProxy
。
④ 优化代码
isReactive
和isReadonly
的实现中使用到的特殊 property 的名为字符串,需要对其进行优化,创建并导出枚举类型ReactiveFlags
用于保存这两个字符串:
// baseHandlers.ts
// 用于保存 isReactive 和 isReadonly 中使用的特殊 property 的名
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
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]
}
3.9 完善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
}
}
分别执行yarn test reactive
和yarn test readonly
命令运行reactive
和readonly
的测试,可以看到测试均通过,这样就进一步完善了reactive
和readonly
的实现。
由于可能会多次使用到,因此可以将判断一个变量是否为对象抽离成一个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)
}
/* 其他代码 */
}
}
3.10 实现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)
}
分别执行yarn test shallowRreactive
和yarn test shallowReadonly
命令运行shallowRreactive
和shallowReadonly
的测试,可以看到测试均通过,这样就实现了shallowRreactive
和shallowReadonly
。
3.11 实现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): 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) {
if (dep.has(activeEffect!)) {
return
}
dep.add(activeEffect!)
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)
}
}
执行yarn test ref
命令运行ref
的测试,可以看到测试通过,这样就完成了ref
最基础的实现。
② 完善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)
}
}
}
执行yarn test ref
命令运行ref
的测试,可以看到测试通过,这样就进一步完善了ref
的实现。
3.12 实现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
}
执行yarn test ref
命令运行ref
的测试,可以看到测试通过,这样就实现了isRef
和unRef
。
3.13 实现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)
}
}
})
}
执行yarn test ref
命令运行ref
的测试,可以看到测试通过,这样就实现了proxyRefs
函数。
3.14 实现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)
}
执行yarn test computed
命令运行computed
的测试,可以看到测试通过,这样就完成了computed
最基础的实现。
② 完善computed
computed
会懒执行 getter 函数,同时响应式 ref 对象的 value property 的 get 具有缓存。
在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
的实现进行完善。
class ComputedImpl {
private _effect: ReactiveEffect
// 用于保存 getter 函数的执行结果
private _value
// 用于记录是否不使用缓存
private _dirty = true
constructor(getter) {
// 利用 getter 函数和一个方法创建 ReactiveEffect 类的实例
this._effect = new ReactiveEffect(
getter,
// 用于关闭缓存
() => {
this._dirty = true
}
)
}
// value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值
get value() {
if (this._dirty) {
// 调用 ReactiveEffect 类的实例的 run 方法,即执行 getter 函数,将结果赋值给 _value property
this._value = this._effect.run()
this._dirty = false
}
return this._value
}
}
执行yarn test computed
命令运行computed
的测试,可以看到测试通过,这样就进一步完善了computed
的实现。
总结
至此,就完成了 Mini-Vue3 的 reactivity 部分,希望这篇文章能够让你有所收获,如果觉得写的不错还请👍🏻支持一下。