原生 setter 响应式系统核心是用 Object.defineProperty 拦截赋值,在 set 中对比新旧值、更新闭包变量并通知订阅者;支持对象递归响应化,但不支持新增/删除属性、数组索引操作及 Map/Set,兼容 IE9+。

用原生 setter 打造极简响应式系统,核心就一条:通过 Object.defineProperty 拦截属性赋值动作,在 set 回调里触发更新逻辑。它不依赖框架,兼容 IE9+,代码量少、逻辑透明,适合理解响应式本质或嵌入轻量项目。
从一个可监听的属性开始
直接对单个属性启用 setter 是最基础的起点。关键点在于:不能直接操作原始值,必须用闭包或私有变量保存真实数据,否则 set 中读取 this.key 会无限递归。
- 定义时传入初始值,用局部变量
val存储,避免在get/set中访问自身属性 -
set内做新旧值对比,防止无意义更新(如obj.name = obj.name) - 更新后执行自定义回调,比如刷新 DOM 或通知函数
示例:
const data = {};<br>let val = 'hello';<br>Object.defineProperty(data, 'message', {<br> get() { return val; },<br> set(newVal) {<br> if (newVal === val) return;<br> val = newVal;<br> console.log('数据已变:', newVal);<br> // 这里可插入 render() 或 dispatchEvent<br> },<br> enumerable: true,<br> configurable: true<br>});
批量处理对象所有属性
真实场景中,数据通常是对象,需递归遍历每个 key 并设为响应式。注意:只处理自有可枚举属性,跳过原型链和不可配置项。
立即学习“前端免费学习笔记(深入)”;
前端设计与 UI/UX 全方位优化专家。覆盖视觉层次、排版系统、色彩理论、响应式布局、交互体验、动画动效、无障碍访问、性能优化八大维度,帮助开发者将普通页面升级为高品质产品级界面。前端设计与 UI/UX 全方位优化专家。覆盖视觉层次、排版系统、色彩理论、响应式布局、交互体验、动画动效、无障碍访问、性能优化八大维度,帮助开发者将普通页面升级为高品质产品级界面。
- 用
Object.keys(obj)获取顶层键名,不包括继承属性 - 对每个 key 调用
defineReactive,把值转成响应式(若值是对象,递归处理) - 确保
configurable: true,否则后续无法重新定义该属性
简化版实现:
function defineReactive(obj, key, val) {<br> // 若值是对象,递归使其响应式<br> if (typeof val === 'object' && val !== null) {<br> observe(val);<br> }<br> Object.defineProperty(obj, key, {<br> get() { return val; },<br> set(newVal) {<br> if (newVal === val) return;<br> val = newVal;<br> console.log(`${key} 更新为`, newVal);<br> // 触发视图更新逻辑<br> },<br> enumerable: true,<br> configurable: true<br> });<br>}<br><br>function observe(obj) {<br> if (typeof obj !== 'object' || obj == null) return;<br> Object.keys(obj).forEach(key => {<br> defineReactive(obj, key, obj[key]);<br> });<br>}
让更新可被外部感知
单纯拦截赋值还不够,需要一种机制让“谁关心这个变化”能注册进来。最简方式是内置一个回调队列,每次 set 时遍历执行。
- 每个响应式属性配一个
subs数组,存放监听函数(如渲染函数、校验函数) -
set中更新值后调用subs.forEach(fn => fn()) - 提供
subscribe(fn)方法供外部添加监听器
增强后的 defineReactive 片段:
function defineReactive(obj, key, val) {<br> const subs = [];<br> Object.defineProperty(obj, key, {<br> get() { return val; },<br> set(newVal) {<br> if (newVal === val) return;<br> val = newVal;<br> subs.forEach(fn => fn()); // 通知所有订阅者<br> },<br> enumerable: true,<br> configurable: true<br> });<br> // 暴露订阅方法<br> obj[`$on${key.charAt(0).toUpperCase() + key.slice(1)}`] = fn => {<br> if (typeof fn === 'function') subs.push(fn);<br> };<br>}
注意事项与边界限制
原生 setter 方案简洁,但有明确适用边界,用前需清楚:
- 无法检测新增或删除属性(
obj.newKey = 1不会触发响应),需用Object.defineProperty显式定义 - 不支持数组下标赋值(
arr[0] = x)、length修改、清空操作(arr.length = 0),数组需重写原型方法或用索引代理 - IE9 是最低支持版本,更低版本需降级为轮询或放弃响应式
- 不能监听 Map/Set 等集合类型,它们的内部变化不触发属性 setter

















