- 3.1 实现 reactive
- 3.2 实现 effect
- 3.3 实现 effect —— 返回 runner
- 3.4 实现 effect —— 接收 scheduler
- 3.5 实现 effect —— stop
- 3.6 实现 effect —— onStop
- 优化 stop
- 3.7 实现 readonly
- 优化 reactive
- 3.8 实现 isReadonly & isReactive & isProxy
- 3.9 实现 readonly & reactive 嵌套对象转换
- 3.10 实现 shallowReactive & shallowReadonly
- 3.11 实现 ref
- 3.12 实现 isRef & unRef
- 3.13 实现 ref —— proxyRefs
- 3.14 实现 computed
Vue3的底层基于 响应式 - reactivity
, reactivity
是vue3的基础,是实现 mini-vue 的第一步。
在实现mini-vue 课程中教学的方式 - TDD
TDD :Test Driven Development 测试驱动开发 。
- 它要求在编写某个功能的代码之前先编写好测试代码
2. 然后只编写使测试通过的功能代码
3. 实现代码的重构 通过测试来推动整个开发的进行。
TDD 开发, 这有助于编写简洁可用和高质量的代码,并加速开发过程。
3.1 实现 reactive
, 对 reactive
的定义实现
- 返回对象响应式副本
- 基于 ES6 的
Proxy
- 返回的响应式对象 不等于 原始对象
根据定义写 测试代码 , 在src/reactivity/test/
创建 reactive.spec.ts
// reactive 的实现
describe('reactive', () => {
// 1. 实现 reactive
it('happy path', () => {
// 原始对象
let original = { foo: 1 }
// 返回对象响应式副本
let observed = reactive(original)
// 初始对象 不等于 响应式对象
expect(observed).not.toBe(original)
// 响应式对象 和 原始对象 具有相同属性和属性值
expect(observed.foo).toBe(1)
})
})
写逻辑代码让测试通过, 在src/reactivity
下创建 reactive.ts
// reactive.ts
export function reactive(raw) {
// 返回一个Proxy 实例
return new Proxy(raw, {
// 对 对象属性进行 代理 get | set
get(target, key) {
let res = Reflect.get(target, key)
// 这里执行依赖收集
return res
},
set(target, key, value) {
let res = Reflect.set(target, key, value)
// 触发依赖
return res
}
})
}
执行 pnpm test reactive
检查 测试是否全部通过
3.2 实现 effect
effect 的实现
- 接收一个 函数 fn 作为参数 , 当执行 effect 是会执行这个 fn
- 这个 fn 内部使用了响应式对象数据,当响应式数据发生变化时, effect 会再次执行 fn
写 effect 的测试代码 src/reactivity/test/effect.spec.ts
describe('effect', () => {
it('happy path', () => {
// 定义响应式对象
const user = reactive({ age: 10 })
let nextAge
// effect 接收一个fn 做参数, 初始时会调用这个fn
effect(() => {
nextAge = user.age + 1
})
// 当执行 effect 完后 nextAge 变为 11
expect(nextAge).toBe(11)
// 当更新响应式对象, effect 会再次调用 fn
user.age++
// 当执行 effect 完后 nextAge 变为 12
expect(nextAge).toBe(12)
})
})
实现 effect 的逻辑, src/reactivity/effect.ts
初始时候, 导出 effect 方法,并且使用 ReactiveEffect
这个类 作为容器,在执行 effect 时候, 创建 ReactiveEffect
实例 _effect
,把 fn
保存到 容器中。 在容器上定义run
方法 ,调用 run()相当于执行fn
// 全局变量
let activeEffect
// 定义存储依赖的容器 ReactiveEffect
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn
}
// 定义 run 方法,调用 run()相当于执行fn
run() {
this._fn()
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
// 执行它的run方法
_effect.run()
}
这样就实现了 effect 一开始就调用了 fn() , 在执行 fn()时,读取到响应式对象的 get | set 属性, 可以在调用这些属性时,在返回的 Proxy 实例中进行 依赖收集 getter
或 触发依赖 setter
// reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
let res = Reflect.get(target, key)
// 这里执行依赖收集
trick(target, key)
return res
},
set(target, key, value) {
let res = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key, value)
return res
}
})
}
在effect.ts
中定义 trick | trigger
函数 , 在实现依赖收集和触发依赖, 需要使用全局变量 activeEffect
保存当前正在执行的实例, 使用 targetMap
保存运行中的依赖
// 全局变量
let activeEffect: ReactiveEffect // 保存执行的实例
const targetMap = new Map() // 保存运行中的依赖
// 定义存储依赖的容器 ReactiveEffect
class ReactiveEffect {
/* 其他代码 */
run() {
// 在调用 run时, 赋值 activeEffect 为当前 ReactiveEffect
activeEffect = this
// 执行 fn() 就是 effect 传入过来的那个函数 ---- 依赖
this._fn()
}
}
// 依赖收集
export function trick(target, key) {
// 如果没有activeEffect这个实例,不需要进行依赖收集
if (!activeEffect) return
// 按照这个方式去设置 dep 的存储:targetMap -> depsMap -> dep -> ReactiveEffect
let depsMap = targetMap.get(target)
if (!depsMap) { // 如果没有depsMap,进行初始化
depsMap = new Map()
targetMap.set(target, depsMap) // 把depsMap 添加到 targetMap
}
// 判断是否有 dep
let dep = depsMap.get(key)
if (!dep) {
dep = new Set() // 定义 dep, 使用 Set() 收集 ReactiveEffect
depsMap.set(key, dep)
}
// 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回, 不用进行收集了
if (dep.has(activeEffect)) return
// ReactiveEffect 收集在 dep 中
dep.add(activeEffect)
}
// 触发依赖
// 执行依赖 run() 方法
export function trigger(target, key, value) {
// targetMap -> depsMap -> dep -> ReactiveEffect
// 1. 先取出 depsMap
let depsMap = targetMap.get(target)
// 2. 再取出 dep
let dep = depsMap.get(key)
// 3. 遍历 dep, 执行 触发依赖 run() 方法
for (const effect of dep) {
effect.run()
}
}
执行 pnpm test effect
运行测试,测试通过就完成 effect
的基本实现
3.3 实现 effect —— 返回 runner
- 执行
effect
会返回一个 函数, 用一个runner
的变量接收该函数 - 当调用
runner
会执行effect
内的fn
同时
fn()
会有一个返回值在
effect.spec.ts
中添加 runner 的测试// 2. 实现 runner
it('should return function when call effect', () => {
// 2.1 effect执行会返回 runner() 函数,
// 2.2 执行runner() 会执行 effect内部的 fn, runner 也就是 fn
// 2.3 执行 runner() 会有一个返回值, 这个返回值就是 fn() 内部的定义的返回值
let foo = 10
// effect 会返回出一个runner()函数
const runner = effect(() => {
foo++
return "foo"
})
// effect 初始时 fn 会调用一次
expect(foo).toBe(11)
// 调用 runner() 会执行 fn()
const run = runner()
expect(foo).toBe(12)
// runner() 返回的值是 fn() 的返回值
expect(run).toBe("foo")
})
effect.ts
实现 返回 runner
和 fn
的返回值
class ReactiveEffect {
run() {
activeEffect = this
// 设置 fn 的返回值
return this._fn()
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
// 定义 返回的 runner -> runner 就是 fn()
const runner: any = _effect.run.bind(_effect)
return runner
}
执行 pnpm test effect
运行测试,测试通过就完成 runner
的基本实现
3.4 实现 effect —— 接收 scheduler
scheduler
是一个函数,是 effect 的第二个参数scheduler
一开始不会调用,也就是 初始化 effect () 不会调用scheduler()
- 当修改响应式对象值时, 会调用
scheduler()
,而且fn()
不被执行 当执行
runner
, 会再次执行fn
在
effect.spec.ts
中添加scheduler
的测试// 3. 实现 scheduler
it('scheduler', () => {
// 1. 通过effect 的第二个参数给定了一个 scheduler 的 fn
// 2. 当 effect 第一次执行的时候, 还会执行fn
// 3. 当 响应式对象 set update 时, 不会执行 fn , 而是执行 scheduler
// 4. 如果说当执行 runner 的时候, 会再次执行 fn
let dummy
const obj = reactive({ foo: 1 })
let run
const scheduler = jest.fn(() => {
run = runner
})
// scheduler 作为 effect 的第二个参数
const runner = effect(() => {
dummy = obj.foo
}, { scheduler })
// effect 初始化时调用 fn
expect(dummy).toBe(1)
// scheduler 初始化时候没有被调用
expect(scheduler).not.toHaveBeenCalled()
// update -> set -> 调用 scheduler
obj.foo = 2
// scheduler 被调用
expect(scheduler).toHaveBeenCalledTimes(1)
// 当 runner 调用时, 才会执行 fn
run()
expect(dummy).toBe(2)
})
在 effect.ts
中实现 scheduler()
的逻辑代码
// 触发依赖
export function trigger(target, key, value) {
// 取出依赖
// targetMap -> depsMap -> dep -> ReactiveEffect
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
// 循环执行 run()
for (const effect of dep) {
// 这里判断是否有 scheduler 参数, 如果没有则执行run() 方法
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
export function effect(fn, options: any = {}) {
// options effect 的第二个参数, 它是可选的
const _effect = new ReactiveEffect(fn, options.scheduler)
/* 其他代码 */
}
执行 pnpm test effect
运行测试,测试通过就完成 scheduler
的基本实现
3.5 实现 effect —— stop
- stop() 是一个函数, 在 effect 内部实现
- stop() 接收 runner 作为参数, 当执行stop() 后,更新响应式对象值时,不会发生改变
但是 直接调用 runner() 响应式对象还是会发生变化
在
effect.spec.ts
中添加stop
的测试// 4. 实现 stop
it('stop', () => {
// 1. stop 是 effect 内部实现的函数
// 2. stop() 接收 runner 参数, 调用后 响应式对象值不会再发生改变
// 3. 但是直接调用 runner() 还是会让响应式对象发生改变
// 定义响应式
let dummy
let obj = reactive({ props: 1 })
const runner = effect(() => {
dummy = obj.props
})
// update -> set
obj.props++
expect(dummy).toBe(2)
// 调用 stop()
stop(runner)
obj.props = 3
expect(dummy).toBe(2)
// 调用 runner() 触发更新
runner()
expect(dummy).toBe(3)
})
effect.ts
实现 stop
的逻辑
// 定义存储依赖的容器 ReactiveEffect
class ReactiveEffect {
private _fn: any
// 用于保存 dep, 方便拿到正在执行的 ReactiveEffect
deps: any[] = []
// 判断 stop() 是否被调用过,如果被调用过,则不需要再次调用 设置 active = false
active: boolean = true
/* 其他代码 */
stop() {
// 实现stop, 只需要把 dep 进行清空
// 问题? 如果通过当前的 ReactiveEffect实例对象 找到 dep 对象
// 实现: 在 ReactiveEffect 定义 deps 的数组,在执行get() 把dep添加到数组中
// 使用 cleanupEffect 函数 抽离清空的逻辑
// 考虑到性能问题,用户可能会多次调用 stop 方法,所以这里需要做一个判断
// 使用 active 判断是否已经被清空
if (this.active) {
// 清空dep的逻辑
cleanupEffect(this)
// 把 active 的状态设置为 false
this.active = false
}
}
}
// 执行清空 deps -> dep
function cleanupEffect(effect) {
effect.deps.forEach(dep => dep.delete(effect))
}
export function trick(target, key) {
if (!activeEffect) return
/* 其他代码 */
// ReactiveEffect 存储在 dep 中
dep.add(activeEffect)
// 添加 dep 到 deps 这个实例的数组中
activeEffect.deps.push(dep)
}
export function effect(fn, options: any = {}) {
/* 其他代码 */
// 定义 返回的 runner -> runner 就是 fn()
const runner: any = _effect.run.bind(_effect)
// 把 effect 实例保存在 runner 上, 这样stop用到runner, 才能拿到正在执行的ReactiveEffect
runner.effect = _effect
return runner
}
export function stop(runner) {
// 在 ReactiveEffect 实习 stop 逻辑
runner.effect.stop()
}
执行 pnpm test effect
运行测试,测试通过就完成 stop
的基本实现
3.6 实现 effect —— onStop
- onStop是一个函数
- onStop 是 effect 的第二个参数
- 当执行完 stop() 后,onStop() 会被执行,也就是说 onStop 是 stop 的一个回调函数
effect.spec.ts
// 5. 实现 onStop
it('onStop', () => {
// 1. onStop() 是 effect 的第二个参数, onStop() 是一个函数
// 2. 当 stop() 执行完后, onStop() 会被执行,也就是 stop() 的回调
// 定义响应式
let dummy
let obj = reactive({ foo: 1 })
// 定义 onStop()
const onStop = jest.fn()
const runner = effect(
() => { dummy = obj.foo },
{ onStop } // onStop() 第二个参数
)
// 当调用stop()时, onStop() 会被执行
stop(runner)
expect(onStop).toHaveBeenCalledTimes(1)
})
effect.ts
class ReactiveEffect {
// 定义onStop, 用于保存 onStop 方法
onStop?: () => void
// ...
stop() {
if (this.active) {
// 清空dep的逻辑
cleanupEffect(this)
// 判断是否有 onStop 回调
if (this.onStop) {
// 执行 onStop 回调
this.onStop()
}
// 把 active 的状态设置为 false
this.active = false
}
}
}
export function effect(fn, options: any = {}) {
// options effect 的第二个参数, 它是可选的
const _effect = new ReactiveEffect(fn, options.scheduler)
// onStop 的逻辑
// 使用 Object.assign(effect, options) 挂载options
// Object.assign(_effect, options)
// 对 Object.assign() 进行封装语义化 -> 把Object.assign抽离出去,改个名字
extend(_effect, options)
// ...
}
对 Objest.assign()
方法抽离为 公共的方法 extend
src/shared/index.ts
export const extend = Object.assing
执行 pnpm test effect
运行测试,测试通过就完成 onStop
的基本实现
优化 stop
在 测试中stop 的 obj.props = 3
, 如果把obj.props++
会出现问题。
因为执行 obj.props++
= obj.props = obj.props + 1
即执行了 getter 又执行了 setter , 而在之前 stop() 时候已经清除掉了 上一个getter 收集的依赖 。
所以执行obj.props++
这里执行 gette 经历初始化,然后执行 setter 时候修改数据 相当于 : 1 + 1 ,已经收集不到依赖
解决问题:
- 只需要在它收集依赖时候 判断它是不是执行过
stop()
的操作, 如果是 就不要进行依赖的收集 , 直接执行fn
- 如果不是 stop() 执行 依赖收集 ```typescript // effect.spec.ts
// 4. 实现 stop it(‘stop’, () => { let dummy let obj = reactive({ props: 1 }) const runner = effect(() => { dummy = obj.props }) obj.props = 2 expect(dummy).toBe(2) // 调用 stop() stop(runner) // obj.props = 3 obj.props++ // 修改时 expect(dummy).toBe(2) // 调用 runner() 触发更新 runner() expect(dummy).toBe(3) // bug : 这里 dummy 为 2 })
优化后 effect<br />逻辑:
- 使用 shouldTrack 判断是否进行依赖收集 , 在 run() 的时候进行给 shouldTrack 赋值状态
- 然后在 `trick()` 执行依赖收集时候, 判断 shouldTrack 的状态 就行
```typescript
// shouldTrack 判断是否执行依赖收集, 在 run() 时候进行调用
let shouldTrack
class ReactiveEffect {
run() {
// 因为执行 run() 方法会依赖收集,
// 执行 stop() 不需要进行依赖的收集, 可以直接在run 方法这里判断, 做操作, active 判断 stop() 是否被调用
if (!this.active) {
// 返回 fn
return this._fn()
}
// 设置 shouldTrack 变量为 true, 表示进行依赖收集
shouldTrack = true
// 赋值 activeEffect 为当前 ReactiveEffect
activeEffect = this
// 执行 fn() 就是 effect 传入过来的那个函数 ---- 依赖
// this._fn()
let result = this._fn()
// reset 复位: 设置 shouldTrack 的状态 为 false , 关闭依赖收集 -> 在 track 执行 收集依赖前把他给返回
shouldTrack = false
return result
}
}
function cleanupEffect(effect) {
effect.deps.forEach(dep => dep.delete(effect))
// 优化
effect.deps.length = 0
}
export function trick(target, key) {
if (!activeEffect) return // 如果 activeEffect 为 undefined 直接返回
if (!shouldTrack) return // 如果 shouldTrack 为 true 直接返回 , 不会执行以下代码 依赖收集
//...
// 如果 dep这个Set() 中 有 activeEffect , 直接 true
// 因为 dep 已经有 相应的依赖了, 不要重复去收集
if (dep.has(activeEffect)) return
// ReactiveEffect 存储在 dep 中
dep.add(activeEffect)
// 添加 dep 到 deps 这个实例的数组中
activeEffect.deps.push(dep)
}
执行 pnpm test
测试全部的代码案例 。
3.7 实现 readonly
创建 readonly.spec.ts
的测试文件
- readonly 表示只读, 表明被 它代理过的 不可以进行 set 操作
- 在尝试 修改 readonly 返回的 代理对象, 会返回一个错误
console.warn
reactivity/test/readonly.ts
测试 readonly
import { readonly } from "../reactive"
describe('readonly', () => {
// 1. 实现 readonly
it('happy path', () => {
// readonly 只读
// 1. 表明着它不可以被 set
// 2. 在尝试修改它时候会抛出一个错误, 调用 set 时候 抛出一个错误
const original = { foo: 1, bar: { baz: 2 } }
const observed = readonly(original)
// readonly 返回对象 与 原始对象 不同
expect(observed).not.toBe(original)
// readonly 返回对象 与 原始对象 的 属性值相同
expect(observed.foo).toBe(1)
})
// 2. 当执行 set 时候抛出一个错误
it('warn then call set', () => {
// update
console.warn = jest.fn()
const user = readonly({ age: 19 })
user.age = 20
expect(console.warn).toBeCalled()
})
})
reactive.ts
// 实现 readonly 的逻辑
export function readonly(raw) {
return new Proxy(raw, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
console.warn(`key: ${String(key)} set 失败, 因为 target 是 readonly`, target)
return true
}
})
}
运行测试 : pnpm test reactive
, 测试通过就完成 readonly
的基本实现
优化 reactive
在实现 reactive
和 readonly
时具有许多相同的逻辑, 需要把这些相同的逻辑进行重构, 抽离重复代码, 提高可读性
创建 reactivity/baseHandlers.ts
将 reactive.ts
中实现数据代理的getter | setter 逻辑抽离, 并使用全局变量进行缓存
// 使用全局变量缓存方法,防止重复调用
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
// 创建 getter 的函数
// 参数设置 是否为 readonly 默认为false
function createGetter(isReadonly: boolean = false) {
// 返回一个函数
return function (target, key) {
let res = Reflect.get(target, key)
// 如果不是只读的
if (!isReadonly) {
// 这里执行依赖收集
trick(target, key)
}
return res
}
}
// 创建 setter 的函数
function createSetter() {
return function (target, key, value) {
let res = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key, value)
return res
}
}
// 导出 reactive 对应的 handler
export const mutableHandlers = {
get,
set
}
// 导出 readonly 对应的 handler
export const readonlyHanders = {
get: readonlyGet,
set(target, key, value) {
console.warn(`key: ${String(key)} set 失败, 因为 target 是 readonly`, target)
return true
}
}
在 reactive.ts
中对 reactive | readonly
的实现进行优化 , 抽离重复代码
import { mutableHandlers, readonlyHanders } from "./baseHandlers"
function createReactiveObject(raw, baseHandlers) {
// 返回创建 new Proxy 的实例
return new Proxy(raw, baseHandlers)
}
export function reactive(raw) {
return createReactiveObject(raw, mutableHandlers)
}
export function readonly(raw) {
return createReactiveObject(raw, readonlyHanders)
}
3.8 实现 isReadonly & isReactive & isProxy
3.8.1 isReactive
- 检查对象是否是由
reactive
创建的响应式代理。
在reactive.spec.ts
写测试内容
// 2. isReactive 判断是否是响应式对象
it('isReactive', () => {
let original = { foo: 1 }
let observable = reactive(original)
expect(isReactive(observable)).toBe(true)
expect(isReactive(original)).toBe(false)
})
在 reactive.ts
实现 isReactive
的逻辑
export function isReactive(value) {
// 实现逻辑:通过读取这个对象的key值,因为读取到key,会触发getter(),
// 然后再getter中去判断是否是reactive
return !!value['__v_isReactive']
}
在触发getter 前对响应式进行判断, baseHandler.ts
中的代码
// 创建 getter
function createGetter(isReadonly: boolean = false) {
return function (target, key) {
// 这里判断 target, 是 reactive 还是 readonly
// 根据 key 来判断
if (key === '__v_isReactive') {
return !isReadonly
}
// ....
}
}
3.8.2 isReadonly
- 检查对象是否是由
readonly
创建的只读代理。
isReadonly
的逻辑基本和 isReactive
的实现是基本一样的 readonly.spec.ts
的测试逻辑
// 3. 判断 isReadonly 判断是否是只读对象
it('isReadonly', () => {
let original = { foo: 1 }
let observable = readonly(original)
expect(isReadonly(observable)).toBe(true)
expect(isReadonly(original)).toBe(false)
})
reactive.ts
export function isReadonly(value) {
// 实现逻辑:通过读取这个对象的key值,因为读取到key,会触发getter(),然后再getter中去判断是否是reactive
return !!value['__v_isReadonly']
}
在触发getter 前对响应式进行判断, baseHandler.ts
中的代码
// 创建 getter
function createGetter(isReadonly: boolean = false) {
return function (target, key) {
// 这里判断 target, 是 reactive 还是 readonly
// 根据 key 来判断
if (key === '__v_isReactive') {
return !isReadonly
} else if (key === '__v_isReadonly') {
return isReadonly
}
// ...
}
}
3.8.3 isProxy
测试逻辑
// readonly.spec.ts
// 4. isProxy
// isProxy 判断对象是否 reactive | readonly
it('nested reactive', () => {
const original = { foo: 1, bar: { baz: 2 }, array: [{ bar: 2 }] }
const observed = readonly(original)
expect(isProxy(observed)).toBe(true)
})
// reactive.spec.ts
// 3. isProxy
// isProxy 判断对象是否 reactive | readonly
it('nested reactive', () => {
const original = { foo: 1, bar: { baz: 2 }, array: [{ bar: 2 }] }
const observed = reactive(original)
expect(isProxy(observed)).toBe(true)
})
reactive.ts
实现 isProxy的逻辑
// isProxy 判断是否是代理对象
export function isProxy(value) {
return isReactive(value) || isReadonly(value)
}
运行测试: pnpm test
,测试没有出现问题说明 基本的响应式API已经实现
3.8.4 重构优化
因为在 isReadonly | isReactive
中实现使用到了 key
的字符串 ,直接使用和在getter中也是直接 ===
判断,这样的代码不够简洁。 可以使用 枚举方式定义字符串
优化 reactive.ts
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
}
export function isReactive(value) {
// 实行逻辑:通过读取这个对象的key值,因为读取到key,会触发getter(),然后再getter中去判断是否是reactive
return !!value[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value) {
// 实行逻辑:通过读取这个对象的key值,因为读取到key,会触发getter(),然后再getter中去判断是否是reactive
return !!value[ReactiveFlags.IS_READONLY]
}
baseHandler.ts
使用 枚举
// 创建 getter
function createGetter(isReadonly: boolean = false) {
return function (target, key) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
// ...
}
}
3.9 实现 readonly & reactive 嵌套对象转换
- 但需要进行代理的源对象是 嵌套形式 , 需要把更深层次的对象也变为代理的对象
- 实现逻辑: 在 执行getter() 时,判断需要反射出去的值
let res = Reflect.get(target, key)
, 如果它是一个对象,那就继续代理 。
测试的逻辑 : readonly.spec.ts | reactive.spec.ts
// readonly.spec.ts
// 5. 嵌套的 readonly
it('nested readonly', () => {
const original = { foo: 1, bar: { baz: 2 }, array: [{ bar: 2 }] }
const observed = readonly(original)
// 1. 嵌套的bar 也应该是一个 redonly 对象
expect(isReadonly(observed.bar)).toBe(true)
// 2. 嵌套的array 也应该是一个 redonly 对象
expect(isReadonly(observed.array)).toBe(true)
// 3. 嵌套的array 中的元素也应该是一个 redonly 对象
expect(isReadonly(observed.array[0])).toBe(true)
})
// reactive.spec.ts
// 4. 嵌套的 reactive
it('nested reactive', () => {
const original = { foo: 1, bar: { baz: 2 }, array: [{ bar: 2 }] }
const observed = reactive(original)
// 1. 嵌套的bar 也应该是一个 redonly 对象
expect(isReactive(observed.bar)).toBe(true)
// 2. 嵌套的array 也应该是一个 redonly 对象
expect(isReactive(observed.array)).toBe(true)
// 3. 嵌套的array 中的元素也应该是一个 redonly 对象
expect(isReactive(observed.array[0])).toBe(true)
})
实现的逻辑: 在 执行getter() 时,判断需要反射出去的值 let res = Reflect.get(target, key)
, 如果它是一个对象,那就继续代理 。 baseHandler.ts
// 创建 getter
function createGetter(isReadonly: boolean = false) {
return function (target, key) {
// ...
let res = Reflect.get(target, key)
// if(res !== null && typeof res === 'object') -> 抽离为isObject
// 这里判断 res 是不是一个对象,如果是执行嵌套
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
// ...
}
}
shared/index.ts
, 定义抽离 对 res
的判断
export function isObject (value) {
return value !== null && typeof value === 'object'
}
运行测试 : pnpm test
, 通过测试说明实现了 嵌套的逻辑
3.10 实现 shallowReactive & shallowReadonly
在vue3响应式API中实现了
shallowReactive & shallowReadonly 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换
创建 shallowReadonly.spec.ts | shallowReactive.spec.ts
测试文件
// shallowReadonly.spec.ts
// shallowReadonly
describe('shallowReadonly', () => {
// 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换
it('should not make not-reactive properties reactive', () => {
const props = shallowReadonly({ n: { foo: 1 } })
// 1. 测试: props 是一个readonly对象
expect(isReadonly(props)).toBe(true)
// 2. 测试: props.n 不是一个 readonly 对象
expect(isReadonly(props.n)).toBe(false)
})
})
// shallowReactive.spec.ts
// shallowReactive
describe('shallowReactive', () => {
// 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换
it('should not make not-reactive properties reactive', () => {
const props = shallowReactive({ n: { foo: 1 } })
// 1. 测试: props 是一个 reactive 对象
expect(isReactive(props)).toBe(true)
// 2. 测试: props.n 不是一个 reactive对象 对象
expect(isReactive(props.n)).toBe(false)
})
})
实现逻辑: 基本和 readonly | reactive 一样,改变它们调用的 getter() 返回值。 不让继续代理
- 在
reactive.ts
创建对应的方法 ```typescript export function shallowReadonly(raw) { return createReactiveObject(raw, shallowReadonlyHandlers) }
export function shallowReactive(raw) { return createReactiveObject(raw, shallowReactiveHandlers) }
- 在 `baseHandlers.ts` 中定义相应返回的 getter 指向
```typescript
const shallowReadonlyGet = createGetter(true, true)
const shallowReactiveGet = createGetter(false, true)
// 创建 getter
// 添加了判断是否是 shallow 类型的方法
function createGetter(isReadonly: boolean = false, isShallow: boolean = false) {
return function (target, key) {
// ...
let res = Reflect.get(target, key)
// 这里添加判断, 如果调用get() 的是 ShallowReadonly 方法, 直接返回 res
if (isShallow) {
return res
}
// ...
}
}
// 这里使用到了 extend() 方法,用于合并 指向的 setter()
export const shallowReadonlyHandlers = extend({}, readonlyHanders, {
get: shallowReadonlyGet
})
export const shallowReactiveHandlers = extend({}, mutableHandlers, {
get: shallowReactiveGet
})
运行测试: pnpm test
, 通过全部测试,说明 shallowReadonly | shallowReactive
的功能实现
3.11 实现 ref
, 对 ref
的定义
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的 property .value。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
最基础的ref 的实现:
- ref 是响应式的,对所有
.value
的操作会追踪。 触发自己的 getter | setter - 可以具有一个
.value
获取属性值
测试用例 test/ref.spec.ts
describe('happy path', () => {
// 1. 实现 ref
it('ref', () => {
const a = ref(1)
// 测试1. 通过访问 .value 获取属性值
expect(a.value).toBe(1)
})
// 2. 使用 响应式的 ref
it('should be reactive', () => {
const a = ref(1)
let dummy
let calls: number = 0
effect(() => {
calls++
dummy = a.value
})
// 执行测试
expect(calls).toBe(1) // effect 应该被调用一次
expect(dummy).toBe(1) // dummy 应该为 1
// 执行更新逻辑
a.value = 2
expect(calls).toBe(2) // fn 会被调用一次, 执行依赖
expect(dummy).toBe(2) //
// 当再次更新为相同的值时,不用重新调用 effect
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
// 3. 当 ref() 的值是一个对象时候
it('should make nested properties reactive', () => {
// ref({}) 参数一个对象
const a = ref({
count: 1
})
let dummy
effect(() => {
// 通过 .value 拿到 count
dummy = a.value.count
})
expect(dummy).toBe(1)
// 更新 count
a.value.count = 2
expect(dummy).toBe(2)
})
})
实现 ref : reactivity/ref.ts
实现最基础的 ref ```typescript // 创建 RefImpl 保存 ref 的类, 并通过 getter | setter 代理.value class RefImpl { private _value: any constructor(value) { tihs._value = value } get value() { return this._value }
// TODO: 触发依赖 set value(newValue: any) { this._value = newValue return true } }
// ref export function ref(ref) { // 返回一个 new RefImpl 的实例 return new RefImpl(ref) }
2. 完善 ref
1. 实现 收集依赖 -> getter
2. 实现 执行依赖 -> setter
因为之前实现 effect 时,封装过依赖收集 和 执行依赖 的方法 `taick() | trigger()`抽离依赖收集 | 执行依赖 的逻辑代码,达到 ref | reactive 都能使用依赖收集 | 执行依赖 <br />`effect.ts`
```typescript
export function track(target, key) {
// ...
// 抽离 依赖收集的逻辑 ,传入 dep 参数, 这样 ref 传入 定义的ref 就能使用依赖收集了
// trackEffects
trackEffects(dep)
}
// 把执行依赖收集的逻辑抽离,同样方便 ref 使用
export function trackEffects(dep) {
if (dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
export function trigger(target, key) {
// ...
triggerEffects(dep) // 抽离出触发依赖的方法
}
// 收集 执行依赖的逻辑抽离,同样方便 ref 使用
export function triggerEffects(dep) {
for (let effect of dep) {
// 当触发依赖时,这里判断是否具有 scheduler,如果有就执行 scheduler,否则就执行 effect.run()
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
export function isTracking() {}
ref.ts
// 创建 RefImpl 保存 ref 的类
class RefImpl {
private _value: any
public dep
public _rawValue: any
public isRef = '_v_isRef'
constructor(value) {
this._rawValue = value
// 如果说 传过来的value 是一个对象的话,使用 reactive() 进行一个代理
// this._value = isObject(value) ? reactive(value) : value // 接收的值
// 重构
this._value = convert(value)
// 实例化 dep 的值
this.dep = new Set()
}
// 通过 .value 访问 ref 的值
// TODO: 执行依赖收集
get value() {
// 重构: 抽离收集依赖的方法: trackRefValue()
trackRefValue(this)
return this._value
}
// TODO: 触发依赖
set value(newValue) {
// 判断修改的值 与 之前的值是否相同,如果相同,则不执行触发依赖
// 重构: hasChanged 判断是否发生改变
// 问题2: 当修改的值是一个对象的话,对比会出现问题 object 的 对比; newValue 是一个 原始对象,而 _value 是一个代理对象
// 解决: 使用 _rawValue 保存 ref 的原始值
if (hasChanged(newValue, this._rawValue)) {
// 转换
this._rawValue = newValue // 赋值为新的值
// this._value 就等于代理的对象
this._value = convert(newValue)
// // 先修改值后,再进行触发依赖
// this._value = newValue
// 与收集依赖一样的逻辑,在 ReactiveEffect 已经定义好收集依赖的逻辑, 调用就行
triggerEffects(this.dep)
}
}
}
// 重构
// isObject的逻辑有重复的代码,使用 convert() 封装
function convert(value) {
return isObject(value) ? reactive(value) : value
}
// 依赖收集的逻辑
function trackRefValue(ref) {
// 在 ReactiveEffect 中的 track() 封装好依赖收集的方法
// 把收集到的依赖,定义到这个类中 dep, 同样dep是一个Set
// 又因为 track() 基于 target, 收集依赖,而 ref 只有一个 value ->对应一个dep
// 将收集依赖的逻辑抽离出来 trackEffects(), 封装 dep 的传参
// 执行到 trackEffects() 出错,因为 dep 可能是 undefined,和之前出现的原因一样
// 因为 读取 .value 值时,有一些数据, 不会触发依赖收集, 没有使用到 effect, 就单纯的读取,所以 dep 可能是 undefined
// 解决: 判断 dep 如果是 undefined, 直接返回读取的值
// 在 effect -> isTracking() 函数有判断过 dep 的存在, 直接使用就行
if (isTracking()) {
// 如果 isTracking() 返回值 为 true, 说明 activeEffect 有值
trackEffects(ref.dep)
}
}
// ref
export function ref(ref) {
// 返回一个 new RefImpl 的实例
return new RefImpl(ref)
}
/**
* ref 的总体逻辑
* 1. 因为 ref 传过来的值是一个单值, 1 -> "1"
* 而且需要知道在什么是否调用 getter 和 setter,
*
* 代理对象使用的是 Proxy, 针对于 {} 对象,
* 所以 ref 定义了 RefImpl 类,类中具有 get value 和 set value 的方法 , 这样就可以知道什么时候进行 get | set
*
* 2. 实现逻辑
* - 基本的 ref , 通过 .value 访问值, 可以通过 RefImpl 中的 get value 和 set value 方法进行代理
* - 实现响应式的ref, 需要用到 effect 和 effect 逻辑中的 track() 和 trigger() 方法 收集依赖 和 触发依赖
* - 同样再 getter 和 setter 中,需要收集依赖和触发依赖
* -
*
* - ref 收集获取依赖的逻辑 传入 dep, 它是一个Set(), 执行 trackEffect() 中 dep 会把 activeEffect 收集到 Set() 中,也就完成了 收集依赖的操作
* - 当 执行到 setter 时 传入 this.dep 收集的依赖, 通过 triggerEffects()方法达到触发依赖的方法
*/
// shared/index.ts
export const hasChanged = (newValue, value) => {
// 如果它们相等 -> false
// 如果不等 -> true -> 才会执行触发依赖
return !Object.is(newValue, value)
}
3.12 实现 isRef & unRef
- isRef 判断一个数据是不是 ref 数据
- unRef 实现如果数据是ref , 获取
ref.value
的值
测试用例:ref.spec.ts
// 4. isRef() 判断是否是 ref
it('isRef', () => {
const a = ref(1)
const user = reactive({ age: 1 })
expect(isRef(a)).toBe(true)
expect(isRef(1)).toBe(false)
expect(isRef(user)).toBe(false)
})
// 5. unRef () 获取 xxx.value 的值
it('unRef', () => {
const a = ref(1)
expect(unRef(a)).toBe(1)
expect(unRef(1)).toBe(1)
})
ref.ts
代码实现
class RefImpl {
// 设置一个内置变量,如果具有这个值,说明是ref
public _v_isRef = true
}
// isRef
export function isRef(ref) {
// 实现逻辑: 在 RefImpl 定义一个 _v_isRef 的属性, 判断是否具有这个属性,如果有,说明是一个 ref
// 使用 !! 转换 undefined
return !!ref._v_isRef
}
// unRef
export function unRef(ref) {
// 实现逻辑:1. 判断 ref 是不是 Ref, 如果是,则返回 ref.value 的值,如果不是,则返回 ref
return isRef(ref) ? ref.value : ref
}
3.13 实现 ref —— proxyRefs
proxyRefs 的功能,把 ref 的 .value 去掉,并且能够访问 ref .value的值
ref.spec.ts
// 6. 实现 proxyRefs
// proxyRefs 的功能,把 ref 的 .value 去掉,并且能够访问 ref .value的值
it('proxyRefs', () => {
const user = {
age: ref(10),
name: 'ZhangSan'
}
// 实现: get 方法 , 如果说 age(ref) 返回 .value
// not ref 返回本身的值 value
const proxyUser = proxyRefs(user)
// 1. 通过proxyRefs 代理过的对象, 中有ref(),通过属性可以访问 值
expect(proxyUser.age).toBe(10)
expect(user.age.value).toBe(10)
expect(proxyUser.name).toBe('ZhangSan')
// 实现 set , 判读是否是 ref 类型 ,
// true -> 修改 .value
// false -> 那就是 ref(xxx) , 那就进行替换
// 修改 user.age 的值,和 proxyUser.age 的值
proxyUser.age = 20
expect(user.age.value).toBe(20)
expect(proxyUser.age).toBe(20)
// 如果要修改的值是 ref 对象, 直接替换
proxyUser.age = ref(10)
expect(user.age.value).toBe(10)
expect(proxyUser.age).toBe(10)
// 实现场景:
// 1. template -> 模板中的 ref 属性
// vue3 -> setup(){return { ref(xxx) } }, 在模板中就不需要使用 .value
})
ref.ts
的实现
export function proxyRefs(objectWithRefs) {
// 如何能知道 读取的值呢 ? 可以通过 new Proxy
// get | set
return new Proxy(objectWithRefs, {
get(target, key) {
// 实现: get 方法 , 如果说 age(ref) 返回 .value
// not ref 返回本身的值 value
// 使用 unRef() 判断就行
return unRef(Reflect.get(target, key))
},
set(target, key, value) {
// 实现 set , 判读 对象之前的 key, 是否是 ref 类型 ,
// true -> 修改 .value
// false -> 那就是 ref(xxx) , 那就进行替换
// 如果之前对象的值,是一个 Ref 类型,并且修改的值 不是一个Ref 类型时
if (isRef(target[key]) && !isRef(value)) {
// 进行替换
return target[key].value = value
} else {
return Reflect.set(target, key, value)
}
}
})
}
3.14 实现 computed
- 计算属性类似 ref , 通过 返回值的 .value 访问属性值
- 计算属性 接收一个参数, 参数->是一个函数
- 参数属性 fn 具有一个返回值
- 计算属性具有 缓存的效果 (重要)
实现最基础的 computed
computed.spec.ts
describe('computed', () => {
// 1. 实现 computed 属性
it('happy path', () => {
// - 计算属性类似 ref , 通过 返回值的 .value 访问属性值
// - 计算属性 接收一个参数, 参数->是一个函数
// - 参数属性 fn 具有一个返回值
// - 计算属性具有 缓存的效果 (重要)
const user = reactive({
age: 1
})
const age = computed(() => {
// 接收一个 fn , fn 具有一个返回值
return user.age
})
// 测试1 : 计算属性返回值 通过 .value 访问
expect(age.value).toBe(1)
})
})
computed.ts
中逻辑代码
// 声明一个容器处理 computed
class ComputedRefImpl {
private _getter: any
constructor(getter) {
this._getter = getter
}
// 当访问 .value 时,返回 fn的的返回值
get value() {
return this._getter()
}
}
// 使用 getter 接收 fn
export function computed(getter) {
return new ComputedRefImpl(getter)
}
完善 computed
- 实现缓存 功能
当响应式数据发生变化时候,更新逻辑
// 2. 实现缓存 功能
// - 当响应式数据 .value 没有发生变化时候 | 二次调用时候,拿到的是缓存的值,也就是上一次的值
it('should compute lazily', () => {
const value = reactive({
foo: 1
})
// 定义一个 getter 函数
const getter = jest.fn(() => {
// 返回一个 响应式的值
return value.foo
})
// 初始化
const cValue = computed(getter)
// 测试1: 当没有使用 cValue,这个计算属性的值时, getter 不会被调用
// lazy 懒执行
expect(getter).not.toHaveBeenCalled()
// 测试2: 当使用 cValue.value, 这个计算属性的值时,调用 getter()获取相应的值, 能够读取到相应的值
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1) // getter 调用一次
// 测试3: 当再次 读取cValue.value, 而是直接拿到缓存的值, 不是从 getter 获取的值
cValue.value // 这里直接返回缓存的值, 不会执行 getter
expect(getter).toHaveBeenCalledTimes(1)
// 测试4: 当修改 响应式对象 value.foo 的值, getter 还是会执行一次
// update
value.foo = 2 // 收集trigger -> 使用 effect 收集 -> get 重新执行
// 修改值,不会调用 getter, 而是执行 scheduler, 把 this._dirty 设置为true,
// toHaveBeenCalledTimes(1) -> 是getter上一次的调用
expect(getter).toHaveBeenCalledTimes(1)
// 执行 trigger 完成修改值
expect(cValue.value).toBe(2)
// 当再次访问 cValue.value时,此时this._dirty 为 true, 这时候会调用 getter
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
computed.ts
逻辑代码
在effect
中导出ReactiveEffect
类使用 ```typescript
class ComputedRefImpl { private _getter: any private _dirty: boolean = true // 控制缓存的变量 private _value: any private _effect: any constructor(getter) { this._getter = getter
// 初始化 effect , 把 getter -> fn , 第二个参数 schelder, 当执行 tirgger() 会执行 schelder
this._effect = new ReactiveEffect(getter, () => {
// 第二次调用时候 不会一直执行 getter , 而是执行这里
// 把 dirty 设置为 true
if (!this._dirty) {
this._dirty = true // 改为 true 后,修改时调用 this._effect.run()
}
})
} get value() { // 实现测试3,缓存功能 // 逻辑实现: 使用一个变量 dire 初始设置为true 判断, // 当初始时使用 .value 计算属性, 判断dire,true -> 表示初始 -> 执行返回值操作 ; dire -> false, 表示二次访问.value , 直接走缓存
// 实现测试4,修改计算属性功能 响应式数据发生变化,
// 重新执行 getter, 但是测试里,不希望执行原来的 getter, 还希望 dirty 为 true
// 解决: 可以使用 effect 的第二个参数 -> scheduler 中实现
// 使用 -> effect
// 判断是否使用 缓存
if (this._dirty) {
// _dire -> true -> 初始操作
this._dirty = false // 关闭 -> 锁上,不能使用初始化变量
// this._value = this._getter()
// 改为 调用 fn , 当 dirty 为 true 时,执行 fn
this._value = this._effect.run()
}
// 返回 .value 的结果
return this._value
} }
`computed`总结
/**
- 计算属性总结:
- 计算属性:
- 计算属性内部有一个 effect 和 getter value
- 当用户调用 get value 时,会调用传过来的 fn, 然后把 fn 的值返回出去
- 怎么做到缓存的
- 使用了 this._dirty 这个变量,这个变量的作用是判断 这个 get value 有没有被调用过
- 第一次调用时 -> 设置 this._dirty为 false , 返回 fn 的值
- 第二次调用 -> 返回缓存的值
- 当内部依赖响应式发生改变时, value.foo = 2
- 会触发 trigger(), 然后会触发 effect 的第二个参数,-> schelder()这个函数, 因为在 trigger 中有判断 scheduler 是否存在
- 执行 scheduler() -> 会把 dirty 设置为 true , 这样当用户再次调用 get value 重新会调用 _effect.run() -> fn , 得到最新的值,然后把他return 出去
- */ ```
运行 pnpm test
, 通过测试,说明 computed()
功能实现