今天我们要聊聊如何用 Proxy 来实现一个简单的响应式系统。如果你对前端开发有所了解,尤其是使用过 Vue.js 或者 React 这样的框架,你一定知道“响应式”这个词。它意味着当数据发生变化时,视图会自动更新,反之亦然。这种机制让开发者可以专注于业务逻辑,而不需要操心手动刷新页面或重新渲染组件。
什么是响应式?
简单来说,响应式就是让数据和视图之间建立一种联系,使得任何一方的变化都能及时反映到另一方。想象一下你在编辑器里打字,每个字母的输入都会立刻显示在屏幕上;或者当你修改了某个表单字段,相应的结果区域也会跟着改变。这就是响应式的魅力所在——无缝连接数据与界面,提供流畅的用户体验。
ES5时代的响应式:Object.defineProperty
在 ES5 的时代,我们主要依靠 Object.defineProperty() 方法来创建响应式对象。通过这个方法,我们可以为对象的属性定义 getter 和 setter,从而在读取或设置属性值的时候执行自定义代码。可以理解为拦截一下对对象属性的操作。
<div id="show1"></div>
<div id="show2"></div>
<button id="add">点击+1</button>
<button id="times2">点击*2</button>
<script>
// 定义全局变量 count1 并初始化为 1
let count1 = 1;
// 定义全局变量 count2 并初始化为 2
let count2 = 2;
// 创建一个对象 obj,包含 count1 和 count2 两个属性
const obj = {
count1: 1,
count2: 2,
}
// 将 obj.count1 的值显示在 id 为 show1 的 div 元素中
document.getElementById('show1').innerHTML = obj.count1;
// 将 count2 的值显示在 id 为 show2 的 div 元素中
document.getElementById('show2').innerHTML = count2;
// 使用 Object.defineProperty 为 obj 的 count1 属性添加 getter 和 setter 方法
Object.defineProperty(obj, 'count1', {
// getter 方法,当读取 obj.count1 时触发
get:function() {
console.log('读取了 count1');
// 返回全局变量 count1 的值
return count1;
//return obj.count1;
},
// setter 方法,当设置 obj.count1 时触发
set:function(newVal) {
console.log('设置了 count1');
// 更新全局变量 count1 的值为新值
count1 = newVal;
// obj.count1 = newVal;
// 更新 id 为 show1 的 div 元素的内容为新的 count1 值
document.getElementById('show1').innerHTML = count1;
}
})
// 为 id 为 add 的按钮添加点击事件监听器
document.getElementById('add').addEventListener('click',function(){
// 点击按钮时,将 obj.count1 的值加 1
obj.count1++;
})
// 你可以试着自己完成 count2 的响应式。
</script>
不要在
getter和setter中访问或修改被重新定义的属性,否则会无限调用
虽然
Object.defineProperty() 能够满足基本需求,但它有一个很大的局限性:必须对每个需要响应式的属性单独进行定义。这意味着如果对象有很多属性,或者属性是动态添加的,维护起来就会变得非常麻烦。
ES6之后的新选择:Proxy
幸运的是,随着 ES6 标准的到来,JavaScript 引入了一个更强大的工具——Proxy。Proxy 允许我们在不改变原始对象的前提下,拦截并控制对该对象的各种操作(如属性访问、赋值、删除等)。这为我们实现更高效、更灵活的响应式系统提供了可能。
使用 Proxy 创建响应式对象
使用 Proxy 来完成之前的例子:
<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); //window
//观察者模式
function watch(target, func) {
// es6 proxy 对象代理
const proxy = new Proxy(target, {
get: function (target, prop) {
console.log(`读取了${prop}`)
return target[prop]
},
set: function (target, prop, value) {
target[prop] = value
func(prop, value)
}
})
return proxy
}
// 暴露给全局
this.watch = watch
})()
let obj = {
value: 1,
count: 1
}
console.log(obj)
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>
匿名函数立即执行(IIFE)
(function () {
// 函数作用域
function watch(target, func) {
const proxy = new Proxy(target, {
get: function (target, prop) {
console.log(`读取了 ${prop}`);
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
func(prop, value); // 触发回调函数更新视图
}
});
return proxy;
}
// 暴露给全局
this.watch = watch;
})();
创建 watch 函数
watch函数:这个函数接收两个参数——target(要代理的目标对象)和func(每当目标对象的属性被修改时调用的回调函数)。watch的目的是返回一个新的Proxy对象,该对象会拦截对target的所有访问和修改。
使用 Proxy 实现代理
-
get拦截器:当读取属性时,返回原始对象上的相应属性值。javascript 深色版本 get: function (target, prop) { console.log(`读取了 ${prop}`); return target[prop]; } -
set拦截器:当设置属性时,更新原始对象上的属性值,并调用传入的回调函数func来处理任何必要的副作用,比如更新 DOM。javascript 深色版本 set: function (target, prop, value) { target[prop] = value; func(prop, value); // 触发回调函数更新视图 }
暴露 watch 函数给全局
this.watch = watch;:将watch函数赋值给全局对象(通常是window),这样可以在其他地方直接调用它。
创建响应式对象并初始化
javascript
深色版本
let obj = {
value: 1,
count: 1
};
console.log(obj);
var newObj = watch(obj, function (key, newValue) {
if (key === 'value') {
document.getElementById('container').innerHTML = newValue;
}
if (key === 'count') {
document.getElementById('count').innerHTML = newValue;
}
});
- 创建普通对象
obj:包含两个属性value和count,初始值都为 1。 - 调用
watch函数:传入obj和一个回调函数,用来更新对应的 DOM 元素内容。watch函数返回的是经过Proxy包装后的newObj,以后对newObj的操作都会触发相应的get和set拦截器。
绑定事件监听器
javascript
深色版本
document.getElementById('button')
.addEventListener('click', function () {
newObj.value++;
});
document.getElementById('btn')
.addEventListener('click', function () {
newObj.count *= 2;
});
- 绑定点击事件:分别为两个按钮绑定了点击事件监听器。当用户点击“+1”按钮时,
newObj.value会自增;当点击“点击*2”按钮时,newObj.count会被乘以 2。由于newObj是通过Proxy创建的,所以每次修改它的属性时,都会触发set拦截器,进而执行回调函数更新相关 DOM 元素的内容。
响应式机制的工作流程
-
初始化阶段:
- 用户加载页面后,JavaScript 代码被执行,创建了
obj和newObj(即Proxy包装的对象)。 - 同时,为两个按钮绑定了点击事件监听器。
- 用户加载页面后,JavaScript 代码被执行,创建了
-
交互:
- 当我点击“+1”按钮时,
newObj.value++被执行。这会触发Proxy的set拦截器,更新obj.value的值,并调用回调函数更新#container的文本内容。 - 类似地,当用户点击“点击*2”按钮时,
newObj.count *= 2被执行。这也会触发set拦截器,更新obj.count的值,并调用回调函数更新#count的文本内容。
- 当我点击“+1”按钮时,
-
自动更新:
- 每次属性值发生变化时,
Proxy的set拦截器都会捕获到变化,并通过回调函数自动更新相关的 DOM 元素,从而实现了数据驱动视图更新的效果。
- 每次属性值发生变化时,
总结
通过上述步骤,我们可以看到 Proxy 在这里扮演了一个非常重要的角色——它不仅允许我们拦截对对象属性的所有访问和修改,还提供了一种简便的方式来添加额外的行为(如更新 DOM)。这种机制使得数据和视图之间建立了紧密的联系,任何一方的变化都能及时反映到另一方,这就是所谓的“响应式”