Vue响应式原理推导过程

0 阅读8分钟

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的优势:

  1. 可以监听动态添加的属性
  2. 可以监听数组的变化
  3. 可以监听更多种类的操作(不仅限于get/set)
  4. 性能更好,不需要递归遍历对象的所有属性
  5. 返回的是一个新对象,不会修改原始对象
  6. 可以拦截更多的操作,如删除属性、检查属性是否存在等

总结

JavaScript响应式原理的演进经历了以下几个阶段:

  1. 最基础的手动绑定与调用
  2. 集中管理响应函数
  3. 使用Depend类组织依赖关系
  4. Object.defineProperty实现自动响应
  5. 完善依赖收集与精确响应
  6. 使用Proxy实现全面的响应式系统