Skip to content

Reactivity 响应式模块 ​

可以在 test 目录下新建一个 index.html 文件来测试

html
<script type="module">
    import { reactive, effect } from "../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js" // 从 vue 中取 reactivity 模块
    const state = reactive({
        name: 'fan',
        age: 18
    })
    // 副作用函数 默认执行一次,响应式数据变化后再次执行
    effect(() => {
        document.getElementById('app').innerHTML = state.name
    })
</script>

reactive 方法会将对象变为 proxy 对象,effect 中使用 reactive 对象时会进行依赖收集,当属性发生变化时会重新执行 effect 函数

1. 编写 shared 包 ​

创建一个共享的包,创建一个 shared 文件夹,使用 pnpm init 命令进行初始化

编辑 package.json 文件

json
{
  "name": "@vue/shared",
  "version": "1.0.0",
  "main": "index.js",
  "module": "dist/shared.esm-bundler.js",
  "buildOptions": {
    "formats": ["esm-bundler", "cjs"]
  }
}

创建文件 src/general.ts, 写一个判断是否为对象的函数

typescript
// 判断类型是否为对象
export const isObject = (val: unknown): val is Record<any, any> =>
    val !== null && typeof val === 'object'

在 src/index.ts 中进行统一导出

typescript
export * from './general'

2. 编写 reactive 方法 ​

  1. 不对非对象的类型来进行处理
  2. 对对象进行代理,通过代理对象操作属性(如果直接获取,会去源对象上进行获取,这时会有 this 问题,可以使用 Reflect 来解决这个问题)
javascript
const person = {
  name: "John",
  get aliasName() { // this 指向的是 person 对象
    return this.name + " Doe";
  },
};

const proxy = new Proxy(person, {
    get(target, prop, receiver) {
        console.log("proxy get: " + prop);
        return target[prop];
    },
    set(target, prop, value, receiver) {
        target[prop] = value;
        return true;
    },
});
// 这样我们只监控到了 aliasName 的取值,name的取值操作监控不到
console.log("proxy: " + proxy.aliasName); // 取 aliasName 属性时,触发了获取 name 的操作

const person2 = {
    name: "Fan",
    get aliasName() { // this 指向的是 person2 对象
        return this.name + " fan";
    },
};

const proxy2 = new Proxy(person2, {
    get(target, prop, receiver) {
        console.log("proxy2 get: " + prop);
        return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
        return Reflect.set(target, prop, value, receiver);
    }
})
console.log("proxy2: " + proxy2.aliasName);

打印结果:

bash
proxy get: aliasName
proxy: John Doe      
proxy2 get: aliasName
proxy2 get: name     
proxy2: Fan fan

使用了 Reflect 后,就会从 proxy 对象上取了,不是去源对象上取

2.1. 创建 reactivity 包 ​

创建一个 reactivity 文件夹,使用 pnpm init 命令进行初始化,并在 src 目录下创建index.ts、reactive.ts、effect.ts文件

reactive:响应式对象

effect:副作用函数

编辑 package.json 文件

json
{
  "name": "@vue/reactivity",
  "version": "1.0.0",
  "main": "index.js",
  "module": "dist/reactivity.esm-bundler.js",
  "unpkg": "dist/reactivity.global.js",
  "buildOptions": {
    "name": "VueReactivity",
    "formats": [
      "esm-browser",
      "esm-bundler",
      "cjs",
      "global"
    ]
  },
}

2.2. 安装 shared 包 ​

使用命令为 reactivity 包安装 shared 包

bash
pnpm install @vue/shared@workspace --filter @vue/reactivity

2.3. 代码实现 ​

2.3.1. 代理对象 ​

使用 proxy 对对象进行代理,并用 Reflect 解决 this 问题

typescript
import { isObject } from "@vue/shared";

export function reactive(target) {
    if (!isObject(target)) {
        // 不对非对象类型进行处理
        return target
    }

    // 对对象进行代理
    const proxy = new Proxy(target, {
        get(target, p, receiver) {
            return Reflect.get(target, p, receiver)
        },
        set(target, p, newValue, receiver) {
            return Reflect.set(target, p, newValue, receiver)
        },
    })
    return proxy
}

在 index.html 中使用一下,使用它代理同一个对象,对比是否相等

html
<script type="module">
    import { reactive } from './packages/reactivity/dist/reactivity.esm.js'

    const state = { name: 'fan', age: 18, obj: { a: 1 } }
    const p1 = reactive(state)
    const p2 = reactive(state)
    console.log(p1 === p2) // false
</script>

我们需要代理同一个对象时,返回都是相等的。可以使用 WeakMap 对对象进行缓存,然后下次使用 reactive 时判断 WeakMap 中是否存在该对象,存在的话就不再做代理

typescript
import { isObject } from "@vue/shared";

const reactiveMap = new WeakMap() // key 只能是对象

export function reactive(target) {
    if (!isObject(target)) {
        // 不对非对象类型进行处理
        return target
    }
    // 对象被代理过直接 return
    const existProxy = reactiveMap.get(target)
    if (existProxy) {
        return existProxy
    }
    // 对对象进行代理
    const proxy = new Proxy(target, {
        get(target, p, receiver) {
            return Reflect.get(target, p, receiver)
        },
        set(target, p, newValue, receiver) {
            return Reflect.set(target, p, newValue, receiver)
        },
    })
    // 缓存一下,代理过的对象,下次再进行代理的时候,直接拿出来用即可
    reactiveMap.set(target, proxy)
    return proxy
}

如果我们对代理过的对象再进行代理,会发现他们就不相等了

html
<script type="module">
    import { reactive } from './packages/reactivity/dist/reactivity.esm.js'

    const state = { name: 'fan', age: 18, obj: { a: 1 } }
    const p1 = reactive(state)
    const p2 = reactive(p1)
    console.log(p1 === p2) // false
</script>

这时候的映射关系为 p1 -> state、p2 -> p1,解决这个问题的方法:

  1. 加个反向映射,但是这种方式需要再开辟一片内存空间,不是很方便
  2. 通过加标识的方法,每个对象通过代理后都会有get、set 方法,可以在 get 方法中判断标识的时候返回一个 true,代表走的是代理对象的 get 方法
typescript
import { isObject } from "@vue/shared";

export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive"
}

const mutableHandlers = {
    get(target, p, receiver) {
        // 被代理过的对象才会走 get方法
        if (ReactiveFlags.IS_REACTIVE === p) {
            return true
        }
        return Reflect.get(target, p, receiver)
    },
    set(target, p, newValue, receiver) {
        return Reflect.set(target, p, newValue, receiver)
    },
}

const reactiveMap = new WeakMap() // key 只能是对象

export function reactive(target) {
    if (!isObject(target)) {
        // 不对非对象类型进行处理
        return target
    }
    // 如果取值的时候走了代理对象的 get 方法,便是被代理过的,并没有增加这个属性
    // 可以处理嵌套的响应式对象和 readonly 对象
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target
    }
    // 对象被代理过直接 return,确保对同一个原始对象不会创建多个代理
    const existProxy = reactiveMap.get(target)
    if (existProxy) {
        return existProxy
    }
    // 对对象进行代理
    const proxy = new Proxy(target, mutableHandlers)
    // 缓存一下,代理过的对象,下次再进行代理的时候,直接拿出来用即可
    reactiveMap.set(target, proxy)
    return proxy
}

这样就完成了 reactive 代理对象:

  • 代理同一个对象,返回的是同一个对象
  • 代理被代理过的对象,放回的是同一个对象

2.3.2. reactive 深度代理 ​

在使用 reactive 代理对象的时候,对象里面可能有对象,需要对深层的对象进行处理

typescript
import { ReactiveFlags, reactive } from './reactive'
import { track, trigger } from './effect'
import { isObject } from '@vue/shared'

export const mutableHandlers = {
    get(target, p: string | symbol, receiver: object) {
        // 被代理过的对象才会走 get方法
        if (ReactiveFlags.IS_REACTIVE === p) {
            return true
        }
        // 触发 get 的时候进行依赖收集
        track(target, p)
        const r = Reflect.get(target, p, receiver)
        // 深度代理
        if (isObject(r)) {
            return reactive(r) // 只有用户取值的时候,才会进行二次代理,不用担心性能
        }
        return r
    },
    set(
        target: object,
        p: string | symbol,
        newValue: unknown,
        receiver: object,
    ) {
        // 用户赋值操作
        let oldValue = target[p] // 没有修改之前的值
        // 返回一个 boolean 类型
        const r = Reflect.set(target, p, newValue, receiver)
        // 新值和老值不相同,触发更新
        if (oldValue !== newValue) {
            trigger(target, p, newValue, oldValue)
        }
        return r
    },
}

2.3.3 reactive 函数图解 ​

image-20240903152801246

工作流程:

  1. 将对象传入 reactive 函数:reactive(target)
  2. 判断 target 是否为对象,如果不是对象则直接 return target
  3. 判断 target 是否已经被代理,如果已经被代理,也 return target
  4. 创建 Proxy 代理,获取值的时候进行 track 依赖收集,设置值的时候进行 trigger 更新
  5. 在 WeakMap 中对 target 及其代理进行存储
  6. 返回代理对象

3. 编写 effect 方法 ​

effect 是副作用函数,也就是会产生副作用的函数。

javascript
function effect() {
  document.body.innerText = p1.name + p1.age
}

effect(fn): fn中的数据变了,需要重新执行fn。

如何处理 effect 嵌套关系呢,可以用以下两种方法

  • 栈(以前使用的方式)
  • 标记(现在使用的方式):用 parent 记录effect的父子关系,默认为undefined。
typescript
// 用一个全局变量存储被注册的副作用函数
export let activeEffect: ReactiveEffect | undefined

class ReactiveEffect<T = any> {
    active = true
    // effect 嵌套的父子关系
    parent = undefined
    // 记录 activeEffect 依赖了哪些属性
    deps = []
    constructor(public fn: () => T) {}

    run() {
        if (!this.active) {
            // 如果是失活的,表示不在effect函数中,直接函数执行即可
            return this.fn()
        }
        // 其他情况为激活状态,表示在 effect 函数中
        try {
            // 父子关系用于 effect 嵌套收集
            this.parent = activeEffect
            activeEffect = this
            return this.fn()
        } finally {
            activeEffect = this.parent
            this.parent = undefined
        }
    }
}

// 依赖收集 就是将当前的 effect 变成全局的 稍后取值的时候可以拿到这个全局的 effect
// 用于注册副作用函数
export function effect<T = any>(fn: () => T) {
    const _effect = new ReactiveEffect(fn)
    _effect.run() // 默认让响应式的 effect 执行一次
}

3.1. 编写 track 方法(依赖收集) ​

在代理对象get的时候进行依赖收集

typescript
export const mutableHandlers = {
    get(target, p: string | symbol, receiver: object) {
        // 被代理过的对象才会走 get方法
        if (ReactiveFlags.IS_REACTIVE === p) {
            return true
        }
        // 触发 get 的时候进行依赖收集
        track(target, p)
        return Reflect.get(target, p, receiver)
    },
    set(
        target: object,
        p: string | symbol,
        newValue: unknown,
        receiver: object,
    ) {
        return Reflect.set(target, p, newValue, receiver)
    },
}

如果用 target 来表示一个代理对象所代理的原始对象,它会有一些属性,比如 name,它可以被注册多个副作用函数,它们的映射关系如下:

typescript
const mapping = { // weakMap()
  target: { // Map()
    name: [activeEffect1, activeEffect2] // Set()
  }
}

/**
 * const state = reactive({ name: 'fan' })
 * effect(function activeEffect1() => {
 *   document.body.innerText = state.name
 * })
 * effect(function activeEffect2() => {
 *   document.body.innerText = state.name
 * })
 */

一个属性对应多个 effect,一个 effect 对应多个属性

属性和 effect 的关系是多对多的关系

根据映射关系编写 track 方法

typescript
const targetMap = new WeakMap()
export function track(target, key) {
    if (!activeEffect) {
        // 取值操作没有发生在 effect 中
        return
    }
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    // 判断需不需要收集,没有 activeEffect 的时候进行收集
    let shouldTrack = !dep.has(activeEffect)
    if (shouldTrack) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
    }
}

通过 debugger 观察映射关系:

img

3.2. 编写 trigger 方法(触发更新) ​

在当 effect(fn) 中的 fn 内收集的值发生了改变,需要进行触发更新。

在mutableHandlers的set中进行触发更新

typescript
export const mutableHandlers = {
    get(target, p: string | symbol, receiver: object) {
        // 被代理过的对象才会走 get方法
        if (ReactiveFlags.IS_REACTIVE === p) {
            return true
        }
        // 触发 get 的时候进行依赖收集
        track(target, p)
        return Reflect.get(target, p, receiver)
    },
    set(
        target: object,
        p: string | symbol,
        newValue: unknown,
        receiver: object,
    ) {
        // 用户赋值操作
        let oldValue = target[p] // 没有修改之前的值
        // 返回一个 boolean 类型
        const r = Reflect.set(target, p, newValue, receiver)
        // 新值和老值不相同,触发更新
        if (oldValue !== newValue) {
            trigger(target, p, newValue, oldValue)
        }
        return r
    },
}

在重新执行 effect 的时候,需要注意,我们如果在 effect 中有频繁更改值的操作,会触发 set,直接执行 run 方法会导致堆栈溢出

javascript
const state = reactive({ name: 'fan', age: 18, obj: { a: 1 } })
// 默认执行一次,数据变化后会再次执行
effect(() => {
  state.age = Math.random()
  document.body.innerText = state.name + state.age
})

setTimeout(() => {
  state.name = 'fanfan'
}, 1000)
export function trigger(target, key, newValue, oldValue) {
    // weakMap { obj: map(key, set(effect))}
    const depsMap = targetMap.get(target)
    // 判断 map 是否有值,没有直接返回
    if (!depsMap) {
        return
    }
    // 如果 set 有值才进行操作
    const dep = depsMap.get(key)
    if (dep) {
        // 遍历存放 effect 的set,执行fn函数
        dep.forEach(effect => {
            effect.run() // 直接执行会有问题
        })
    }
}

img

需要判断一下是否执行过

typescript
export function trigger(target, key, newValue, oldValue) {
    // weakMap { obj: map(key, set(effect))}
    const depsMap = targetMap.get(target)
    // 判断 map 是否有值,没有直接返回
    if (!depsMap) {
        return
    }
    // 如果 set 有值才进行操作
    const dep = depsMap.get(key)
    if (dep) {
        // 遍历存放 effect 的set,执行fn函数
        dep.forEach(effect => {
            // effect.run() // 直接执行会有问题
            // 当重新执行此effect函数,会将当前的effect放在全局上 activeEffect
            // 判断是否与全局的一样,如果一样,表示执行过
            if (activeEffect != effect) {
                effect.run()
            }
        })
    }
}

3.2.1. 清理依赖 ​

下面这个例子,一秒钟后,flag 变成了 true,age更改后是不需要触发更新的。

typescript
const state = reactive({ flag: false, name: 'fan', age: 18, obj: { a: 1 } })

effect(() => {
  document.body.innerText = state.flag ? state.name : state.age
})

// 默认第一次 flag 会收集 effect, age 会收集当前的 effect
// 默认第一次 flag 会收集 effect, name 会收集了我们的 effect, 
// 应该将age收集的依赖移除掉,否则更改age还是会触发更新
setTimeout(() => {
  state.flag = !state.flag

  setTimeout(() => {
    state.age = 20
  }, 1000)
}, 1000)

所以我们每次执行渲染的时候,应该先做一次清理操作

typescript
run() {
    if (!this.active) {
        // 如果是失活的,表示不在effect函数中,直接函数执行即可
        return this.fn()
    }
    // 其他情况为激活状态,表示在 effect 函数中
    try {
        // 父子关系用于 effect 嵌套收集
        this.parent = activeEffect
        activeEffect = this
        cleanupEffect(this)
        return this.fn() // 这个地方做了依赖收集
    } finally {
        activeEffect = this.parent
        this.parent = undefined
    }
}
function cleanupEffect(effect: ReactiveEffect) {
    effect.deps.length = 0 // 清理对应数组的
}

先把 set 清空,再清除 deps,直接清除的话,set 中还是有数据的。清除 deps 中的 set:

typescript
// 每次执行依赖收集前,先做清理操作
function cleanupEffect(effect: ReactiveEffect) {
    // 每次执行 effect 之前,我们应该清除掉 effect 中依赖的所有属性
    let { deps } = effect
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect) // 属性记录了 effect     { key: new Set() }
    }
    // 先把 set 清空,再清除 deps,直接清除的话,set 中还是有数据的
    effect.deps.length = 0 // 清理对应数组的
}

**注意:**容易犯的错,先删除后添加,会出现死循环:

typescript
const set = new Set(['a'])

set.forEach(item => {
  set.delete(item)
  set.add(item)
})

所以需要在 trigger 中对 dep 进行克隆

typescript
export function trigger(target, key, newValue, oldValue) {
    // weakMap { obj: map(key, set(effect))}
    const depsMap = targetMap.get(target)
    // 判断 map 是否有值,没有直接返回
    if (!depsMap) {
        return
    }
    // 如果 set 有值才进行操作
    const dep = depsMap.get(key)
    if (dep) {
        // 克隆一个新数组,否则会导致死循环
        const effects = [...dep]
        // 遍历存放 effect 的set,执行fn函数
        effects.forEach(effect => {
            // effect.run() // 直接执行会有问题
            // 当重新执行此effect函数,会将当前的effect放在全局上 activeEffect
            // 判断是否与全局的一样,如果一样,表示执行过
            if (activeEffect != effect) {
                effect.run()
            }
        })
    }
}

3.3. track 和 trigger 图解 ​

image-20240904110529975

3.4. 自定义调度函数(scheduler) ​

effect 中还有个 stop 函数,可以将 effect 变成失活态,失去响应式。

effect 的返回值可以是 run 函数,可以强制更新,相当于 vue2 的 $forceupdate

typescript
class ReactiveEffect<T = any> {
    active = true
    // effect 嵌套的父子关系
    parent = undefined
    // 记录 activeEffect 依赖了哪些属性
    deps = []
    constructor(public fn: () => T) {}
    // 将 effect 变成失活态
    stop() {
        if (this.active) {
            cleanupEffect(this) // 先清除依赖
            this.active = false // 再将它变成失活态
        }
    }
}

添加返回值

typescript
export function effect<T = any>(fn: () => T) {
    const _effect = new ReactiveEffect(fn)
    _effect.run() // 默认让响应式的 effect 执行一次

    const runner = _effect.run.bind(_effect) // 确保this指向的是当前的effect
    runner.effect = _effect
    return runner
}

测试一下

html
<script type="module">
  import { reactive, effect } from './packages/reactivity/dist/reactivity.esm.js'

  const state = reactive({ flag: false, name: 'fan', age: 18, obj: { a: 1 } })

  const runner = effect(() => {
    document.body.innerText = state.flag ? state.name : state.age
  })

  // 将其变成失活态
  runner.effect.stop()
  // 默认第一次 flag 会收集 effect, age 会收集当前的 effect
  // 默认第一次 flag 会收集 effect, name 会收集了我们的 effect, 应该将age收集的依赖移除掉,否则更改age还是会触发更新
  setTimeout(() => {
    state.flag = !state.flag
    console.log('flag修改了')
    setTimeout(() => {
      console.log('age修改了')
      state.age = 20
    }, 1000)
  }, 1000)
</script>

我们执行了 stop,effect 就变成失活的了,后续 flag 状态发生了改变,也没有再对页面发生改变

img

当我们执行 runner 函数,它会强制更新,页面就会刷新

html
<script type="module">
  import { reactive, effect } from './packages/reactivity/dist/reactivity.esm.js'

  const state = reactive({ flag: false, name: 'fan', age: 18, obj: { a: 1 } })

  const runner = effect(() => {
    document.body.innerText = state.flag ? state.name : state.age
  })

  // 将其变成失活态
  runner.effect.stop()
  // 默认第一次 flag 会收集 effect, age 会收集当前的 effect
  // 默认第一次 flag 会收集 effect, name 会收集了我们的 effect, 应该将age收集的依赖移除掉,否则更改age还是会触发更新
  setTimeout(() => {
    state.flag = !state.flag
    runner()
    console.log('flag修改了')
    setTimeout(() => {
      console.log('age修改了')
      state.age = 20
    }, 1000)
  }, 1000)
</script>

img

3.4.1. 实现自定义调度函数 ​

实现了 stop 和 runner,数据改变了一次他又需要调用 runner,可以写一个 scheduler 函数对数据更改做统一runner,可以减少更新次数。

html
<script type="module">
  import { reactive, effect } from './packages/reactivity/dist/reactivity.esm.js'

  const state = reactive({ flag: false, name: 'fan', age: 18, obj: { a: 1 } })

  const runner = effect(() => {
    console.log('runner')
    document.body.innerText = state.flag ? state.name : state.age
  }, {
    scheduler() {
      console.log(this) // this 指向当前的 effect
      runner()
      console.log('数据变化了')
    }
  })

  setTimeout(() => {
    state.flag = !state.flag
  }, 1000)
</script>

使用 effect 的时候传入一个选项 options

typescript
export function effect<T = any>(fn: () => T, options?) {
    const _effect = new ReactiveEffect(fn, options.scheduler)
    _effect.run() // 默认让响应式的 effect 执行一次

    const runner = _effect.run.bind(_effect) // 确保this指向的是当前的effect
    runner.effect = _effect
    return runner
}

ReactiveEffect 接收一个 scheduler 函数

typescript
export type EffectScheduler = (...args: any[]) => any

class ReactiveEffect<T = any> {
    active = true
    // effect 嵌套的父子关系
    parent = undefined
    // 记录 activeEffect 依赖了哪些属性
    deps = []
    constructor(
        public fn: () => T,
        public scheduler?: EffectScheduler,
    ) {}
}

在更新的时候判断,有传 scheduler 就执行 scheduler,否则执行 run 方法

typescript
export function trigger(target, key, newValue, oldValue) {
    // weakMap { obj: map(key, set(effect))}
    const depsMap = targetMap.get(target)
    // 判断 map 是否有值,没有直接返回
    if (!depsMap) {
        return
    }
    // 如果 set 有值才进行操作
    const dep = depsMap.get(key)
    if (dep) {
        // 克隆一个新数组,否则会导致死循环
        const effects = [...dep]
        // 遍历存放 effect 的set,执行fn函数
        effects.forEach(effect => {
            // effect.run() // 直接执行,如果 effect 中有很多赋值操作,会导致堆栈溢出
            // 当重新执行此effect函数,会将当前的effect放在全局上
            // 判断是否与全局的一样,如果一样,表示执行过
            if (activeEffect != effect) {
                if (!effect.scheduler) {
                    effect.run() // 每次调用run 都会重新依赖收集
                } else {
                    effect.scheduler()
                }
            }
        })
    }
}

3.5 effect 方法图解 ​

image-20240904141808950

4. 编写 computed 方法 ​

计算属性常用两种写法:

  • 只传 getter
  • 传 getter 和 setter
typescript
import { NOOP, isFunction } from '@vue/shared'

export type ComputedGetter<T> = (oldValue?: T) => T
export type ComputedSetter<T> = (newValue: T) => void

export interface WritableComputedOptions<T> {
    get: ComputedGetter<T>
    set: ComputedSetter<T>
}

export interface WritableComputedRef<T> extends Ref<T> {
    readonly effect: ReactiveEffect<T>
}

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
    readonly value: T
}

export function computed<T>(
    getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
) {
    let getter: ComputedGetter<T>
    let setter: ComputedSetter<T>
    // 判断是否为函数,是的话代表只传了一个 getter
    const onlyGetter = isFunction(getterOrOptions)
    if (onlyGetter) {
        // 将传入的值赋给 getter,setter为空操作
        getter = getterOrOptions
        setter = NOOP
    } else {
        // 传入对象代表有getter和setter,进行赋值
        getter = getterOrOptions.get
        setter = getterOrOptions.set || NOOP
    }
    // 返回 ComputedRef 实现类,对数据进行依赖收集和触发更新
    return new ComputedRefImpl(getter, setter)
}

4.1. 实现 ComputedRefImpl 类 ​

computed 也是依赖与 effect 实现的,vue3 不同于 vue2,它的 computed 做了依赖收集。

typescript
import { ReactiveEffect, trackEffects, triggerEffects } from './effect'

export class ComputedRefImpl<T> {
    public dep = undefined
    public readonly effect: ReactiveEffect<T>
    public readonly __v_isRef = true // 意味着有这个属性,需要用 .value 来进行取值
    public _dirty = true // 数据是否是脏的,脏的说明值变了
    private _value!: T // 默认的缓存结果

    constructor(
        private getter: ComputedGetter<T>,
        private readonly _setter: ComputedSetter<T>,
    ) {
        // 源码这里不能使用 effect(() => {}, { scheduler() {} })
        this.effect = new ReactiveEffect<T>(this.getter, () => {
            this._dirty = true
            // 触发更新
            triggerEffects(this.dep)
        })
    }
    // 类的属性访问器 Object.defineProperty(实例, value, { get })
    get value() {
        // 如果有 activeEffect 意味着这个计算属性在 effect 中使用
        // 需要让计算属性收集这个 effect
        // 用户取值的时候发生依赖收集
        trackEffects(this.dep || (this.dep = new Set()))
        // 取值才执行,并把取的值缓存起来
        if (this._dirty) {
            this._value = this.effect.run()
            this._dirty = false // 意味着取过值了
        }
        return this._value
    }

    set value(newValue: T) {
        this._setter(newValue)
    }
}

在 computed 里用到了track依赖收集和trigger触发更新,之前写的需要传入 target 和 key,在 computed 中只有 dep 能拿到,所以需要对 track 和 trigger 中的内容进行抽离。

typescript
export function track(target, key) {
    if (!activeEffect) {
        // 取值操作没有发生在 effect 中
        return
    }
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    // 判断需不需要收集,没有 activeEffect 的时候进行收集
   	// let shouldTrack = !dep.has(activeEffect)
    // if (shouldTrack) {
    //     dep.add(activeEffect)
    //     activeEffect.deps.push(dep)
    // }
    trackEffects(dep)
}

export function trackEffects(dep) {
    // 判断需不需要收集,没有 activeEffect 的时候进行收集
    let shouldTrack = !dep.has(activeEffect)
    if (shouldTrack) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
    }
}
export function trigger(target, key, newValue, oldValue) {
    // weakMap { obj: map(key, set(effect))}
    const depsMap = targetMap.get(target)
    // 判断 map 是否有值,没有直接返回
    if (!depsMap) {
        return
    }
    // 如果 set 有值才进行操作
    const dep = depsMap.get(key)
    // if (dep) {
    // 克隆一个新数组,否则会导致死循环
    // const effects = [...dep]
    // 遍历存放 effect 的set,执行fn函数
    // effects.forEach(effect => {
    //     // effect.run() // 直接执行,如果 effect 中有很多赋值操作,会导致堆栈溢出
    //     // 当重新执行此effect函数,会将当前的effect放在全局上
    //     // 判断是否与全局的一样,如果一样,表示执行过
    //     if (activeEffect != effect) {
    //         if (!effect.scheduler) {
    //             effect.run() // 每次调用run 都会重新依赖收集
    //         } else {
    //             effect.scheduler()
    //         }
    //     }
    // })
    // }
    triggerEffects(dep)
}

export function triggerEffects(dep) {
    if (dep) {
        // 克隆一个新数组,否则会导致死循环
        const effects = [...dep]
        // 遍历存放 effect 的set,执行fn函数
        effects.forEach(effect => {
            // effect.run() // 直接执行,如果 effect 中有很多赋值操作,会导致堆栈溢出
            // 当重新执行此effect函数,会将当前的effect放在全局上
            // 判断是否与全局的一样,如果一样,表示执行过
            if (activeEffect != effect) {
                if (!effect.scheduler) {
                    effect.run() // 每次调用run 都会重新依赖收集
                } else {
                    effect.scheduler()
                }
            }
        })
    }
}

4.2 实现步骤 ​

  1. 创建 ComputedRefImpl 实例,这个实例包含了计算属性的核心逻辑,如缓存机制、依赖追踪;他有一个 _dirty 标志来表示当前缓存是否有效

  2. 创建 effect,使用传入的 getter 函数创建一个 ReactiveEffect 实例,执行 effect 的 run 方法会收集计算属性的依赖。

  3. 设置 scheduler 调度器,为 effect 设置一个自定义调度器,当依赖发生变化时,调度器会被触发,调度器主要做两件事情:

    ​ a. 将_dirty 标记为 true,表示当前缓存已失效

    ​ b. 触发更新

  4. get 取值的时候触发依赖收集,_dirty 为 true 时,才执行 effect 重新计算,计算完成后将 _dirty 设置为 false

5. 编写 watch 方法 ​

watch 有很多种写法,常用的有两种写法,侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数

typescript
// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): StopHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): StopHandle

第一个参数 source 为侦听源,来源有以下几种:

  • 一个函数,返回一个值
  • 一个 ref
  • 一个响应式对象
  • 或是由以上类型的值组成的数组
typescript
import { isReactive } from './reactive'
import { isFunction, isObject, isPlainObject } from '@vue/shared'
import { ReactiveEffect } from './effect'

export interface WatchOptions<Immediate = boolean> {
    immediate?: Immediate
    deep?: boolean
    once?: boolean
}

export function traverse(value: unknown, seen?: Set<unknown>) {
    if (isObject(value)) {
        return value
    }
    // 考虑循环引用的问题,采用set来解决这个问题
    seen = seen || new Set()
    if (seen.has(value)) {
        return value
    }

    seen.add(value)
    // 如果值还是对象,接着取
    if (isPlainObject(value)) {
        for (const key in value) {
            traverse(value[key], seen) // 递归取值
        }
    }
    return value
}

export function watch(source, cb, { immediate }: WatchOptions = {}) {
    let getter: () => any
    if (isReactive(source)) {
        // 最终都处理成函数
        getter = () => traverse(source) // 直接稍后调用run的时候会执行此函数,直接返回对象,只有访问属性才能依赖收集
    } else if (isFunction(source)) {
        getter = source
    }

    let oldValue: any
    const job = () => {
        // 内部调用 cb 也就是 watch 的回调方法
        if (!effect.active) {
            return
        }
        if (cb) {
            // watch(source, cb)
            const newValue = effect.run() // 再次调用 effect,拿到新值
            cb(newValue, oldValue) // 调用回调传入新值和老值
            oldValue = newValue // 更新
        }
    }

    const effect = new ReactiveEffect(getter, job)

    // 初始化
    if (cb) {
        if (immediate) {
            job()
        } else {
            oldValue = effect.run() // 保留老值
        }
    }
}

编写 isPlainObject 方法

typescript
// toString 方法
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
    objectToString.call(value)
// 判断是否为原始对象
export const isPlainObject = (val: unknown): val is object =>
    toTypeString(val) === '[object Object]'

5.1. 根据 watch 实现 watchEffect 方法 ​

watch` 它是懒侦听的,不会立即执行。`watchEffect` 会**立即执行**。其实它们在实现上面就是传与不传 `cb` 的区别,对 `watch` 中的公共方法进行抽离成 `dowatch` 函数。然后通过判断是否传了 `cb`,来实现 `watchEffect

笼统的来说:

  • watch 本身就是一个 effect + 自定义 scheduler
  • watchEffect 就是一个 effect
typescript
function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): StopHandle

type OnCleanup = (cleanupFn: () => void) => void

抽离 doWatch

typescript
export function doWatch(source, cb, { immediate }: WatchOptions = {}) {
    let getter: () => any
    if (isReactive(source)) {
        // 最终都处理成函数
        getter = () => traverse(source) // 直接稍后调用run的时候会执行此函数,直接返回对象,只有访问属性才能依赖收集
    } else if (isFunction(source)) {
        getter = source
    }

    let oldValue: any
    const job = () => {
        // 内部调用 cb 也就是 watch 的回调方法
        if (!effect.active) {
            return
        }
        if (cb) {
            // watch(source, cb)
            const newValue = effect.run() // 再次调用 effect,拿到新值
            cb(newValue, oldValue) // 调用回调传入新值和老值
            oldValue = newValue // 更新
        } else {
            // watchEffect
            effect.run() // 调用 run 方法,会触发依赖重新清理和收集
        }
    }

    const effect = new ReactiveEffect(getter, job)

    // 初始化
    if (cb) {
        if (immediate) {
            job()
        } else {
            oldValue = effect.run() // 保留老值
        }
    } else {
        // watchEffect
        effect.run()
    }
}

watch 和 watchEffect 方法

typescript
export interface WatchOptionsBase {
    flush?: 'pre' | 'post' | 'sync'
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
    immediate?: Immediate
    deep?: boolean
    once?: boolean
}

export function watch(source: any, cb: any, options?: WatchOptions) {
    return doWatch(source, cb, options)
}

export function watchEffect(source: any, options?: WatchOptionsBase) {
    return doWatch(source, null, options)
}

5.2. 实现 onCleanup 副作用清除方法 ​

应用场景:输入框搜索,只取最后的值

官网有个清除副作用的例子:

typescript
watch(id, async (newId, oldId, onCleanup) => {
  const { response, cancel } = doAsyncWork(newId)
  // 当 `id` 变化时,`cancel` 将被调用,
  // 取消之前的未完成的请求
  onCleanup(cancel)
  data.value = await response
})

watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

对例子进行完善,更容易理解:

html
<script type="module">
  import { reactive, watch } from '../packages/reactivity/dist/reactivity.esm.js'

  const state = reactive({ flag: false, name: 'fan', age: 18, obj: { a: 1 } })
  /**
   * 第一次数据变化了,调用一个接口getData()  2000ms后返回数据
   * 第二次数据变化了,再调用一个接口getData() 1000ms后返回数据
   * 这样就会导致第一次的结果覆盖掉第二次的结果
   */
  let timer = 3000
  function getData(data) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(data)
      }, timer -= 1000)
    })
  }


  watch(() => state.age, async function(newValue) {
    const result = await getData(newValue)
    document.body.innerText = result
  })

  state.age = 19
  state.age = 20
</script>

上述代码,页面中最终显示的数字为 19。如果想让他显示 20,可以用标记的方法进行处理,也可以说是防抖,多次触发,只取一次。

javascript
let arr = []
watch(() => state.age, async function(newValue) {
  let flag = true
  // 如果不是最后一个请求,就把 flag 设为false
  while (arr.length) {
    let cb = arr.shift()
    cb()
  }
  arr.push(() => {
    flag = false
  })
  const result = await getData(newValue)
  // 只渲染最后一个值
  if (flag) {
    document.body.innerText = result
  }
})

实现清除副作用的方法

typescript
type OnCleanup = (cleanupFn: () => void) => void

let getter: () => any
if (isReactive(source)) {
    // 最终都处理成函数
    getter = () => traverse(source) // 直接稍后调用run的时候会执行此函数,直接返回对象,只有访问属性才能依赖收集
} else if (isFunction(source)) {
    if (cb) {
        getter = source
    } else {
        getter = () => {
            if (cleanup) {
                cleanup()
            }
            // watchEffect 也可以接收一个 onCleanup 参数
            return source(onCleanup)
        }
    }
}

let oldValue: any
let cleanup: (() => void) | undefined
const onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = fn
}
const job = () => {
    // 内部调用 cb 也就是 watch 的回调方法
    if (!effect.active) {
        return
    }
    if (cb) {
        // watch(source, cb)
        const newValue = effect.run() // 再次调用 effect,拿到新值
        if (cleanup) cleanup()
        cb(newValue, oldValue, onCleanup) // 调用回调传入新值和老值
        oldValue = newValue // 更新
    } else {
        // watchEffect
        effect.run() // 调用 run 方法,会触发依赖重新清理和收集
    }
}

使用:

在 watch 中,onCleanup 是 callback 的第三个参数

在 watchEffect 中,onCleanup 是 source 中的一个参数

typescript
watch(() => state.age, async function(newValue, oldValue, onCleanup) {
  let flag = true
  onCleanup(() => {
    flag = false
  })
  const result = await getData(newValue)
  // 只渲染最后一个值
  if (flag) {
    document.body.innerText = result
  }
})

watchEffect(async (onCleanup) => {
  let flag = true
  onCleanup(() => {
    flag = false
  })
  const result = await getData(state.age)
  // 只渲染最后一个值
  if (flag) {
    document.body.innerText = result
  }
})

6. 编写 ref 方法 ​

ref 是基于 reactive 实现的,它可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。ref 会使它的值具有深层响应性。

6.1. 实现 isRef 方法 ​

通过判断是否有 __v_isRef 属性来判断是否为 ref。它可以被用做 ts 的类型守卫

typescript
export interface Ref<T = any> {
    value: T
}

export function isRef(r: any): r is Ref {
    return !!(r && r.__v_isRef === true)
}

6.2. 实现 ref 方法 ​

typescript
export function createRef(rawValue: unknown) {
    // 判断传入的值是否为Ref,如果是则直接返回
    if (isRef(rawValue)) {
        return rawValue
    }
    return new RefImpl(rawValue)
}

export function ref(value?: unknown) {
    return createRef(value)
}

// 如果是对象就转成 reactive,否则返回原来的值
export const toReactive = <T extends unknown>(value: T): T =>
    isObject(value) ? reactive(value) : value

class RefImpl<T> {
    private _value: T
    public readonly __v_isRef = true
    public dep? = undefined

    constructor(public rawValue: T) {
        this._value = toReactive(this.rawValue)
    }

    get value() {
        // 依赖收集
        if (activeEffect) {
            trackEffects(this.dep || (this.dep = new Set()))
        }
        return this._value
    }

    set value(newValue) {
        // 比较两个值是否一样,如果不一样则重新赋值
        if (hasChanged(newValue, this.rawValue)) {
            this.rawValue = newValue
            this._value = toReactive(newValue)

            // 触发更新
            triggerEffects(this.dep)
        }
    }
}

6.3. 实现 unRef 方法 ​

如果参数是 ref,则返回内部值,否则返回参数本身。

它是val = isRef(val) ? val.value : val计算的一个语法糖。

typescript
export type MaybeRef<T = any> = T | Ref<T>

export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
  return isRef(ref) ? ref.value : ref
}

它一般用于保证 ts 类型

typescript
function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x)
  // unwrapped 现在保证为 number 类型
}

6.4. 实现 toRef 方法 ​

toRef 的主要用法:

  • 规范化签名
typescript
// 按原样返回现有的 ref
toRef(existingRef)

// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo)

// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1)
  • 对象属性签名
typescript
const state = reactive({
  foo: 1,
  bar: 2
})

// 双向 ref,会与源属性同步
const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

**注意:**这跟 const fooRef = ref(state.foo) 不同。这个 ref 不会和 state.foo 保持同步,因为这个 ref() 接收到的是一个纯数值。

toRef 使用场景:

typescript
<script setup>
import { toRef } from 'vue'

const props = defineProps(/* ... */)

// 将 `props.foo` 转换为 ref,然后传入
// 一个组合式函数
useSomeFeature(toRef(props, 'foo'))

// getter 语法——推荐在 3.3+ 版本使用
useSomeFeature(toRef(() => props.foo))
</script>

实现:

typescript
class ObjectRefImpl<T extends object, K extends keyof T> {
    public __v_isRef = true

    constructor(
        private readonly object: T,
        private readonly key: K,
    ) {}

    get value() {
        return this.object[this.key]
    }

    set value(newVal) {
        this.object[this.key] = newVal
    }
}

function propertyToRef(source: Record<string, any>, key: string) {
    const val = source[key]
    return isRef(val) ? val : (new ObjectRefImpl(source, key) as any)
}

class GetterRefImpl<T> {
    public readonly __v_isRef = true
    public readonly __v_isReadonly = true

    constructor(private readonly _getter: () => T) {}

    get value() {
        return this._getter()
    }
}

export function toRef(source, key?: string): Ref {
    if (isRef(source)) {
        // 如果是 ref 则直接返回
        return source
    } else if (isFunction(source)) {
        // return source()
        // 直接调用source()是对函数的一次性调用,得到一个立即值。
        // 而new GetterRefImpl(source)创建了一个可以被Vue追踪并在其响应式系统中作为只读引用使用的对象,它延迟了source函数的执行,直到实际需要这个值的时候
        // .value 的时候再取值
        return new GetterRefImpl(source) as any
    } else if (isObject(source) && arguments.length > 1) {
        // 假如传入的是对象,则必须传入 key,也需要转成 ref
        return propertyToRef(source, key!)
    } else {
        // 返回个 ref
        return ref(source)
    }
}

6.5. 实现 toValue 方法 ​

他的作用是将值、refs 或 getters 规范化为值

typescript
toValue(1) //       --> 1
toValue(ref(1)) //  --> 1
toValue(() => 1) // --> 1

实现原理就是,判断是否为函数,是的话就执行,不是的话就用 unref 取值

typescript
export type MaybeRefOrGetter<T = any> = MaybeRef<T> | (() => T)

export function toValue<T>(source: MaybeRefOrGetter<T> | ComputedRef<T>): T {
    return isFunction(source) ? source() : unref(source)
}

6.6. 实现 toRefs 方法 ​

将一个响应式对象转换为一个普通对象

typescript
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
  foo: Ref<number>,
  bar: Ref<number>
}
state.foo === stateAsRefs.foo.value // true
*/

实现:

typescript
export function toRefs<T extends object>(object: T) {
    const ret: any = Array.isArray(object) ? new Array(object.length) : {}
    for (const key in object) {
        ret[key] = propertyToRef(object, key)
    }
    return ret
}

6.7. 实现 isReactive 方法 ​

通过判断对象是否有IS_REACTIVE 属性,有则为 reactive

typescript
export function isReactive(value: unknown): boolean {
    return !!(value && value[ReactiveFlags.IS_REACTIVE])
}

6.8. 实现 proxyRefs 方法 ​

他的作用是:在内部对数据做处理,用户在模板中使用的时候,不用 .value 取值

typescript
const shallowUnwrapHandles: ProxyHandler<any>  = {
    get: (target, p, receiver) => {
        return unref(Reflect.get(target, p, receiver))
    },
    set(target, p, newValue, receiver) {
        const oldValue = target[p]
        if (isRef(oldValue) && !isRef(newValue)) {
            oldValue.value = newValue
            return true
        } else {
            return Reflect.set(target, p, newValue, receiver)
        }
    },
}

export function proxyRefs<T extends object>(object: T) {
    return isReactive(object) ? object : new Proxy(object, shallowUnwrapHandles)
}

7. 编写 effectScope 方法 ​

状态管理工具 pinia,有使用这个方法。在组件中用了一堆响应式数据,计算属性,watch,我们如果需要将它们都变成失活态,需要一个一个的进行调用 stop 函数。所以就有了 effectScope 方法,他可以统一将作用域中的 effect 变成失活态。把 effect 放在 effectScope 的 run 方法中,可以调用 effectScope 的 stop 方法,统一改变 effect 的状态

typescript
// 创建一个 effect 域
const scope = effectScope()

scope.run(() => {
  const state = reactive({ name: 'fan', age: '18' })
  watchEffect(() => {
    console.log(state.name)
  })
  setTimeout(() => {
    state.name = 'fanfan'
  }, 1000)

  const scope = effectScope(true) // 如果设置为true ,里面的 effect 就不会被收集,还是激活态
  scope.run(() => {
    const state = reactive({ name: 'fan', age: '18' })
    watchEffect(() => {
      console.log(state.age)
    })
    setTimeout(() => {
      state.age = 19
    }, 1000)
  })
})

scope.stop()
// 打印
// fan
// 18
// 19

实现:

typescript
import type { ReactiveEffect } from './effect'

export let activeEffectScope: EffectScope | undefined

class EffectScope {
    _active = true
    parent = undefined
    effects = [] // 这个是收集内部的 effect
    scopes // 这个是收集作用域的

    constructor(public detached = false) {
        if (!detached && activeEffectScope) {
            // 内部的 effectScope, 如果传的 detached 为 true,则不被收集
            activeEffectScope.scopes ||
                (activeEffectScope.scopes = []).push(this)
        }
    }

    get active() {
        return this._active
    }

    run<T>(fn: () => T): T | undefined {
        if (this._active) {
            try {
                this.parent = activeEffectScope
                activeEffectScope = this
                return fn()
            } finally {
                activeEffectScope = this.parent
            }
        }
    }

    stop() {
        if (this._active) {
            for (let i = 0; i < this.effects.length; i++) {
                this.effects[i].stop() // 让每一个存储的effect都停止
            }

            if (this.scopes) {
                for (let i = 0; i < this.scopes.length; i++) {
                    this.scopes[i].stop() // 调用的是作用域的stop
                }
            }
            this.parent = undefined
            // 变成失活态
            this._active = false
        }
    }
}

export function recordEffectScope(effect: ReactiveEffect) {
    // 收集内部的 effect
    if (activeEffectScope && activeEffectScope.active) {
        activeEffectScope.effects.push(effect)
    }
}

export function effectScope(detached?: boolean) {
    return new EffectScope(detached)
}

在 ReactiveEffect 中收集 effect

typescript
export class ReactiveEffect<T = any> {
  active = true
  // effect 嵌套的父子关系
  parent = undefined
  // 记录 activeEffect 依赖了哪些属性
  deps = []
  constructor(
      public fn: () => T,
      public scheduler?: EffectScheduler,
  ) {
      // 收集 effect
      recordEffectScope(this)
  }
}

Released under the MIT License.