深入浅出:JavaScript 的 响应式 与 Proxy 代理,你了解吗? 🚀

226 阅读8分钟

image.png

引言

前端开发中,响应式是一个非常火热的话题。你一定听说过 Vue 和 React,它们在背后运用了强大的响应式技术,使得数据和界面的交互变得异常流畅。而作为 JavaScript 开发者,我们也可以不依赖框架,自己动手实现一个响应式系统,甚至可以通过一些技巧和 API 来构建自己的响应式机制。那么,今天我们就来聊聊 响应式 以及如何使用 Proxy 来实现这种机制,带着一些有趣的代码示例,你一定会玩得开心的!🎉


响应式:让数据变“聪明”起来 🤖

在我们进入正题之前,先来定义一下“响应式”到底是什么。响应式的核心思想是:当数据发生变化时,系统能够自动“感知”到这种变化,并作出响应。这种响应通常表现为界面的自动更新。例如,当你修改某个变量的值时,页面上的显示内容也会随之变化。是不是感觉像魔法一样?✨

1. Vue 的响应式实现原理 🦸‍♂️

Vue 是通过响应式设计让数据和视图之间的互动变得异常简便。当你修改了数据,Vue 会自动帮你更新视图。Vue 背后的原理其实非常简单,它利用了 gettersetter 来拦截对数据的访问和修改。Vue 会在你的数据属性上加上一层“魔法滤镜”,当你读写这些数据时,它能“感知”到变化,从而自动更新视图。

这里是一个 Vue 中的响应式代码示例:

let count = ref(0); // 响应式对象
count.value++; // 修改数据,视图会自动更新

在这个例子中,ref 是 Vue 提供的一个 API,它将 count 包装成一个响应式对象。每次你修改 count.value 时,Vue 会自动触发视图的更新。只要数据变化,页面就会随着变化,像施了魔法一样。✨

我们接下来的讨论的就是如何手动实现这样的效果,让我们也能开发一个自己的响应式

image.png

2. 自定义实现响应式:Object.defineProperty 🛠️

你以为 Vue 的这些功能都离不开框架吗?其实不然!我们也可以自己实现一个简单的响应式系统。JavaScript 的原生 API Object.defineProperty 就是实现数据代理的基础工具,它允许我们为对象的属性设置 gettersetter,从而可以拦截对数据的访问和修改。

比如,我们可以通过 Object.defineProperty 来实现一个简单的响应式系统:

<!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  // 简单数据类型
    // 拦截器  修改值之前
    //属性定义  定义一下
    Object.defineProperty(obj, 'value', {
      get: function () {
        console.log('读了value 属性');
        return value  // 原来的职责
      },
      set: function (newvalue) {
        console.log('修改了value 属性');
        value = newvalue    // 原来的职责
        document.getElementById('container').innerHTML = newvalue
      }
    })
    Object.defineProperty(obj, 'count', {
      get: function () {
        console.log('读了count 属性');
        return count
      },
      set: function (newcount) {
        console.log('修改了count 属性');
        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>

image.png

这段代码的解释:

有些小伙伴会觉得这一段代码有多此一举的感觉。

image.png

其实不然,我们设置这两个基本变量是为了避免一个死循环的问题。

  • 为什么会出现死循环呢?

这是由于我们如果不定义这两个基本变量,就得通过obj.valueobj.count来访问我们的属性值,这样就造成了我要访问它,就得先被getset给阻拦了,然后再次调用这个方法......,就陷入了无限递归调用。这就会导致程序崩塌咯。

通过这些分析我们发现,我们一直在操作的都是外面的两个基本数据,至于obj是不过是充当一个代理的身份。

image.png

解决了这个疑问,我们继续来一点文邹邹的概念讲解吧。

  • Object.defineProperty 允许我们定义 valuecount 属性的 gettersetter
  • 当你点击“点击加1”或“点击*2”时,分别修改了 obj.valueobj.count,而在 setter 中我们不仅修改了数据,还通过 document.getElementById(...).innerHTML 来更新页面中的内容。这样一来,数据变化就能自动同步到视图,形成了响应式效果!

不过,如果数据项多了,手动为每个属性设置 gettersetter 也变得比较麻烦,这就暴露了 Object.defineProperty 的一个缺点:需要为每个属性单独处理。这时,我们的故事迎来了新主角——Proxy。🔮


Proxy:进阶版的响应式 👑

Proxy 的神奇之处 🧙‍♂️

Proxy 是 JavaScript 提供的一种新特性,它允许我们创建一个代理对象,来拦截对目标对象的所有操作,包括读取、修改属性等。相比于 Object.definePropertyProxy 更加灵活,因为它不需要为每个属性单独写 gettersetter,而是可以在一个地方统一处理所有操作。

通过 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="button">+1</button>
  <button id="btn">*2</button>
  <script>
    // 匿名函数-> 立即执行 + 回调函数(事件处理函数、定时器、文件操作)
    (function () {
      // 函数作用域
      // console.log(this) 观察者模式
      function watch(target, func) {
        // es6 proxy 对象代理
        const proxy = new Proxy(target, {
          get: function (target, prop) {
            console.log(`读取了${target} ${prop}`)
            return target[prop]
          },
          set: function (taget, prop, value) {
            taget[prop] = value;
            func(prop, value)   // 回调函数
          }
        })
        return proxy
      }
      // 暴露给全局
      this.watch = watch
    })()
    let obj = {
      value: 1,
      count: 2
    }
    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('button')
      .addEventListener('click', function () {
        // 交给代理对象
        newObj.value++;
      })
    document.getElementById('btn')
      .addEventListener('click', function () {
        // 交给代理对象
        newObj.count *= 2;
      })
  </script>
</body>
</html>

代码解析:

  • 我们创建了一个 watch 函数,用来返回一个代理对象,getset 拦截器负责拦截所有对数据的操作。
  • newObj.valuenewObj.count 被修改时,set 拦截器会触发回调函数,更新视图。

在这里外面可以发现外并没有写:

屏幕截图 2024-12-31 003303.png

这个难道是上面我们的分析有误吗?

这个就要讲到Proxy,的优势了,相比于 Object.definePropertyProxy 充当了一个真正的 "代理" 身份,让我们在处理对象属性时无需额外的外部变量来存储数据,也无需担心递归死循环的问题。 我们可以看到:

image.png

我们操作的对象是newobj,那么obj的值是不是改变了呢?

image.png

我们可以通过set里面的这个方式来更新obj里面的值。

通过这些解释,在这里我们可以更加了解Proxy,它不止是能让我们的代码复用性更强,而且可以帮助我们自动生成一个代理对象,让我们的操作有了一个更加完善的保证。我们每一次操作obj都必须要通过这一个代理商让代码更加严谨一点。

为什么要用回调函数? 📞

回调函数的最大好处是:你可以在数据变化时执行任何你想要的操作。比如,更新视图、记录日志,或者触发其他复杂的逻辑。上面的例子,我们仅仅是更新了页面内容,但你完全可以在 func 回调函数中做更多事情。


Proxy 的基础示例:理解 getset 拦截器 🎩

也许上面的例子有点晦涩难懂,哈哈,其实这是小编故意而为之,俗话说由简入奢易,由奢入简难

image.png

让我们来看一个简单的 Proxy 示例,帮助你更直观地理解 getset 的工作原理:

const proxy = new Proxy({}, {
  get: function (obj, prop) {
    console.log('设置 get 操作');
    return obj[prop];
  },
  set: function (obj, prop, value) {
    console.log('设置 set 操作');
    obj[prop] = value;
  }
});

proxy.time = 35;  // 触发 set 操作
console.log(proxy.time);  // 触发 get 操作

运行结果:

image.png

  • proxy.time = 35 被执行时,set 拦截器会被触发,打印出“设置 set 操作”。
  • console.log(proxy.time) 被执行时,get 拦截器会被触发,打印出“设置 get 操作”。

在这里有读者又会产生一个疑问,为什么还能给空对象做代理?

image.png

这是 Proxy 的基本用法,它允许我们在数据操作时加入额外的属性数据行为。这让我们对对象的操作更加丝滑流畅。

image.png

这也是我们由ES5到ES6的一个小进步咯。

总结:轻松玩转响应式 🔑

今天,我们从 Vue 的响应式实现 开始,逐步走进了 Object.definePropertyProxy 的世界。通过这些技术,我们可以非常灵活地实现响应式,甚至不依赖框架。

小结一下重点:

  • 响应式:使得数据的变化能够自动同步到界面。
  • Object.defineProperty:通过手动为每个属性定义 gettersetter 来实现响应式。
  • Proxy:更为强大的工具,通过统一的拦截机制来实现响应式,且可以在回调中执行自定义操作。

掌握了这些,你不仅能理解前端框架的实现原理,还能在自己的项目中灵活应用响应式机制。记住,Proxy 就是你实现高效响应式的得力助手!🪄

希望你在探索 JavaScript 的过程中,能够发现更多魔法,玩得开心!🎉

20200229174423_bzukt.jpg