阅读 153

【手写 mini-vue3】- 实现 reactivity(vue minix)

1. Vue3 源码结构

首先对 Vue3 源码结构进行说明。(查看 Vue3 源码)

1.1 Monorepo

Vue3 的源码采用 Monorepo 方式进行管理。

Monorepo 是管理项目代码的一种方式,指的是在一个项目仓库(repo)中管理多个模块/包(package)。

优点:

  • 在一个项目仓库中维护多个模块

  • 方便版本管理和依赖管理,便于模块间的引用和调用

1.2 Vue3 项目结构

Vue3 源码结构.png

  • 各模块作用

    • reactivity:响应式系统

    • runtime-core:与平台无关的运行时核心

    • runtime-dom:针对浏览器的运行时

    • runtime-test:用于测试

    • server-renderer:用于服务端渲染

    • compiler-core:与平台无关的编译器核心

    • compiler-dom:针对浏览器的编译模块

    • compiler-ssr:针对服务端渲染的编译模块

    • template-explorer:用于调试编译器输出的开发工具

    • shared:多个模块之间共享的内容

    • vue:完整版本,包括运行时和编译器

  • 各模块间关系图

                              +-------------------+                           |                   |                           | @vue/compiler-sfc |                           |                   |                           +-----+--------+----+                                 |        |                                 v        v               +-------------------+    +--------------------+               |                   |    |                    |      +------->| @vue/compiler-dom +--->| @vue/compiler-core |      |        |                   |    |                    | +----+----+   +-------------------+    +--------------------+ |         | |   vue   | |         | +----+----+   +------------------+    +-------------------+    +-----------------+      |        |                  |    |                   |    |                 |      +------->| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |               |                  |    |                   |    |                 |               +------------------+    +-------------------+    +-----------------+ 复制代码

2. 初始化项目

  1. 在项目文件夹下执行yarn init -y命令生成package.json文件

  2. 执行yarn add typescript jest @types/jest -D命令安装 typescript、jest 和 @types/jest

  3. 执行tsc --init命令生成tsconfig.json文件,并修改以下几项:

    "lib": ["DOM", "ES6"],     // 引入 DOM 和 ES6 "noImplicitAny": false,    // 允许在表达式和声明中不指定类型,使用隐含的 any "types": ["jest"],         // 将 jest 的类型声明文件添加到编译中 "downlevelIteration": true // 开启 for-of 对可迭代对象的支持 复制代码

  4. package.json中配置scripts

    "scripts": {   "test": "jest" } 复制代码

  5. 根据 Jest 官网进行配置,以在 Jest 中使用 Babel 和 TypeScript:执行yarn add -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript安装 babel-jest、@babel/core、@babel/preset-env 和 @babel/preset-typescript;创建babel.config.js文件,并写入以下内容:

    module.exports = {   presets: [     ['@babel/preset-env', { targets: { node: 'current' } }],     '@babel/preset-typescript'   ] } 复制代码

  6. 执行yarn add ts-jest -D命令安装 ts-jest,之后执行ts-jest命令生成jest.config.js文件

  7. 创建src文件夹用于保存项目的代码

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文件中实现并导出tracktrigger函数,在实现过程中使用了一个全局的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 类的实例的 onStop 方法   _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的介绍:

接受一个对象(响应式或纯对象)或 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最基础的实现。

reactivereadonly的实现中有较多重复,需要对其中的代码进行优化,抽离重复代码,提高可读性。在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 } 复制代码

之后对reactivereadonly的实现进行优化,抽离出工具函数:

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 实现isReactiveisReadonlyisProxy

查看 Vue3 API 文档中的响应性 API 部分,找到isProxyisReactiveisReadonly的介绍:

isProxy

检查对象是否是由reactivereadonly创建的 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.tsreadonly的测试文件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 reactiveyarn test readonly命令运行reactivereadonly的测试,可以看到测试均通过,这样就实现了isProxy

④ 优化代码

isReactiveisReadonly的实现中使用到的特殊 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 完善reactivereadonly——响应式转换嵌套对象

reactivereadonly的响应式转换是“深层”的,会影响所有嵌套的 property,即嵌套的 property 也应该是响应式的。

分别在reactive的测试文件reactive.spec.tsreadonly的测试文件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)   }) }) 复制代码

为了通过以上测试,需要对reactivereadonly的实现进行完善,对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 reactiveyarn test readonly命令运行reactivereadonly的测试,可以看到测试均通过,这样就进一步完善了reactivereadonly的实现。

由于可能会多次使用到,因此可以将判断一个变量是否为对象抽离成一个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 实现shallowReactiveshallowReadonly

查看 Vue3 API 文档中的响应性 API 部分,找到shallowReactiveshallowReadonly的介绍:

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 都不会被代理自动解包。

在实现shallowReactiveshallowReadonly之前,首先在src/reactivity/__tests__目录下分别创建shallowReactiveshallowReadonly的测试文件shallowReactive.spec.tsshallowReadonly.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文件中分别构建shallowRreactiveshallowReadonly对应的handlers,二者分别是由mutableHandlersreadonlyHandlers替换 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文件中实现并导出shallowRreactiveshallowReadonly

export function shallowReactive(raw) {   return createReactiveObject(raw, shallowHandlers) } export function shallowReadonly(raw) {   return createReactiveObject(raw, shallowReadonlyHandlers) } 复制代码

分别执行yarn test shallowRreactiveyarn test shallowReadonly命令运行shallowRreactiveshallowReadonly的测试,可以看到测试均通过,这样就实现了shallowRreactiveshallowReadonly

3.11 实现ref

查看 Vue3 API 文档中的响应性 API 部分,找到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 e  xtends 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文件中的tracktrigger函数中抽离并导出isTrackingtrackEffectstriggerEffects函数:

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 实现isRefunRef

查看 Vue3 API 文档中的响应性 API 部分,找到isRefunRef的介绍。

isRef

检查值是否为一个 ref 对象。

unref

如果参数是一个 ref,则返回内部值,否则返回参数本身。这是val = isRef(val) ? val.value : val的语法糖函数。

function useFoo(x: number | Ref<number>) {   const unwrapped = unref(x) // unwrapped 现在一定是数字类型 } 复制代码

在实现isRefunRef之前,首先在ref的测试文件ref.spec.ts中增加关于isRefunRef的测试代码:

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文件中实现并导出isRefunRef

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的测试,可以看到测试通过,这样就实现了isRefunRef

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的介绍。

接受一个 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的实现。


作者:Stan9726
链接:https://juejin.cn/post/7028771190416080933

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐