从 reactive 入手,实现对象属性读取、修改、新增和删除的拦截
前面已经梳理了响应式的基本流程:读取数据时进行依赖收集,修改数据时派发更新。
这一篇开始,按照这个流程手写一个简化版的响应式系统。为了先把核心逻辑梳理清楚,这里只使用 js 文件。
先把 reactive 的基础结构搭起来:
- 使用
Proxy代理对象 - 拦截对象的读取、修改、新增和删除
- 预留
track和trigger两个入口 - 处理嵌套对象、重复赋值和无效删除这些细节
创建 reactive 入口
先创建一个 reactive.js 文件。
reactive 的作用就是接收一个对象,并返回这个对象的代理对象:
export function reactive(target) {
const proxy = new Proxy(target, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
return Reflect.set(target, key, value)
}
})
return proxy
}
再创建一个 test.js 简单验证一下:
import { reactive } from './reactive.js'
const obj = {
a: 1
}
const proxyObj = reactive(obj)
proxyObj.a
proxyObj.a = 2
执行 proxyObj.a 时会进入 get,执行 proxyObj.a = 2 时会进入 set。
拆分 handler
如果继续把所有拦截逻辑都写在 reactive.js 中,后面代码会越来越乱。
所以先把 Proxy 的 handler 拆出去,创建 behavior/index.js:
function get(target, key) {
console.log('track')
return Reflect.get(target, key)
}
function set(target, key, value) {
console.log('trigger')
return Reflect.set(target, key, value)
}
export default {
get,
set
}
然后更新 reactive.js:
import handler from './behavior/index.js'
export function reactive(target) {
const proxy = new Proxy(target, handler)
return proxy
}
这样 reactive.js 只负责创建代理对象,具体的拦截逻辑交给 behavior/index.js。
定义操作类型
读取和修改只是最基础的两类操作。
对于一个对象来说,还会有其他行为:
proxyObj.a
'a' in proxyObj
Object.keys(proxyObj)
proxyObj.a = 2
proxyObj.b = 3
delete proxyObj.a
这些操作在响应式系统中需要区分开。
所以创建 constant/enum.js:
/**
* 收集依赖的操作
* GET:读取属性,比如 `proxyObj.a`
* HAS:判断属性是否存在,比如 `'a' in proxyObj`
* ITERATE:遍历对象属性,比如 `Object.keys(proxyObj)`
*/
export const TrackOpTypes = {
GET: 'get',
HAS: 'has',
ITERATE: 'iterate'
}
/**
* 派发更新的操作
* SET:修改已有属性
* ADD:新增属性
* DELETE:删除属性
*/
export const TriggerOpTypes = {
SET: 'set',
ADD: 'add',
DELETE: 'delete'
}
接入 track 和 trigger
接下来创建两个入口文件。
effect/track.js:
function track(target, type, key) {
console.log('track', type, key)
}
export default track
effect/trigger.js:
function trigger(target, type, key) {
console.log('trigger', type, key)
}
export default trigger
然后更新 behavior/index.js:
import { TrackOpTypes, TriggerOpTypes } from '../constant/enum.js'
import track from '../effect/track.js'
import trigger from '../effect/trigger.js'
function get(target, key) {
track(target, TrackOpTypes.GET, key)
return Reflect.get(target, key)
}
function has(target, key) {
track(target, TrackOpTypes.HAS, key)
return Reflect.has(target, key)
}
function set(target, key, value) {
trigger(target, TriggerOpTypes.SET, key)
return Reflect.set(target, key, value)
}
function handleDelete(target, key) {
trigger(target, TriggerOpTypes.DELETE, key)
return Reflect.deleteProperty(target, key)
}
function ownKeys(target) {
track(target, TrackOpTypes.ITERATE)
return Reflect.ownKeys(target)
}
export default {
get,
set,
has,
ownKeys,
deleteProperty: handleDelete
}
这里多处理了三个拦截:
has:对应'key' in proxyObjownKeys:对应Object.keys(proxyObj)、for...in等遍历行为deleteProperty:对应delete proxyObj.key
这样对象的常见操作就基本覆盖到了。
修复细节问题
代理对象
import handler from './behavior/index.js'
import { isObject } from './utils/util.js'
// 创建代理对象和原始对象之间的关系
const proxyMap = new WeakMap()
export function reactive(target) {
// 不是对象,直接返回
if (!isObject(target)) {
return target
}
// 如果已经代理过了,直接返回之前的代理对象
if (proxyMap.has(target)) {
return proxyMap.get(target)
}
const proxy = new Proxy(target, handler)
proxyMap.set(target, proxy)
return proxy
}
嵌套对象处理
把测试代码改成这样:
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
c: {
name: '小明',
age: 18
}
}
const proxyObj = reactive(obj)
proxyObj.c.name
这里访问的是 proxyObj.c.name,直觉上应该触发两次读取:
- 读取
proxyObj.c - 读取
proxyObj.c.name
但现在只会触发一次 track。
原因是 proxyObj 只有第一层被代理了,c 这个对象本身还是普通对象。读取到 c 之后,再访问 c.name,就不会进入 Proxy 的 get 了。
所以在 get 中需要判断读取结果是否为对象。如果是对象,就继续调用 reactive 包一层:
import { reactive } from '../reactive.js'
import { isObject } from '../utils/util.js'
function get(target, key) {
track(target, TrackOpTypes.GET, key)
const result = Reflect.get(target, key)
if (isObject(result)) {
return reactive(result)
}
return result
}
补充一个工具函数:
export function isObject(target) {
return typeof target === 'object' && target !== null
}
这样访问 proxyObj.c.name 时,c 也会被转成响应式对象。
避免无效更新
现在的 set 只要执行赋值,就会触发 trigger。
proxyObj.a = 1
proxyObj.a 本来就是 1,这次赋值其实没有产生变化,不应该派发更新。
所以在触发更新前,需要先比较新旧值:
function set(target, key, value) {
const oldValue = Reflect.get(target, key)
const result = Reflect.set(target, key, value)
if (hasChanged(oldValue, value)) {
trigger(target, TriggerOpTypes.SET, key)
}
return result
}
再补充一个工具函数 hasChanged:
export function hasChanged(oldValue, value) {
return !Object.is(oldValue, value)
}
删除属性时也有类似问题:
delete proxyObj.f
如果 proxyObj 上本来就没有 f,删除它也不应该触发更新。
function handleDelete(target, key) {
const hasKey = Object.hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
trigger(target, TriggerOpTypes.DELETE, key)
}
return result
}
这样只有属性原本存在,并且删除成功时,才会触发 trigger。
区分新增和修改
proxyObj.d = 100
如果 d 原来不存在,这次操作不是修改已有属性,而是新增属性。
新增和修改都会触发 set,所以需要在 set 里区分操作类型:
function set(target, key, value) {
const type = Object.hasOwn(target, key)
? TriggerOpTypes.SET
: TriggerOpTypes.ADD
const oldValue = Reflect.get(target, key)
const result = Reflect.set(target, key, value)
if (hasChanged(oldValue, value)) {
trigger(target, type, key)
}
return result
}
这样:
- 修改已有属性时,触发
SET - 新增属性时,触发
ADD
总结
目前完成了这些能力:
- 使用
Proxy创建响应式对象 - 拦截属性读取、修改、新增、删除和遍历
- 在读取时调用
track - 在修改时调用
trigger
下一步就可以开始实现 effect,让读取时收集当前正在执行的函数,修改时再把这个函数重新执行。
当前代码
到这里,核心代码可以整理成下面这样:
// behavior/index.js
import { TrackOpTypes, TriggerOpTypes } from '../constant/enum.js'
import track from '../effect/track.js'
import trigger from '../effect/trigger.js'
import { reactive } from '../reactive.js'
import { hasChanged, isObject } from '../utils/util.js'
function get(target, key) {
track(target, TrackOpTypes.GET, key)
const result = Reflect.get(target, key)
if (isObject(result)) {
return reactive(result)
}
return result
}
function has(target, key) {
track(target, TrackOpTypes.HAS, key)
return Reflect.has(target, key)
}
function set(target, key, value) {
const type = Object.hasOwn(target, key)
? TriggerOpTypes.SET
: TriggerOpTypes.ADD
const oldValue = Reflect.get(target, key)
const result = Reflect.set(target, key, value)
if (hasChanged(oldValue, value)) {
trigger(target, type, key)
}
return result
}
function handleDelete(target, key) {
const hasKey = Object.hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
trigger(target, TriggerOpTypes.DELETE, key)
}
return result
}
function ownKeys(target) {
track(target, TrackOpTypes.ITERATE)
return Reflect.ownKeys(target)
}
export default {
get,
set,
has,
deleteProperty: handleDelete,
ownKeys
}