Skip to content

手写 effect

基础的effect

js
/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

class ReactiveEffect {
    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 让 fn 执行一次先
        return this.fn()
    }
}

示例:

vue
<template>
    <div ref="wrapper"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { effect, reactive } from '../source/reactivity'

const wrapper = ref(null)

const state = reactive({
    name: '小李大人',
    age: 18
})

onMounted(() => {
    effect(() => {
        wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    })

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

</script>

页面效果 页面效果

可以看到这时候的effect会在一开始执行一次,但是当state.age变化后,effect没有再次执行

将响应式数据跟effect关联起来

vue
<template>
    <div ref="wrapper"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { effect, reactive } from '../source/reactivity'

const wrapper = ref(null)

const state = reactive({
    name: '小李大人',
    age: 18
})

onMounted(() => {
    effect(() => {
        wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    })

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

</script>
js
// effect.js
/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

class ReactiveEffect {

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        // 激活状态下,执行后需要进行依赖搜集
        activeEffect = this
        return this.fn()
    }
}

// reactive.js
import { isObject } from '../utils'
import { activeEffect } from './effect'

const IS_REACTIVE_KEY = '__v_isReactive'

/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()

const mutableHandlers = {
    get(target, key, receiver) {

        // 访问对象是否被代理过的属性
        if (key === IS_REACTIVE_KEY) return true

        const value = Reflect.get(target, key, receiver)

        // 依赖收集 todo...
        console.log(activeEffect, key)

        return value
    },
    set(target, key, value, receiver) {

        // 触发更新 todo...

        return Reflect.set(target, key, value, receiver)
    }
}

/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
    // 首先判断参数是不是对象
    if (!isObject(obj)) {
        return obj
    }

    // 判断对象是否已经被代理过
    if (obj[IS_REACTIVE_KEY]) {
        return obj
    }

    // 如果代理过,则直接返回代理过的对象
    if (reactiveMap.has(obj)) {
        return reactiveMap.get(obj)
    }

    const proxy = new Proxy(obj, mutableHandlers)

    // 缓存代理过的对象
    reactiveMap.set(obj, proxy)

    return proxy
}

控制台输出 控制台输出

这里通过activeEffect获取到当前正在执行的副作用函数,然后通过key获取到当前正在访问的属性,从而将数据跟effect关联起来。

这里有一个明显问题,effect执行完成后,等到setTimeout执行的时候,activeEffect还有值,这显然不太多,因为effect执行完成后,activeEffect应该就没有了

故做如下修改

js

/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

class ReactiveEffect {

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        try {
            activeEffect = this
            return this.fn()
        } finally {
            activeEffect = null
        }
    }
}

控制台输出 控制台输出

这样一来,effect执行完成后,activeEffect就没有了,setTimeout执行的时候,activeEffectnull

这样会有一个bug,当effect进行嵌套调用的时候,activeEffect会丢失

解决嵌套调用的问题

例:

vue
<template>
    <div ref="wrapper"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { effect, reactive } from '../source/reactivity'

const wrapper = ref(null)

const state = reactive({
    name: '小李大人',
    age: 18
})

onMounted(() => {
    effect(() => {
        console.log(state.name)

        effect(() => {
            console.log(state.name)
        })

        console.log(state.age)
    })

    // effect(() => {
    //     wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    // })

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

</script>

控制台输出 控制台输出

可以看到,effect嵌套调用的时候,activeEffect丢失了

在第二个effect执行完成后,activeEffectnull,导致第一个effect中读取state.age的时候,activeEffectnull

所以,在每一次effect执行的时候,我们需要保存上一次的effect,在执行完成后,再恢复

如下:

js

/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

class ReactiveEffect {

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        let lastEffect = activeEffect
        try {
            activeEffect = this
            return this.fn()
        } finally {
            activeEffect = lastEffect
        }
    }
}

控制台输出 控制台输出

这样在每一次执行effect前,都会保存上一次的effect,执行完成后,再将上一次的effect恢复,这样就可以实现effect的嵌套执行了

依赖收集

依赖收集表的结构如下:

js
{
    {name: '小李大人',age: 18}: {
        name: {effect: effect._trackId, effect: effect._trackId, ...},
        age: {effect: effect._trackId, effect: effect._trackId, ...}
    }
}

整个依赖收集表都是一个WeakMap,键是target,值是一个MapMap的键是targetkey,值是一个MapMap的是键是effect,值是effect_trackId, 这样就可以实现一个target对应多个key,一个key对应多个effect

每一个effect中,都会有一个deps数组,用于存储当前effect依赖的属性,形成一个双向依赖关系

vue
<template>
    <div ref="wrapper"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { effect, reactive } from '../source/reactivity'

const wrapper = ref(null)

const state = reactive({
    name: '小李大人',
    age: 18
})

onMounted(() => {
    effect(() => {
        wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
    })

    effect(() => {
        wrapper.value.innerHTML = `姓名:${state.name}`
    })

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

</script>
js
// reactive.js

import { isObject } from '../utils'
import { track } from './reactiveEffect'

const IS_REACTIVE_KEY = '__v_isReactive'

/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()

const mutableHandlers = {
    get(target, key, receiver) {

        // 访问对象是否被代理过的属性
        if (key === IS_REACTIVE_KEY) return true

        const value = Reflect.get(target, key, receiver)

        // 依赖收集
        track(target, key)

        return value
    },
    set(target, key, value, receiver) {

        // 触发更新 todo...

        return Reflect.set(target, key, value, receiver)
    }
}

/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
    // 首先判断参数是不是对象
    if (!isObject(obj)) {
        return obj
    }

    // 判断对象是否已经被代理过
    if (obj[IS_REACTIVE_KEY]) {
        return obj
    }

    // 如果代理过,则直接返回代理过的对象
    if (reactiveMap.has(obj)) {
        return reactiveMap.get(obj)
    }

    const proxy = new Proxy(obj, mutableHandlers)

    // 缓存代理过的对象
    reactiveMap.set(obj, proxy)

    return proxy
}

// reactiveEffect.js
import { activeEffect, trackEffect } from './effect'

/** 依赖收集表 */
const targetMap = new WeakMap()

export const createDep = (cleanUp, name) => {
    const dep = new Map()
    dep.cleanUp = cleanUp
    dep.name = name
    return dep
}

export function track(target, key) {
    // 没有 activeEffect,说明不是在effect中使用这个key的,不需要收集依赖
    if (!activeEffect) return

    if (!targetMap.has(target)) {
        targetMap.set(target, new Map())
    }

    const depsMap = targetMap.get(target)

    if (!depsMap.has(key)) {
        depsMap.set(key, createDep(() => depsMap.delete(key), key))
    }

    const dep = depsMap.get(key)

    trackEffect(activeEffect, dep)

    console.log(targetMap)
}

// effect.js

/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

class ReactiveEffect {
    /** 记录当前effect执行的次数 */
    _trackId = 0
    /** 依赖收集表 */
    deps = []
    /** 依赖收集表的长度 */
    _depsLength = 0

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        let lastEffect = activeEffect
        try {
            activeEffect = this
            return this.fn()
        } finally {
            activeEffect = lastEffect
        }
    }
}

export function trackEffect(effect, dep) {
    dep.set(effect, effect._trackId)
    effect.deps[effect._depsLength++] = dep
}

控制台输出 控制台输出

根据依赖收集表,可以清晰的看到,{name: '小李大人',age: 18}这个对象的依赖关系,因为name属性在两个effect函数中被使用,所以可以看到name属性的依赖收集表有两个effect对象,而age属性只有一个effect对象。

这里为了方便观察,给每一个dep添加了一个name属性,用于标识这个dep是哪一个属性的依赖收集表。同时给这个dep添加了一个cleanUp方法用来将当前的dep从它的父depsMap中删除。

触发更新

reactiveset方法被调用的时候去判断新旧的值是否发生变化,如果发生变化,则触发更新。

js
// reactive.js
import { isObject } from '../utils'
import { track, trigger } from './reactiveEffect'

const IS_REACTIVE_KEY = '__v_isReactive'

/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()

const mutableHandlers = {
    get(target, key, receiver) {

        // 访问对象是否被代理过的属性
        if (key === IS_REACTIVE_KEY) return true

        const value = Reflect.get(target, key, receiver)

        // 依赖收集
        track(target, key)

        return value
    },
    set(target, key, value, receiver) {

        const oldValue = target[key]

        const result = Reflect.set(target, key, value, receiver)

        if (oldValue !== value) {
            // 触发更新
            trigger(target, key, value, oldValue)
        }

        return result
    }
}

/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
    // 首先判断参数是不是对象
    if (!isObject(obj)) {
        return obj
    }

    // 判断对象是否已经被代理过
    if (obj[IS_REACTIVE_KEY]) {
        return obj
    }

    // 如果代理过,则直接返回代理过的对象
    if (reactiveMap.has(obj)) {
        return reactiveMap.get(obj)
    }

    const proxy = new Proxy(obj, mutableHandlers)

    // 缓存代理过的对象
    reactiveMap.set(obj, proxy)

    return proxy
}

// reactiveEffect.js
import { activeEffect, trackEffect } from './effect'

/** 依赖收集表 */
const targetMap = new WeakMap()

export const createDep = (cleanUp, name) => {
    const dep = new Map()
    dep.cleanUp = cleanUp
    dep.name = name
    return dep
}

export function track(target, key) {
    // 没有 activeEffect,说明不是在effect中使用这个key的,不需要收集依赖
    if (!activeEffect) return

    if (!targetMap.has(target)) {
        targetMap.set(target, new Map())
    }

    const depsMap = targetMap.get(target)

    if (!depsMap.has(key)) {
        depsMap.set(
            key,
            createDep(() => depsMap.delete(key), key)
        )
    }

    const dep = depsMap.get(key)

    trackEffect(activeEffect, dep)
}

export function trigger(target, key, newValue, oldValue) {
    console.log(target, key, newValue, oldValue)
}

控制台输出 控制台输出

可以看到在1s后更改state.age的时候触发了trigger函数,并且打印了state对象,age属性,新的值和旧的值。

接下来直接去触发key的所有effect就可以了。

js
// reactiveEffect.js
import { activeEffect, trackEffect, triggerEffects } from './effect'

/** 依赖收集表 */
const targetMap = new WeakMap()

export const createDep = (cleanUp, name) => {
    const dep = new Map()
    dep.cleanUp = cleanUp
    dep.name = name
    return dep
}

export function track(target, key) {
    // 没有 activeEffect,说明不是在effect中使用这个key的,不需要收集依赖
    if (!activeEffect) return

    // 如果对象没有被track过,则创建一个依赖收集表
    if (!targetMap.has(target)) {
        targetMap.set(target, new Map())
    }

    // 获取对象的依赖收集表
    const depsMap = targetMap.get(target)

    // 如果对象的依赖收集表中没有这个key,则创建一个依赖收集表
    if (!depsMap.has(key)) {
        depsMap.set(
            key,
            createDep(() => depsMap.delete(key), key)
        )
    }

    // 获取key的依赖收集表
    const dep = depsMap.get(key)

    trackEffect(activeEffect, dep)
}

export function trigger(target, key, newValue, oldValue) {

    // 获取对象的依赖收集表
    const depsMap = targetMap.get(target)

    // 没有依赖收集表,说明没有track过这个对象,直接返回
    if (!depsMap) return

    // 获取key的依赖收集表
    const dep = depsMap.get(key)

    // 没有依赖收集表,说明这个key没有对应的effect,直接返回
    if (!dep) return

    // 触发这个key的依赖收集表中的所有effect
    triggerEffects(dep)
}

// effect.js

/** 
 * 实现一个effect函数
 * 创建一个响应式的effect
 * 数据变化后可以重新执行
 */
export function effect(fn, options) {
    // 创建一个effect,只要依赖的属性变化了就要执行回调

    const _effect = new ReactiveEffect(fn, () => {
        // scheduler 调度器,用于控制effect的执行时机
        _effect.run()
    })
    _effect.run()
}

export let activeEffect = null

class ReactiveEffect {
    /** 记录当前effect执行的次数 */
    _trackId = 0
    /** 依赖收集表 */
    deps = []
    /** 依赖收集表的长度 */
    _depsLength = 0

    // 判断当前effect是否是响应式的
    // 默认激活状态
    active = true

    // fn 用户传入的回调函数
    // scheduler 调度器,用于控制effect的执行时机
    // 如果fn中依赖的数据变化后,需要重新调用 run
    constructor(fn, scheduler) {
        this.fn = fn
        this.scheduler = scheduler
    }

    run() {
        // 不是激活状态,直接执行
        if (!this.active) {
            return this.fn()
        }

        let lastEffect = activeEffect
        try {
            activeEffect = this
            return this.fn()
        } finally {
            activeEffect = lastEffect
        }
    }
}

export function trackEffect(effect, dep) {
    dep.set(effect, effect._trackId)
    effect.deps[effect._depsLength++] = dep
}

export function triggerEffects(dep) {
    // 遍历依赖收集表,执行effect
    for (const effect of dep.keys()) {
        effect?.scheduler()
    }
}

页面效果 页面效果

1s后

页面效果

到这里,一个基础的响应式系统就完成了。