手写 effect
基础的effect
/**
* 实现一个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()
}
}示例:
<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关联起来
<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.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应该就没有了
故做如下修改
/**
* 实现一个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执行的时候,activeEffect为null
这样会有一个bug,当effect进行嵌套调用的时候,activeEffect会丢失
解决嵌套调用的问题
例:
<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执行完成后,activeEffect为null,导致第一个effect中读取state.age的时候,activeEffect为null
所以,在每一次effect执行的时候,我们需要保存上一次的effect,在执行完成后,再恢复
如下:
/**
* 实现一个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的嵌套执行了
依赖收集
依赖收集表的结构如下:
{
{name: '小李大人',age: 18}: {
name: {effect: effect._trackId, effect: effect._trackId, ...},
age: {effect: effect._trackId, effect: effect._trackId, ...}
}
}整个依赖收集表都是一个WeakMap,键是target,值是一个Map, Map的键是target的key,值是一个Map, Map的是键是effect,值是effect的_trackId, 这样就可以实现一个target对应多个key,一个key对应多个effect了
每一个effect中,都会有一个deps数组,用于存储当前effect依赖的属性,形成一个双向依赖关系
<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>// 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中删除。
触发更新
在reactive的set方法被调用的时候去判断新旧的值是否发生变化,如果发生变化,则触发更新。
// 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就可以了。
// 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后

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