推荐答案
Proxy 对象可以通过拦截对象的操作,并在这些操作发生时执行自定义的逻辑,从而实现数据绑定。核心思路是利用 get 和 set 拦截器,在读取属性时收集依赖,在设置属性时触发更新。以下是实现数据绑定的基本步骤:
- 创建一个
reactive函数,接收一个普通对象作为参数,返回一个响应式对象(Proxy 对象)。 - 在
reactive函数内部,使用Proxy创建一个代理对象。 - 在
Proxy的get拦截器中,收集当前正在执行的副作用函数(如渲染函数)到依赖集合中。 - 在
Proxy的set拦截器中,当属性值发生改变时,遍历依赖集合,执行所有收集到的副作用函数,从而触发更新。
// www.javascriptcn.com code example
function reactive(target) {
const depsMap = new Map(); // 存储属性与依赖关系的 Map
function track(target, key) {
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
if (activeEffect) { // activeEffect 存储当前的副作用函数
deps.add(activeEffect);
}
}
function trigger(target, key) {
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => effect());
}
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
if(target[key] === value) return true; // 新旧值相同,不触发更新
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
return proxy;
}
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn(); // 立即执行一次,收集依赖
activeEffect = null;
}
// 示例
const data = { count: 0 };
const reactiveData = reactive(data);
let renderCount = 0;
effect(() => {
console.log('render, count:', reactiveData.count);
renderCount++;
});
reactiveData.count++;
reactiveData.count++;
console.log('renderCount:', renderCount)本题详细解读
什么是 Proxy
Proxy 是 ES6 中新增的一个对象,可以用来创建一个对象的代理,从而拦截并自定义对象的基本操作(如属性读取、赋值、删除等)。 Proxy 的基本语法如下:
const proxy = new Proxy(target, handler);
target: 需要代理的目标对象。handler: 一个对象,包含用于拦截操作的函数(称为陷阱)。
get 和 set 陷阱
get 和 set 是 handler 对象中常用的陷阱:
get(target, key, receiver): 当访问代理对象的属性时会被调用。target: 目标对象。key: 被访问的属性名。receiver: 代理对象本身(或继承自代理对象的子对象)。
set(target, key, value, receiver): 当设置代理对象的属性时会被调用。target: 目标对象。key: 被设置的属性名。value: 要设置的新值。receiver: 代理对象本身。
数据绑定原理
- 响应式对象:
reactive函数返回的 Proxy 对象。 - 依赖收集:
- 在
get陷阱中,当访问一个响应式对象的属性时,会执行track函数。 track函数会检查当前是否有正在执行的副作用函数(通过activeEffect判断)。- 如果存在,则将该副作用函数添加到该属性的依赖集合中(
depsMap)。
- 在
- 触发更新:
- 在
set陷阱中,当修改响应式对象的属性时,会执行trigger函数。 trigger函数会遍历该属性的依赖集合,并执行其中所有的副作用函数,从而触发视图的更新(重新渲染)。
- 在
- 副作用函数:
effect函数用于定义副作用函数,例如render函数。- 在
effect函数执行期间,会将当前正在执行的副作用函数赋值给activeEffect。
代码解释
depsMap: 一个Map对象,用于存储属性和依赖集合之间的关系。Key 是属性名,Value 是一个Set,存储依赖该属性的所有副作用函数。track: 用于收集依赖的函数。它会查找属性的依赖集合,如果不存在则创建新的,并将当前正在执行的副作用函数添加到集合中。trigger: 用于触发更新的函数。它会查找属性的依赖集合,并执行集合中的所有副作用函数。reactive: 创建响应式对象的函数,返回一个Proxy对象,其中get和set拦截器分别调用track和trigger。effect: 用于创建副作用函数的函数,会设置activeEffect指向当前函数,执行函数,并将activeEffect设置为null。
代码示例运行流程
- 创建
data对象 和reactiveData响应式对象。 - 执行
effect函数
- 设置
activeEffect为当前的渲染函数 - 执行渲染函数(console.log输出,记录第一次渲染)
- 设置
activeEffect为null
- 修改
reactiveData.count两次
- 每次修改都会执行
set陷阱 set陷阱执行trigger函数,通知count属性对应的所有副作用函数执行(也就是渲染函数)。- 每次渲染函数都会被执行,并输出当前 count 的值
- 最后,输出渲染次数
renderCount,值为 3,因为首次 effect 执行渲染了一次,两次 set 触发了渲染。
优点和缺点
优点:
- 简单易懂: 使用
Proxy实现数据绑定代码相对简洁,更容易理解。 - 拦截能力强:
Proxy可以拦截对象的所有基本操作,功能强大。 - 高性能: 相对于
Object.defineProperty,Proxy在某些情况下性能更高。
缺点:
- 兼容性问题:
Proxy在 IE 浏览器中不兼容,需要使用 polyfill。 - 深层响应式: 如果需要实现对嵌套对象的深层响应式,需要递归地使用
reactive函数,稍显复杂。