- 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. 实现 reactiveit('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.tsexport function reactive(raw) {// 返回一个Proxy 实例return new Proxy(raw, {// 对 对象属性进行 代理 get | setget(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 做参数, 初始时会调用这个fneffect(() => {nextAge = user.age + 1})// 当执行 effect 完后 nextAge 变为 11expect(nextAge).toBe(11)// 当更新响应式对象, effect 会再次调用 fnuser.age++// 当执行 effect 完后 nextAge 变为 12expect(nextAge).toBe(12)})})
实现 effect 的逻辑, src/reactivity/effect.ts
初始时候, 导出 effect 方法,并且使用 ReactiveEffect 这个类 作为容器,在执行 effect 时候, 创建 ReactiveEffect实例 _effect,把 fn 保存到 容器中。 在容器上定义run方法 ,调用 run()相当于执行fn
// 全局变量let activeEffect// 定义存储依赖的容器 ReactiveEffectclass ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn}// 定义 run 方法,调用 run()相当于执行fnrun() {this._fn()}}export function effect(fn) {const _effect = new ReactiveEffect(fn)// 执行它的run方法_effect.run()}
这样就实现了 effect 一开始就调用了 fn() , 在执行 fn()时,读取到响应式对象的 get | set 属性, 可以在调用这些属性时,在返回的 Proxy 实例中进行 依赖收集 getter或 触发依赖 setter
// reactive.tsexport 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() // 保存运行中的依赖// 定义存储依赖的容器 ReactiveEffectclass ReactiveEffect {/* 其他代码 */run() {// 在调用 run时, 赋值 activeEffect 为当前 ReactiveEffectactiveEffect = this// 执行 fn() 就是 effect 传入过来的那个函数 ---- 依赖this._fn()}}// 依赖收集export function trick(target, key) {// 如果没有activeEffect这个实例,不需要进行依赖收集if (!activeEffect) return// 按照这个方式去设置 dep 的存储:targetMap -> depsMap -> dep -> ReactiveEffectlet depsMap = targetMap.get(target)if (!depsMap) { // 如果没有depsMap,进行初始化depsMap = new Map()targetMap.set(target, depsMap) // 把depsMap 添加到 targetMap}// 判断是否有 deplet dep = depsMap.get(key)if (!dep) {dep = new Set() // 定义 dep, 使用 Set() 收集 ReactiveEffectdepsMap.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. 先取出 depsMaplet depsMap = targetMap.get(target)// 2. 再取出 deplet 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. 实现 runnerit('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. 实现 schedulerit('scheduler', () => {// 1. 通过effect 的第二个参数给定了一个 scheduler 的 fn// 2. 当 effect 第一次执行的时候, 还会执行fn// 3. 当 响应式对象 set update 时, 不会执行 fn , 而是执行 scheduler// 4. 如果说当执行 runner 的时候, 会再次执行 fnlet dummyconst obj = reactive({ foo: 1 })let runconst scheduler = jest.fn(() => {run = runner})// scheduler 作为 effect 的第二个参数const runner = effect(() => {dummy = obj.foo}, { scheduler })// effect 初始化时调用 fnexpect(dummy).toBe(1)// scheduler 初始化时候没有被调用expect(scheduler).not.toHaveBeenCalled()// update -> set -> 调用 schedulerobj.foo = 2// scheduler 被调用expect(scheduler).toHaveBeenCalledTimes(1)// 当 runner 调用时, 才会执行 fnrun()expect(dummy).toBe(2)})
在 effect.ts中实现 scheduler() 的逻辑代码
// 触发依赖export function trigger(target, key, value) {// 取出依赖// targetMap -> depsMap -> dep -> ReactiveEffectlet 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. 实现 stopit('stop', () => {// 1. stop 是 effect 内部实现的函数// 2. stop() 接收 runner 参数, 调用后 响应式对象值不会再发生改变// 3. 但是直接调用 runner() 还是会让响应式对象发生改变// 定义响应式let dummylet obj = reactive({ props: 1 })const runner = effect(() => {dummy = obj.props})// update -> setobj.props++expect(dummy).toBe(2)// 调用 stop()stop(runner)obj.props = 3expect(dummy).toBe(2)// 调用 runner() 触发更新runner()expect(dummy).toBe(3)})
effect.ts 实现 stop 的逻辑
// 定义存储依赖的容器 ReactiveEffectclass ReactiveEffect {private _fn: any// 用于保存 dep, 方便拿到正在执行的 ReactiveEffectdeps: any[] = []// 判断 stop() 是否被调用过,如果被调用过,则不需要再次调用 设置 active = falseactive: boolean = true/* 其他代码 */stop() {// 实现stop, 只需要把 dep 进行清空// 问题? 如果通过当前的 ReactiveEffect实例对象 找到 dep 对象// 实现: 在 ReactiveEffect 定义 deps 的数组,在执行get() 把dep添加到数组中// 使用 cleanupEffect 函数 抽离清空的逻辑// 考虑到性能问题,用户可能会多次调用 stop 方法,所以这里需要做一个判断// 使用 active 判断是否已经被清空if (this.active) {// 清空dep的逻辑cleanupEffect(this)// 把 active 的状态设置为 falsethis.active = false}}}// 执行清空 deps -> depfunction 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, 才能拿到正在执行的ReactiveEffectrunner.effect = _effectreturn 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. 实现 onStopit('onStop', () => {// 1. onStop() 是 effect 的第二个参数, onStop() 是一个函数// 2. 当 stop() 执行完后, onStop() 会被执行,也就是 stop() 的回调// 定义响应式let dummylet 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 的状态设置为 falsethis.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 shouldTrackclass ReactiveEffect {run() {// 因为执行 run() 方法会依赖收集,// 执行 stop() 不需要进行依赖的收集, 可以直接在run 方法这里判断, 做操作, active 判断 stop() 是否被调用if (!this.active) {// 返回 fnreturn this._fn()}// 设置 shouldTrack 变量为 true, 表示进行依赖收集shouldTrack = true// 赋值 activeEffect 为当前 ReactiveEffectactiveEffect = this// 执行 fn() 就是 effect 传入过来的那个函数 ---- 依赖// this._fn()let result = this._fn()// reset 复位: 设置 shouldTrack 的状态 为 false , 关闭依赖收集 -> 在 track 执行 收集依赖前把他给返回shouldTrack = falsereturn 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. 实现 readonlyit('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', () => {// updateconsole.warn = jest.fn()const user = readonly({ age: 19 })user.age = 20expect(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 默认为falsefunction 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 对应的 handlerexport const mutableHandlers = {get,set}// 导出 readonly 对应的 handlerexport 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中去判断是否是reactivereturn !!value['__v_isReactive']}
在触发getter 前对响应式进行判断, baseHandler.ts 中的代码
// 创建 getterfunction 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中去判断是否是reactivereturn !!value['__v_isReadonly']}
在触发getter 前对响应式进行判断, baseHandler.ts 中的代码
// 创建 getterfunction 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 | readonlyit('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 | readonlyit('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中去判断是否是reactivereturn !!value[ReactiveFlags.IS_REACTIVE]}export function isReadonly(value) {// 实行逻辑:通过读取这个对象的key值,因为读取到key,会触发getter(),然后再getter中去判断是否是reactivereturn !!value[ReactiveFlags.IS_READONLY]}
baseHandler.ts使用 枚举
// 创建 getterfunction 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. 嵌套的 readonlyit('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. 嵌套的 reactiveit('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
// 创建 getterfunction 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// shallowReadonlydescribe('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// shallowReactivedescribe('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 指向```typescriptconst 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 方法, 直接返回 resif (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) // 0count.value++console.log(count.value) // 1
最基础的ref 的实现:
- ref 是响应式的,对所有
.value的操作会追踪。 触发自己的 getter | setter - 可以具有一个
.value获取属性值
测试用例 test/ref.spec.ts
describe('happy path', () => {// 1. 实现 refit('ref', () => {const a = ref(1)// 测试1. 通过访问 .value 获取属性值expect(a.value).toBe(1)})// 2. 使用 响应式的 refit('should be reactive', () => {const a = ref(1)let dummylet calls: number = 0effect(() => {calls++dummy = a.value})// 执行测试expect(calls).toBe(1) // effect 应该被调用一次expect(dummy).toBe(1) // dummy 应该为 1// 执行更新逻辑a.value = 2expect(calls).toBe(2) // fn 会被调用一次, 执行依赖expect(dummy).toBe(2) //// 当再次更新为相同的值时,不用重新调用 effecta.value = 2expect(calls).toBe(2)expect(dummy).toBe(2)})// 3. 当 ref() 的值是一个对象时候it('should make nested properties reactive', () => {// ref({}) 参数一个对象const a = ref({count: 1})let dummyeffect(() => {// 通过 .value 拿到 countdummy = a.value.count})expect(dummy).toBe(1)// 更新 counta.value.count = 2expect(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. 完善 ref1. 实现 收集依赖 -> getter2. 实现 执行依赖 -> setter因为之前实现 effect 时,封装过依赖收集 和 执行依赖 的方法 `taick() | trigger()`抽离依赖收集 | 执行依赖 的逻辑代码,达到 ref | reactive 都能使用依赖收集 | 执行依赖 <br />`effect.ts````typescriptexport function track(target, key) {// ...// 抽离 依赖收集的逻辑 ,传入 dep 参数, 这样 ref 传入 定义的ref 就能使用依赖收集了// trackEffectstrackEffects(dep)}// 把执行依赖收集的逻辑抽离,同样方便 ref 使用export function trackEffects(dep) {if (dep.has(activeEffect)) returndep.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: anypublic deppublic _rawValue: anypublic 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)}}// refexport 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.tsexport 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() 判断是否是 refit('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 {// 设置一个内置变量,如果具有这个值,说明是refpublic _v_isRef = true}// isRefexport function isRef(ref) {// 实现逻辑: 在 RefImpl 定义一个 _v_isRef 的属性, 判断是否具有这个属性,如果有,说明是一个 ref// 使用 !! 转换 undefinedreturn !!ref._v_isRef}// unRefexport function unRef(ref) {// 实现逻辑:1. 判断 ref 是不是 Ref, 如果是,则返回 ref.value 的值,如果不是,则返回 refreturn 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 返回本身的值 valueconst 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 = 20expect(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 | setreturn 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 中逻辑代码
// 声明一个容器处理 computedclass ComputedRefImpl {private _getter: anyconstructor(getter) {this._getter = getter}// 当访问 .value 时,返回 fn的的返回值get value() {return this._getter()}}// 使用 getter 接收 fnexport 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 // 这里直接返回缓存的值, 不会执行 getterexpect(getter).toHaveBeenCalledTimes(1)// 测试4: 当修改 响应式对象 value.foo 的值, getter 还是会执行一次// updatevalue.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, 这时候会调用 gettercValue.valueexpect(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() 会执行 schelderthis._effect = new ReactiveEffect(getter, () => {// 第二次调用时候 不会一直执行 getter , 而是执行这里// 把 dirty 设置为 trueif (!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 时,执行 fnthis._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() 功能实现
