Vue响应式原理推导过程
1. 最基础的响应式模型
最初的响应式模型非常简单,就是手动实现函数与对象的绑定关系:
/**
* 阶段一:函数与对象的绑定
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
* 缺点:
* 1.函数需要在对象属性修改后重新手动调用
* 2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
* 3.多对象依赖绑定,某一对象发生变化,所有对象依赖函数都要调用
* 4.函数需要一个一个手动调用
*/
const obj={
name:'mike',
age:18
}
function foo(){
console.log(obj.name)
console.log(obj.age)
}
//初始调用
foo() //mike 18
//修改对象属性
obj.name='jack'
//重新调用
foo() //jack 18
存在的问题:
- 需要手动调用函数
- 对象属性发生变化时,所有函数都要手动重新调用
- 多对象依赖混乱
- 每个函数需要单独调用
优势:
- 概念简单,容易理解
- 实现方式直接明了
- 不需要额外的监听机制,代码量少
2. 集中管理响应函数
改进方向是使用数组收集需要响应的函数,方便统一管理和调用:
/**
* 阶段二:函数与对象的绑定
*
* 改变:1.新增收集函数
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
* 缺点:
* 1.函数需要在对象属性修改后重新手动调用
* 2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
* 3.多对象依赖绑定,某一对象发生变化,所有对象依赖函数都要调用
*
* 优点:
* 1.函数可以一起调用
*/
const obj={
name:'mike',
age:18
}
//需要响应式的函数都存入该数组里面
const reactiveFns=[]
//收集响应式函数
function watchFn(fn){
reactiveFns.push(fn)
}
function foo(){
console.log('first:'+obj.name)
console.log('first:'+obj.age)
}
//调用收集函数(第一种调用方法,foo仍然可以手动调用)
watchFn(foo)
//调用收集函数(第二种调用方法,需使用数组才可手动调用)
watchFn(function(){
console.log('second:'+obj.name)
console.log('second:'+obj.age)
})
reactiveFns.forEach(fn=>{
fn()
})
// first:mike
// first:18
// second:mike
// second:18
obj.name='jack'
reactiveFns.forEach(fn=>{
fn()
})
// first:jack
// first18
// second:jack
// second:18
改进点:
- 函数可以统一调用
- 响应式函数集中管理
仍存在的问题:
- 属性修改后需要手动触发函数调用
- 不同对象的依赖函数混在一起
优势:
- 统一管理所有需要响应的函数
- 批量执行响应函数,减少重复代码
- 可以动态添加新的响应函数
3. 使用Depend类管理依赖
为了更好地组织代码,引入Depend
类来管理依赖关系:
/**
* 阶段三:函数与对象的绑定
*
* 改变:
* 1.新增收集函数
* 2.新增类Depend
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
* 缺点:
* 1.函数需要在对象属性修改后重新手动调用
* 2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
*
*
* 优点:
* 1.函数可以一起调用
* 2.对象可以分类管理
*/
class Depend{
constructor(){
this.reactiveFns=[]
}
//收集依赖函数
addDepend(fn){
this.reactiveFns.push(fn)
}
//调用依赖函数
notify(){
this.reactiveFns.forEach(fn=>{
fn()
})
}
}
// ========= 分类管理 obj =============
const obj={
name:'mike',
age:18
}
const dep=new Depend()
dep.addDepend(function(){
console.log('second:'+obj.name)
console.log('second:'+obj.age)
})
dep.notify()
// second:mike
// second:18
obj.name='jack'
dep.notify()
// second:jack
// second:18
改进点:
- 对象依赖可以分类管理
- 代码结构更加清晰
优势:
- 面向对象的设计,更加结构化
- 可以为不同对象创建独立的依赖管理
- 封装了依赖收集和通知逻辑,使用更加灵活
- 为后续扩展提供了基础设施
4. 使用Object.defineProperty实现自动响应
通过Object.defineProperty
监听对象的属性变化,实现自动响应:
/**
* 阶段四:函数与对象的绑定
*
* 改变:
* 1.新增收集函数
* 2.新增类Depend
* 3.使用Object.defineProperty监听对象属性====>vue2
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
* 缺点:
* 1.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
*
*
* 优点:
* 1.函数可以一起调用
* 2.对象可以分类管理
* 3.属性被监听,属性修改时,函数自动调用
*/
class Depend{
constructor(){
this.reactiveFns=[]
}
//收集依赖函数
addDepend(fn){
this.reactiveFns.push(fn)
}
//调用依赖函数
notify(){
this.reactiveFns.forEach(fn=>{
fn()
})
}
}
// ========= 分类管理 obj =============
const obj={
name:'mike',
age:18
}
const dep=new Depend()
dep.addDepend(function(){
console.log('second:'+obj.name)
console.log('second:'+obj.age)
})
Object.keys(obj).forEach(key=>{
let value=obj[key]
Object.defineProperty(obj,key,{
set:(newValue)=>{
value=newValue
dep.notify()
},
get:()=>{
return value
}
})
})
dep.notify()
// second:mike
// second:18
obj.name='jack'//监听修改后调用dep.notify()
// second:jack
// second:18
改进点:
- 属性被监听,修改时自动调用依赖函数
- 无需手动调用notify()
优势:
- 实现了真正的响应式,属性变化自动触发更新
- 简化了使用流程,无需手动监听属性变化
- 提供了更加透明的使用体验
- 在底层实现变化监听,应用代码更加纯净
5. 完善依赖收集与精确响应
为每个对象的每个属性创建独立的依赖收集器,实现更精确的响应:
/**
* 阶段五:函数与对象的绑定
*
* 改变:
* 1.新增收集函数
* 2.新增类Depend
* 3.使用Object.defineProperty监听对象属性====>vue2
* 4.新增map结构封装函数,给每个对象和每个属性设置对应的dep实例
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
*
*
* 优点:
* 1.函数可以一起调用
* 2.对象可以分类管理
* 3.属性被监听,属性修改时,函数自动调用
* 4.对象属性之间减少依赖关系,独立调用对应函数
*/
class Depend{
constructor(){
this.reactiveFns=new Set() //避免重复添加依赖函数
}
//收集依赖函数
addDepend(fn){
this.reactiveFns.add(fn)
}
//调用依赖函数
notify(){
this.reactiveFns.forEach(fn=>{
fn()
})
}
}
//存储响应式函数
let reactiveFn=null
function watchFn(fn){
reactiveFn=fn
fn() //激活监听响应
}
//对象map源头,弱引用weakMap
const objMap=new WeakMap()
//map结构的封装函数(分配dep实例)
function getDepend(obj,key){
//1.根据源头map,找到obj对应的map
let map=objMap.get(obj)
//若map不存在
if(!map){
map=new Map()
objMap.set(obj,map)
}
//2.根据obj的map对象,找到key对应的depend对象(有一个选择,如果key也是一个对象,是否需要深层响应)
let dep=map.get(key)
//若dep不存在
if(!dep){
dep=new Depend()
map.set(key,dep)
}
return dep
}
//添加响应式函数
function reactive(obj){
Object.keys(obj).forEach(key=>{
let value=obj[key]
Object.defineProperty(obj,key,{
set:(newValue)=>{
value=newValue
const dep=getDepend(obj,key)
dep.notify()
},
get:()=>{
const dep=getDepend(obj,key)
//防止重复添加依赖函数
dep.addDepend(reactiveFn)
return value
}
})
})
return obj
}
改进点:
- 对象属性与依赖函数的关系更加精确
- 只触发依赖特定属性的函数
- 自动收集依赖关系
优势:
- 细粒度的依赖收集,只有真正依赖某属性的函数才会被触发
- 使用WeakMap避免内存泄漏问题
- 使用Set避免重复添加依赖函数
- 自动在属性获取时收集依赖,无需手动指定依赖关系
- 提供了完整的reactive函数,封装了响应式逻辑
6. 使用Proxy实现全面的响应式
最终使用ES6的Proxy
替代Object.defineProperty
,实现更强大的响应式系统:
/**
* 阶段六:函数与对象的绑定
*
* 改变:
* 1.新增收集函数
* 2.新增类Depend
* 3.使用Proxy代理对象属性====>vue3
* 4.新增map结构封装函数,给每个对象和每个属性设置对应的dep实例
*
* 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
*
*
*
*
* 优点:
* 1.函数可以一起调用
* 2.对象可以分类管理
* 3.属性被监听,属性修改时,函数自动调用
* 4.对象属性之间减少依赖关系,独立调用对应函数
*/
//添加响应式函数
function reactive(obj){
const objProxy=new Proxy(obj,{
set(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver)
const dep=getDepend(target,key)
dep.notify()
},
get(target,key,receiver){
const dep=getDepend(target,key)
dep.addDepend(reactiveFn)
return Reflect.get(target,key,receiver)
}
})
return objProxy
}
Proxy的优势:
- 可以监听动态添加的属性
- 可以监听数组的变化
- 可以监听更多种类的操作(不仅限于get/set)
- 性能更好,不需要递归遍历对象的所有属性
- 返回的是一个新对象,不会修改原始对象
- 可以拦截更多的操作,如删除属性、检查属性是否存在等
总结
JavaScript响应式原理的演进经历了以下几个阶段:
- 最基础的手动绑定与调用
- 集中管理响应函数
- 使用Depend类组织依赖关系
- Object.defineProperty实现自动响应
- 完善依赖收集与精确响应
- 使用Proxy实现全面的响应式系统