前置涩话
掘金的小伙伴们大家好,这里是家里蹲选手sin~ 拖延症真是太严重啦,这个debuff真的是伴随一生啊(〝▼皿▼) 每天都是哄着自己放下手机起来学一点是一点。也是坚持了五天啦~ 浅浅夸一下自己吧(^-^)V sin太棒啦!那今天就带来比较烧脑的知识,vue3响应式原理的简单实现。١١(❛ᴗ❛)冲冲冲!!
✅ vue3响应式原理的简单实现
数据劫持
首先我们知道,vue2是使用Object.defineProperty()对数据进行劫持。Object.defineProperty()是对象的内部基本方法之一(操作的是对象的属性)。而且有一个缺点,对于通过下标修改数组或给对象新增属性时没有进行劫持,因为数组的增删会改变下标,劫持要重新遍历数组,这样造成的性能代价与用户收益不成正比。而vue3使用proxy实现数据响应式,proxy是对象所有内部基本方法的拦截器(直接操作对象)。可以捕获到对象或数组的所有改变,proxy并不能监听内部深层次的对象变化,而vue3处理方式是在getter中递归响应式,这样的好处就是真正访问到的内部对象才会变成响应式。
实现reactive
import {track, trigger} from './effect.js'
const isObject = (target) => target !== null&&typeof target == 'object'
export const reactive = (target) => {
return new Proxy(target, {
get(target,key,receiver){
let res = Reflect.get(target,key,receiver)
track(target, key)
if(isObject(res)){
return reactive(res)
}
return res
},
set(target,key,value,receiver){
let res = Reflect.set(target,key,value,receiver)
trigger(target, key)
return res
}
})
}
实现 effect track trigger(响应式核心)
effect函数(副作用函数)
let activeEffect
export const effect = (fn) => {
const _effect = function() {
activeEffect = _effect // 设置当前激活的副作用
fn() // 执行原始函数
}
_effect() // 立即执行一次
}
-
功能:创建响应式副作用
-
核心流程:
-
定义内部函数
_effect -
执行
_effect时:- 将自身设为
activeEffect(当前激活的副作用) - 执行传入的回调函数
fn
- 将自身设为
-
立即执行一次
_effect进行依赖收集
-
-
关键点:
activeEffect是全局变量,存储当前正在执行的副作用- 当响应式数据被访问时,会将
activeEffect收集为依赖
track函数(依赖追踪)
const targetMap = new WeakMap()
export const track = (target, key) => {
let depsMap = targetMap.get(target)
if(!depsMap){
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps){
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect) //收集当前副作用
}
- 数据结构:
graph LR
targetMap --> |键:响应式对象| target
targetMap --> |值:Map| depsMap
depsMap --> |键:属性名| key
depsMap --> |值:依赖Set集合| deps
deps --> |值:effect函数| effect1
deps --> effect2
-
核心流程:
- 从
targetMap中获取对象对应的依赖映射depsMap - 从
depsMap中获取属性对应的依赖集合deps - 将当前激活的副作用
activeEffect添加到依赖集合
- 从
-
作用:建立「响应式对象属性 → 副作用函数」的映射关系
trigger函数(触发更新)
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach(effect=>effect())
}
-
核心流程:
- 从
targetMap中获取对象对应的depsMap - 从
depsMap中获取属性对应的依赖集合deps - 遍历执行所有依赖的副作用函数
- 从
-
作用:当响应式数据变化时,触发所有相关的副作用函数重新执行
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const user = reactive({
name: '小林忽悠',
age: 23,
a: {
b: {
c: {
money: 1000000
}
}
}
})
effect(()=>{
document.querySelector('#app').innerText = `${user.name} - ${user.age} - ${user.a.b.c.money}`
})
setTimeout(()=>{
user.name = '大林忽悠'
user.age = 26
setTimeout(()=>{
user.a.b.c.money = 9999999
},1000)
},2000)
</script>
</body>
</html>
当前实现的局限性
- 缺少嵌套 effect 支持(Vue 3 使用 effect 栈解决)
- 未处理重复触发问题(需用调度队列)
- 缺少 cleanup 机制处理分支切换
- 没有处理数组等特殊对象
这个实现虽然简化,但完整展示了 Vue 3 响应式系统的核心思想:通过
effect注册副作用,通过track收集依赖,通过trigger触发更新。实际 Vue 3 源码中还有更多优化和安全处理。
Proxy为什么要配合Reflect使用?
同样有参数receiver,触发代理对象的劫持时保证正确的 this 上下文指向。
Proxy 中接受的 Receiver 形参表示代理对象本身或者继承于代理对象的对象。
Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。
你可以简单的将 Reflect.get(target, key, receiver) 理解成为 target[key].call(receiver),不过这是一段伪代码,但是这样你可能更好理解。