Vue reactive / effect
熟悉 Vue 3 的同学们应该知道,vue 3 有 reactive 和 effect 两个 API,
reactive 接收一个 Object 对象,返回一个通过 Proxy 包装后的响应式对象,effect 接收一个函数,可用于触发响应式,我们来看看下面的例子
import { reactive, effect } from 'vue'
const counter = reactive({
value: 1
})
let doubleCounter;
watchEffect(() => {
doubleCounter = counter.value++
console.log(doubleCounter)
})
counter.value++
如果执行上面的代码,你将会在控制台看到两次输出,一次是 2,一次是 4,那么我们就知道了 effect 的机制:
- 首先会执行一次函数,在每次
reactive对象进行setter操作时,也会执行函数
下面我们将动手来试试这个 reactive 和 effect
实现 reactive
通过上面的例子我们已经知道:reactive 接收一个对象,返回通过 Proxy 包装后的这个对象,那么我们就可以直接来写了:
// src/reactive.js
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
// 通过 Reflect.get 来获取对象的值
const res = Reflect.get(target, key)
return res
},
// foo.value = 10
set(target, key, value) {
// 通过 Reflect.set 来更新对象中某个属性的值
const res = Reflect.set(target, key, value)
return res
},
})
}
通过简单的测试:(这里使用 jest 作为测试框架),证明我们写的代码是没问题的
// src/tests/reactive.spec.js
import { reactive } from '../reactive'
describe('result', () => {
it('should ', () => {
const foo = {
value: 0,
}
const observe = reactive(foo)
// 测试包装后的对象和源对象不等
expect(observe).not.toBe(foo)
// 测试包装后的对象的某个属性的值和源对象相同
expect(observe.value).toBe(0)
})
})
实现 effect
查看需求
effect 就是本文章的最难点了,我们先统计一下需求:
- 立即执行一次传入的函数
- 当被
reactive包装后的对象执行setter时,触发 effect 传入的函数
下面,我们来写一个测试:
import { reactive } from '../reactive'
import { effect } from '../effect'
describe('result', () => {
it('should ', () => {
const foo = reactive({
value: 1,
})
let newFoo
effect(() => {
newFoo = foo.value * 10
})
// 首先会执行一次函数
expect(newFoo).toBe(10)
foo.value = 20
// 当进行 setter 的时候会再次执行一次函数
expect(newFoo).toBe(200)
})
})
当执行 setter 的时候,该如何知道自己需要执行哪些函数呢?下面,将是 effect 的两个最重要的功能:
- 依赖收集 【在 getter 的时候将与整个属性有关的依赖收集起来】
- 触发依赖【在 setter 的时候就触发所有有关的依赖】
+ import { track, trigger } from './effect'
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
+ track(target, key)
return res
},
// foo.value = 10
set(target, key, value) {
const res = Reflect.set(target, key, value)
+ trigger(target, key)
return res
},
})
}
写功能
// 我们先实现最简单的,先执行一次函数,将 effect 抽象一层
class ReactiveEffect {
constructor(fn) {
this._fn = fn
}
run() {
// 这一步是为了下面
activeEffect = this
this._fn()
}
}
let activeEffect
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
下面我们来写 track 函数
const targetMap = new Map()
export function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 将当前的依赖添加到 dep Set 中
dep.add(activeEffect)
}
targetMap、depsMap、dep 都是啥???看图
下面是 trigger 函数
// trigger 函数很简单,获取到当前 key 的所有依赖,并遍历执行
export function trigger(target, key) {
const depsMap = targetMap.get(target)
const dep = depsMap.get(key)
// set
for (const effect of dep) {
effect.run()
}
}
所有的 effect.js 代码
class ReactiveEffect {
private _fn: any
constructor(fn) {
this._fn = fn
}
run() {
activeEffect = this
this._fn()
}
}
const targetMap = new Map()
export function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
let activeEffect
export function trigger(target, key) {
const depsMap = targetMap.get(target)
const dep = depsMap.get(key)
// set
for (const effect of dep) {
effect.run()
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
经过测试成功,那么我们的最简版的 reactive 和 effect 就完成了
最后感谢 崔大(阿崔crx) 的 mini-vue ,感谢【催学社】学习社群,好的学习氛围可以提高学习效率,欢迎你加入一起学习一起成长!也感谢掘金,可以提供这样良好的技术社区一起交流