深入解析Vue2和Vue3响应式原理

Vue 框架凭借其简洁的响应式系统在前端开发中获得了广泛的应用。从 Vue 2 到 Vue 3,响应式系统的实现方式经历了重要的变化,Vue 3 引入了 ProxyReflect,取代了 Vue 2 中基于 Object.defineProperty 的数据劫持机制。这篇文章将详细解读 Vue2 和 Vue3 响应式原理的不同,结合代码示例,帮助读者理解这些变化带来的优势与改进。

Vue2 响应式原理

在 Vue2 中,响应式系统的核心机制是基于 Object.defineProperty 的数据劫持。虽然 Vue2 实现了对对象属性的响应式追踪,但在处理数组时,Vue2 的响应式机制有一定的局限性,比如直接修改数组的值不会触发响应式更新。

首先,来看一个简单的 Vue2 示例,它展示了 Vue2 在数组劫持方面的局限性:

在上面的代码中,当你点击列表时,this.list[0] = 2 并不会触发视图的更新。为什么直接修改数组的值不会触发响应式?Vue2 只能通过修改数组的方法(例如 pushpop)或使用 this.$set 添加新属性来触发响应式更新。

手撸响应式

在 Vue2 中,响应式系统是通过 Object.defineProperty 对对象的每个属性进行数据劫持(getter 和 setter)来实现的。但对于数组,Vue 并不会对每个元素进行单独的 get/set 拦截。Vue 只能通过重写数组的原型方法(例如 pushpop 等)来检测数组的变化。因此,当你直接通过索引修改数组的某个元素时,Vue 无法监听到这个变化。

以下是简单模拟 Vue2 中数据劫持的原理代码:

js 复制代码
let  obj = {
    a:1,
    b:2,
    c :{
        n:3
    },
    d:['1','2','3']
}
function updateView() {
    console.log('更新视图');
}
    let value = obj.a
    // 数据劫持
    Object.defineProperty(obj,'a',{
        get(){ // 取值
            return value;
            // return obj.a  会无限递归,死循环
        },
        set(val){ // 设置值
            value = val
            //  obj.a = val 同样也不能
            updateView()
        }
    })

    obj.a = 100
   
   console.log(obj.a);
   
 

在这个例子中,Object.defineProperty 通过 getset 方法来拦截对属性的访问和修改。

在这里 a 被拦截了,所以对 a 的修改可以更新视图。

在看看下面的示例

js 复制代码
let  obj = {
    a:1,
    b:2,
    c :{
        n:3
    },
    d:['1','2','3']
}
function updateView() {
    console.log('更新视图');
}
    let value = obj.c
    // 数据劫持
    Object.defineProperty(obj,'c',{
        get(){ // 取值
            return value;
            
        },
        set(val){ // 设置值
            value = val
            //  obj.a = val 同样也不能
            updateView()
        }
    })

    obj.c.n = 4
   
   console.log(obj.c);
   

会发现没有触发更新视图,但是 c 这个属性的值又修改了,说明 Object.defineProperty() 还是不能去劫持为对象的属性。

因此我们需要遍历整个对象的每个属性,并通过 Object.defineProperty() 为其添加 getter 和 setter。这个过程的递归遍历,确保了对象中嵌套的所有层级都能被劫持,并且可以响应状态变化。

Vue 的响应式系统的核心是一个递归函数,负责观察对象的每个属性。如果对象中的某个属性本身也是对象,则继续递归观察它的子属性。这个函数通常被称为 observer(),其目的是确保对象的每一层次都能响应数据变化。

js 复制代码
let  obj = {
    a:1,
    b:2,
    c :{
        n:3
    },
    d:['1','2','3']
}

function observer(target){
   
        for(let key in target){
            defineReactive(target,key,target[key])
        }
}

function defineReactive(target,key,value) {
    if(typeof value === 'object' && value !==null){
        observer(value)
    }
    Object.defineProperty(target,key,{
        get(){ // 取值
            return value;
            
        },
        set(newVal){ // 设置值
          if(newVal !== value){
                value = newVal
                updateView()
          }
        }
    })
}

observer(obj)

为对象的属性这种情况处理完了,那回到最开始的,当属性类型为数组时,Vue2 使用了另一种策略:重写数组原型上的方法,例如 pushpop 等方法来实现对数组的响应式处理。

js 复制代码
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)

Array.from(['push','shift','pop','unshift']).forEach((method) =>{
    // 函数劫持,重写函数
    proto[method] = function(){
        oldArrayPrototype[method].call(this,...arguments)
        updateView()
    }
})

function observer(target){
    if(Array.isArray(target)){
        target.__proto__ = proto  // 重写数组原型
        return
    }
        for(let key in target){
            defineReactive(target,key,target[key])
        }
}

这里主要的改动就是,去新创建数组的响应式原型对象,创建一个新的原型对象去存放数组的原型对象,我们后续对将重写的数组方法都放在了这个新对象上,避免修改了数组的原型对象属性。

然后就是重写 proto对象中的这些数组方法,每次调用时都触发 updateView()

完整代码:

js 复制代码
let  obj = {
    a:1,
    b:2,
    c :{
        n:3
    },
    d:['1','2','3']
}


let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)

Array.from(['push','shift','pop','unshift']).forEach((method) =>{
    // 函数劫持,重写函数
    proto[method] = function(){
        oldArrayPrototype[method].call(this,...arguments)
        updateView()
    }
})

function updateView() {
    console.log('更新视图');
}

// 观察者
function observer(target){
    if(Array.isArray(target)){
        target.__proto__ = proto  // 重写数组原型
        return
    }
        for(let key in target){
            defineReactive(target,key,target[key])
        }
}

function defineReactive(target,key,value) {
    if(typeof value === 'object' && value !==null){
        observer(value)
    }
    Object.defineProperty(target,key,{
        get(){ // 取值
            return value;
            
        },
        set(newVal){ // 设置值
          if(newVal !== value){
                value = newVal
                updateView()
          }
        }
    })
}

observer(obj)

 obj.d.push('4')

到这里我们已经实现了一个简易的Vue2 响应式原理。

总结

主要是完成了一下三点:

  • 重写数组方法: Vue2 通过重写数组的常用方法,如 pushpopshiftunshift 等,使这些方法在执行后可以触发 updateView 函数,更新视图。这种方式并没有对数组的每个元素进行劫持,只是对数组整体操作进行了拦截。

  • 递归劫持对象: 对于对象,Vue2 使用递归的方式来劫持每个属性,尤其是深层嵌套的对象。对于数组中的对象元素,也可以通过递归的方式进行劫持。

  • 响应式视图更新: 当对象或数组发生变化时,updateView 函数会被调用,模拟视图的更新过程。这在 Vue2 中是通过虚拟 DOM 和 diff 算法完成的。

Vue3 响应式原理

首先,我们来看一个简单的 Vue 3 响应式示例,通过 Vue 3 的 reactive API 将数据对象 message 变成响应式的,并且在点击标题 <h2> 时修改 message.msg 的值,来实现数据的双向绑定。

Vue 3 中的 reactive 函数用于将普通的 JavaScript 对象变成 响应式对象 。当我们对 reactive 包装的对象进行修改时,Vue 会自动检测到变化,并更新视图。 再这个示例中,message 是一个通过 reactive 包装的对象,当我们点击 <h2> 标签时,message.msg 被修改,Vue 通过响应式系统自动重新渲染视图。

Vue 3 的响应式系统主要基于 JavaScript 的 Proxy API 来实现。Proxy 可以拦截对对象的操作(如 getset),从而为 Vue 实现自动追踪对象的变化并触发视图更新。

Proxy

Proxy 是 ES6 引入的新特性,允许你创建一个代理对象,这个对象可以拦截并定义对原始对象的基本操作(例如属性读取、设置、删除等)。Proxy 可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy详细API可参考Proxy - ECMAScript 6入门 (ruanyifeng.com)

Reflect

此外,还会用到 ReflectReflect 是与 Proxy 一同引入的,它提供了一些与对象操作相关的静态方法,比如 Reflect.get()Reflect.set()。这些方法和 Proxy 捕获器中的行为一致。使用 Reflect 可以避免我们手动调用目标对象的方法,使代码更简洁和安全。Proxy - ECMAScript 6入门 (ruanyifeng.com)

手撸Vue3 响应式原理

现在我们了解了 ProxyReflect 的基础知识,接下来可以开始实现一个简化版的响应式系统。Vue 3 的响应式系统核心思想是:通过 Proxy 拦截对象的属性读取和修改,动态跟踪依赖关系,在数据变化时更新视图。

js 复制代码
function isObject(val){
    return typeof val === 'object' && val !== null 
}

function reactive(target){
    return createReactiveObject(target)
}

function createReactiveObject(target){
        if(!isObject(target)) {
            return target
        }

        let baseHandler = {
            get(target , key , receiver) {
                console.log("读取");
                
                let result =   Reflect.get(target,key)   // target[key]  // 不会无限递归
                return isObject(result) ? reactive(result) : result  // 按需递归
            },
            set(target, key , value , receiver) {
                console.log("修改");
                
               let result =   Reflect.set(target, key , value ,receiver)  // 将target 中的key值修改为value
               return isObject(result) ? reactive(result) : result
            }
        }

       // 对象代理
        let observed = new Proxy(target,baseHandler)
        return observed
}




let obj = {
    a: 1,
    b: 2,
    c: {
        n: 3
    },
    d: ['a','b','c']
}

let proxy = reactive(obj)

因为 Proxy 功能很强大,可以支持13种拦截操作,所以我们也更简单的去实现了Vue3的响应式原理,

isObject 函数用于判断 target 是否是对象,只有对象才能被代理。如果传入的不是对象(如基本数据类型),我们直接返回原始值。

接下来就是核心------创建 Proxy 拦截器,

js 复制代码
let baseHandler = {
    get(target, key, receiver) {
        console.log("读取");

        let result = Reflect.get(target, key) // target[key]
        return isObject(result) ? reactive(result) : result  // 按需递归
    },
    set(target, key, value, receiver) {
        console.log("修改");

        let result = Reflect.set(target, key, value, receiver) // target[key] = value
        return isObject(result) ? reactive(result) : result
    }
}

这里我们主要定义了 读和改 两个拦截操作:

get :当读取对象的某个属性时,执行 Reflect.get(target, key) 来获取属性值。如果这个值是对象类型,则递归调用 reactive,将其转换为响应式对象。将递归放进在 get 中,实现了按需递归,只有用到的为对象的属性,才会去递归,优化了一部分性能。

set :当修改对象的某个属性时,执行 Reflect.set(target, key, value) 来完成属性的修改。如果新的值是对象类型,同样递归转换为响应式对象。

仅仅是这样还不够,因为我们还漏掉了两种情况,一个是如果原对象已经被代理了,另一个是给被代理的对象代理。无论是哪种都是很浪费性能。因此我们引入了 WeakMapWeakSet 去处理。

WeakMap 只接受对象(null除外)和作为键名,不接受其他类型的值作为键名,且一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

WeakSet 的成员只能是对象和 Symbol 值,而不能是其他类型的值。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

代码示例

js 复制代码
let toProxy = new WeakMap()  // 原对象 : 代理对象
let toRow = new WeakSet() // 对象代理


function isObject(val){
    return typeof val === 'object' && val !== null 
}

function reactive(target){
    return createReactiveObject(target)
}

function createReactiveObject(target){
        if(!isObject(target)) {
            return target
        }

        // 防止原对象被多次代理
        let proxy = toProxy.get(target)
        if(proxy) {
            return proxy
        }
        // 防止被代理后的对象,再次被代理
        if(toRow.has(target)){
            return target;
        }

        let baseHandler = {
            get(target , key , receiver) {
                console.log("读取");
                
                let result =   Reflect.get(target,key)   // target[key]  // 不会无限递归
                return isObject(result) ? reactive(result) : result  // 按需递归
            },
            set(target, key , value , receiver) {
                console.log("修改");
                
               let result =   Reflect.set(target, key , value ,receiver)  // 将target 中的key值修改为value
               return isObject(result) ? reactive(result) : result
            }
        }

       // 对象代理
        let observed = new Proxy(target,baseHandler)
        toProxy.set(target,observed)
        toRow.add(observed,target)
        return observed
}




let obj = {
    a: 1,
    b: 2,
    c: {
        n: 3
    },
    d: ['a','b','c']
}

let proxy = reactive(obj)

    proxy.e = 8

首先是通过传入的 target 作为键名去 toProxy 查找有无对应的值,如果又就代表 target 之前被代理过了,直接返回toProxy值就好,每次代理完则 以 { target: observed } 存入 WeakMap 中。然后再去查找 WeakSet 中有无 target,有则代表 target 已经是被代理过的对象,无需代理,同样每次代理后也要将 targetobserved 存入。

总结

Vue2

Vue 2 的响应式系统主要基于 Object.defineProperty ,会遍历每个对象的属性,使用 Object.defineProperty 对其进行数据劫持,定义 gettersetter。 当我们读取或修改数据时,会触发 getset,Vue 通过这些拦截来记录依赖(组件、模板中的绑定),当数据发生变化时,触发依赖更新视图。局限性无法监听数组的变化Object.defineProperty 只能对对象属性进行劫持,无法直接监听数组的变动。Vue 2 通过重写数组方法 (如 pushpop 等)来实现对数组变化的检测。不能监测对象属性的新增或删除 :Vue 2 无法动态监听对象新加的属性或删除的属性,必须使用 Vue.setVue.delete 来手动触发响应式。

Vue3

Vue 3 的响应式系统则完全基于 Proxy,它能够拦截几乎所有的操作,解决了 Vue 2 的局限性。Vue 3 使用 Proxy 来代理整个对象,拦截对象的各种操作(包括属性读取、修改、删除、添加等),还避免 Vue 2 中的深度递归监听,同时减少不必要的依赖收集和更新,按需递归。 Proxy 能直接监听数组的索引变化和方法调用(如 pushpop 等),不需要手动重写数组方法。

Vue 3 的响应式系统通过 Proxy 实现了更高的灵活性和更少的限制,使得框架的整体性能更高,开发体验更好。如果文章对你有帮助,可以点个赞哦😊!

相关推荐
码农爱java1 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
开心工作室_kaic2 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育2 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博2 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
isSamle2 小时前
使用Vue+Django开发的旅游路书应用
前端·vue.js·django
Jiude2 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试
ss2733 小时前
基于Springboot + vue实现的汽车资讯网站
vue.js·spring boot·后端
武昌库里写JAVA3 小时前
浅谈怎样系统的准备前端面试
数据结构·vue.js·spring boot·算法·课程设计
TttHhhYy4 小时前
uniapp+vue开发app,蓝牙连接,蓝牙接收文件保存到手机特定文件夹,从手机特定目录(可自定义),读取文件内容,这篇首先说如何读取,手机目录如何寻找
开发语言·前端·javascript·vue.js·uni-app
CodeChampion6 小时前
61.基于SpringBoot + Vue实现的前后端分离-在线动漫信息平台(项目+论文)
java·vue.js·spring boot·后端·node.js·maven·idea