Vue 响应式系统的发展历程

149 阅读5分钟

Vue 响应式的发展历程

Vue 的响应式系统经历了显著的演进,从早期基于 Object.defineProperty 的实现到如今 Vue 3 中采用 ES6 的 Proxy 对象。这一转变不仅提升了系统的灵活性和性能,还简化了开发者的使用体验。接下来,我们将深入探讨这两种技术的实现过程及其优缺点,并通过具体的示例代码来具体说明。


Object.defineProperty

实现过程

  1. 声明数据:创建一个普通对象,包含需要响应式的属性。
  2. 包装数据成为响应式对象的属性:将这些属性转换为响应式属性。
  3. 定义属性 Object.defineProperty() :使用 Object.defineProperty 定义 getter 和 setter 方法,确保每次访问或修改属性时都能触发相应的逻辑。
  4. 进行 get、set 方法访问、修改属性:当访问或修改这些属性时,触发相应的 getter 和 setter。
  5. 通过事件绑定,进行 DOM 的更新:通过事件监听器更新 DOM,确保视图与数据同步。

示例代码:

接下来我通过简单的点击事件来给大家描述一下它的实现过程。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>响应式API</title>
</head>
<body>
   <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>响应式API</title>
</head>
<body>
    <div id="container">1</div>
    <div id="count">2</div>
    <button id="button">点击+1</button>
    <button id="btn">点击*2</button>
    <script>
        // JSON对象, JSON数据
        var obj = {
            value:1, // 相当与count
            count:2 // 包装成为响应式对象
        }
        let value = 1;
        let count = 2; // 数据
        // 拦截器 修改值之前
        // 属性定义 定义一下value属性
        Object.defineProperty(obj,'value',{
           get:function(){
                console.log('读了value属性');
                return value; // 原来的职责
           },
           set:function(newValue){
                value = newValue; // 原来的职责
                document.getElementById('container').innerHTML = newValue;
           } 
        })
        // 属性定义 定义一下count属性
        Object.defineProperty(obj,'count',{
            get:function(){
                 console.log('读了count属性');
                 return count; // 原来的职责
            },
            set:function(newCount){
                 count = newCount; // 原来的职责 数据改变了
                 document.getElementById('count').innerHTML = newCount;
            }
         })
        document.getElementById('button')
            .addEventListener('click',function(){
               obj.value++;
            })

        document.getElementById('btn')
           .addEventListener('click',function(){
               obj.count *= 2;
            })
    </script>
</body>
</html>

ezgif-4-dcfab98fa6.gif 很明显这里对不同属性进行访问、修改操作的时候,都要进行Object.definedProperty()相同的操作。同时我们应该注意进行set操作时对内部属性的更新(示例中value = newValue; count = newCount; )要不然每次执行更新时都是访问以前的旧值,如下。

ezgif-4-08f33d6147.gif

Proxy代理对象

Proxy是vue3响应式API(ref)的底层实现原理之一。

  • ref

    • 在 Vue 3 中,ref 是一个函数,它接收一个内部值并返回一个带有 .value 属性的对象。这个对象是通过 Proxy 实现的响应式对象。
    • 当你使用 ref 包装一个值时,Vue 会创建一个 Proxy 来拦截对该值的访问和修改,确保任何对该值的操作都能触发视图更新。
  • reactive

    • reactive 函数接收一个普通对象并返回一个响应式代理对象,内部也是基于 Proxy 实现的。
    • 它允许你将整个对象变成响应式的,而不仅仅是单个属性。

实现过程

  1. 定义 watch 函数:创建一个函数用于初始化代理对象。
  2. 创建代理对象:使用 Proxy 包装目标对象。
  3. 暴露 watch 函数:使 watch 函数可以在全局范围内使用。
  4. 初始化数据对象:定义初始数据结构。
  5. 创建响应式对象:通过 watch 函数将普通对象转换为响应式对象。
  6. 添加事件监听器:为按钮添加点击事件监听器,触发对代理对象的操作。
  • 优点:
    • 全面监听。
    • 灵活性强。

示例代码:

根据上述示例修改,使用Proxy代理对象实现。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Proxy</title>
</head>
<body>
    <div id="container">1</div>
    <div id="count">2</div>
    <button id="cutton">+1</button>
    <button id="btn">*2</button>
    <script>
        // 匿名函数->立即执行函数 + 回调函数(事件处理函数、定时器、文件操作)
        (function(){
            // 函数作用域
            // 设计模式 观察者模式
            function watch(target,func){
                // es6  proxy 实例化代理对象
                const proxy = new Proxy(target,{
                    get:function(target,prop){
                        console.log(`读了${target}${prop}属性`);
                        console.log(target[prop]);
                        return target[prop];
                     },
                 // get获取修改前的值,javascript 引擎会执行运算,原值修改后,
                 // 试图将新值赋给newobj的时候,就会执行set 此时newValue就是修改后的值 
                    set:function(target,prop,newValue){
                        console.log(`修改了${target}${prop}属性`);
                        target[prop] = newValue;
                        func(prop,newValue);
                    }
                })
                return proxy;
            }
            // 暴露全局
            this.watch = watch;
        })()
    let obj = {
        value:1,
        count:2
    }
    let obj2 = {
       
    }
    // 代理对象
    var newObj = watch(obj,function(key,newValue){
        if(key==='value'){
            document.getElementById('container').innerHTML = newValue;
        }
        if(key==='count'){
            document.getElementById('count').innerHTML = newValue;
        }
    })
    // 事件处理函数
    document.getElementById('cutton')
           .addEventListener('click',function(){
            // 交给代理对象去处理
                newObj.value++;
           })

    document.getElementById('btn')
          .addEventListener('click',function(){
            // 交给代理对象去处理
                newObj.count *= 2;
           })   
    </script>
</body>
</html>

newObj是一个由Proxy构造函数创建的对象,它包装了原始的对象obj。当尝试修改newObj.count时,实际上是在与Proxy对象交互,而不是直接与obj交互。

  • 执行过程描述

当用户点击按钮时,事件处理函数被触发,进而调用 newObj.value++newObj.count *= 2。此时:

  1. get 捕获器触发:尝试读取 newObj.value 或 newObj.count 触发 Proxy 的 get 捕获器,控制台输出相应信息,并返回当前属性值。
  2. 计算新值:JavaScript 引擎计算表达式的结果。
  3. set 捕获器触发:尝试设置新的属性值,触发 Proxy 的 set 捕获器,控制台输出相应信息,更新属性值,并通过回调函数更新DOM。

总结

Vue 的响应式系统在 Vue 3 中借助 Proxy 对象实现了更强大和灵活的功能,解决了 Object.defineProperty 存在的局限性。Proxy 不仅可以全面监听属性变化,还能轻松处理动态属性和复杂的数据结构。通过上述代码示例,我们可以清晰地看到两种方式的具体实现及其差异。对于现代应用开发而言,Proxy 提供了一个更加优雅和高效的解决方案。z