Vue.js 设计与实现-响应系统-代理Set和Map
在本章中,我们首先介绍了 Proxy与 Reflect。Vue.js 3的响应式数据是基于 Proxy实现的,Proxy可以为其他对象创建一个代理对象。所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。在实现代理的过程中,我们遇到了访问器属性的 this指向问题,这需要使用 Reflect.*方法并指定正确的 receiver来解决。
然后我们详细讨论了 JavaScript中对象的概念,以及 Proxy的工作原理。在 ECMAScript规范中,JavaScript中有两种对象,其中一种叫作常规对象,另一种叫作异质对象。满足以下三点要求的对象就是常规对象:
对于表 5-1给出的内部方法,必须使用规范 10.1.x节给出的定义实现;
对于内部方法[[Call]],必须使用规范 10.2.1节给出的定义实现;
●对于内部方法[[Construct]],必须使用规范 10.2.2节给出的定义实现。
而所有不符合这三点要求的对象都是异质对象。一个对象是函数还是其他对象,是由部署在该对象上的内部方法和内部槽决定的。
接着,我们讨论了关于对象 Object的代理。代理对象的本质,就是查阅规范并找到可拦截的基本操作的方法。有一些操作并不是基本操作,而是复合操作,这需要我们查阅规范了解它们都依赖哪些基本操作,从而通过基本操作的拦截方法间接地处理复合操作。我们还详细分析了添加、修改、删除属性对 for...in操作的影响,其中添加和删除属性都会影响 for...in循环的执行次数,所以当这些操作发生时,需要触发与 ITERATE_KEY相关联的副作用函数重新执行。而修改属性值则不影响 for...in循环的执行次数,因此无须处理。我们还讨论了如何合理地触发副作用函数重新执行,包括对 NaN的处理,以及访问原型链上的属性导致的副作用函数重新执行两次的问题。对于 NaN,我们主要注意的是 NaN=== NaN永远等于 false。对于原型链属性问题,需要我们查阅规范定位问题的原因。由此可见,想要基于 Proxy实现一个相对完善的响应系统,免不了去了解ECMAScript规范。
而后,我们讨论了深响应与浅响应,以及深只读与浅只读。这里的深和浅指的是对象的层级,浅响应(或只读)代表仅代理一个对象的第一层属性,即只有对象的第一层属性值是响应(或只读)的。深响应(或只读)则恰恰相反,为了实现深响应(或只读),我们需要在返回属性值之前,对值做一层包装,将其包装为响应式(或只读)数据后再返回。
之后,我们讨论了关于数组的代理。数组是一个异质对象,因为数组对象部署的内部方法[[DefineOwnProperty]]不同于常规对象。通过索引为数组设置新的元素,可能会隐式地改变数组 length属性的值。对应地,修改数组 length属性的值,也可能会间接影响数组中的已有元素。所以在触发响应的时候需要额外注意。我们还讨论了如何拦截 for...in和 for...of对数组的遍历操作。使用for...in循环遍历数组与遍历普通对象区别不大,唯一需要注意的是,当追踪 for...in操作时,应该使用数组的 length作为追踪的key。for...of基于迭代协议工作,数组内建了Symbol.iterator方法。根据规范的 23.1.5.1节可知,数组迭代器执行时,会读取数组的 length属性或数组的索引。因此,我们不需
要做其他额外的处理,就能够实现对 for...of迭代的响应式支持。
我们还讨论了数组的查找方法。如 includes、indexOf以及lastIndexOf等。对于数组元素的查找,需要注意的一点是,用户既可能使用代理对象进行查找,也可能使用原始对象进行查找。为了支持这两种形式,我们需要重写数组的查找方法。原理很简单,当用户使用这些方法查找元素时,我们可以先去代理对象中查找,如果找不到,再去原始数组中查找。
我们还介绍了会隐式修改数组长度的原型方法,即 push、pop、shift、unshift以及 splice等方法。调用这些方法会间接地读取和设置数组的 length属性,因此,在不同的副作用函数内对同一个数组执行上述方法,会导致多个副作用函数之间循环调用,最终导致调用栈溢出。为了解决这个问题,我们使用一个标记变量shouldTrack来代表是否允许进行追踪,然后重写了上述这些方法,目的是,当这些方法间接读取 length属性值时,我们会先将shouldTrack的值设置为 false,即禁止追踪。这样就可以断开length属性与副作用函数之间的响应联系,从而避免循环调用导致的调用栈溢出。
最后,我们讨论了关于集合类型数据的响应式方案。集合类型指Set、Map、WeakSet以及 WeakMap。我们讨论了使用 Proxy为集合类型创建代理对象的一些注意事项。集合类型不同于普通对象,它有特定的数据操作方法。当使用 Proxy代理集合类型的数据时要格外注意,例如,集合类型的 size属性是一个访问器属性,当通过代理对象访问 size属性时,由于代理对象本身并没有部署[[SetData]]这样的内部槽,所以会发生错误。另外,通过代理对象执行集合类型的操作方法时,要注意这些方法执行时的 this指向,我们需要在get拦截函数内通过.bind函数为这些方法绑定正确的 this值。我们还讨论了集合类型响应式数据的实现。我们需要通过“重写”集合方法的方式来实现自定义的能力,当 Set集合的 add方法执行时,需要调用 trigger函数触发响应。我们也讨论了关于“数据污染”的问题。数据污染指的是不小心将响应式数据添加到原始数据中,它导致用户可以通过原始数据执行响应式相关操作,这不是我们所期望的。为了避免这类问题发生,我们通过响应式数据对象的 raw属性来访问对应的原始数据对象,后续操作使用原始数据对象就可以了。我们还讨论了关于集合类型的遍历,即 forEach方法。集合的 forEach方法与对象的 for...in遍历类似,最大的不同体现在,当使用 for...in遍历对象时,我们只关心对象的键是否变化,而不关心值;但使用forEach遍历集合时,我们既关心键的变化,也关心值的变化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义区
let activeEffect
const effectStack = []
const reactiveMap = new Map()
const arrayInstrumentations = {}
const pro = reactive(new Map([
['key1', 'value1'],
['key2', 'value2']
]))
;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function(...args) {
// this时代理对象,现在代理对象中查找,将结果存储到res中
let res = originMethod.apply(this, args)
if (res === false || res === -1) {
// res 为false说明没找到,通过this.raw拿到原始数组,再去其中查找并更新res值
res = originMethod.apply(this.raw, args)
}
return res
}
})
// 一个标记变量,代表是否进行追踪。默认值为true,即允许追踪
let shouldTrack = true
// 重写数组的push方法
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
// 取得原始push方法
const originMethod = Array.prototype[method]
// 重写
arrayInstrumentations[method] = function(...args) {
// 在调用原始方法之前,禁止追踪
shouldTrack = false
// push方法的默认行为
let res = originMethod.apply(this, args)
// 在调用原始方法之后,恢复原来的行为,即允许追踪
shouldTrack = true
return res
}
})
// 抽离为独立的函数,便于复用
function iterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 获取原始迭代器方法
const itr = target[Symbol.iterator] ()
const wrap = (val) => typeof val === 'object' && val !== null ? reactive(val) : val
// 调用track函数建立响应联系
track(target, ITERATE_KEY)
// 返回自定义的迭代器
return {
next() {
// 调用原始迭代器的nextfangfa获取value和done
const { value, done } = itr.next()
return {
// 如果value不是undefined,则对其进行包裹
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
},
// 并且实现可迭代协议
[Symbol.iterator] () {
return this
}
}
}
function valuesIterationMethod() {
// 获取原始数据对象
const target = this.raw
// 通过target.values获取原始迭代器方法
const itr = target.values()
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
track(target, ITERATE_KEY)
// 将其返回
return {
next() {
const { value, done } = itr.next()
return {
// value是值,而非键值对
value: wrap(value),
done
}
},
[Symbol.iterator] () {
return this
}
}
}
const MAP_KEY_ITERATE_KEY = Symbol()
function keysIterationMethod() {
// 获取原始数据对象
const target = this.raw
// 通过target.keys获取原始迭代器方法
const itr = target.keys()
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
track(target, MAP_KEY_ITERATE_KEY)
// 将其返回
return {
next() {
const { value, done } = itr.next()
return {
// value是值,而非键值对
value: wrap(value),
done
}
},
[Symbol.iterator] () {
return this
}
}
}
const mutableInstrumentations = {
add(key) {
// this 仍然指向的是代理对象,通过raw属性获取原始数据对象
const target = this.raw
// 先判断值是否已经存在
const hadKey = target.has(key)
// 通过原始数据对象执行add方法添加具体的值
// 注意,这里不再需要.bind了,因为是直接通过targeet调用并执行的
const res = target.add(key)
if (!hadKey) {
// 通过调用trigger函数触发响应,并指定操作类型为ADD
trigger(target, key, 'ADD')
}
return res
},
delete(key) {
const target = this.raw
const hadKey = target.has(key)
const res = target.delete(key)
if (hadKey) {
trigger(target, key, 'DELETE')
}
return res
},
get(key) {
// 获取原始对象
const target = this.raw
// 判断读取的key是否存在
const had = target.has(key)
// 追踪依赖,建立响应联系
track(target, key)
// 如果存在,则返回结果。这里要注意的是,如果得到的结果res仍然是可代理的数据
// 则要用reactive返回包装后的响应式数据
if (had) {
const res = target.get(key)
return typeof res === 'object' ? reactive(res) : res
}
},
set(key, value) {
const target = this.raw
const had = target.has(key)
const oldValue = target.get(key)
// 获取原始数据,由于value本身可能已经是原始数据,所以此时value.raw不存在,则直接使用value
const rawValue = value.raw || value
target.set(key, rawValue)
if (!had) {
trigger(target, key, 'ADD')
} else if (oldValue !== value || (oldValue === oldValue && value === value)) {
trigger(target, key, 'SET')
}
},
forEach(callback, thisArg) {
// wrap函数用来把可代理的值转换为响应式数据
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
// 取得原始数据对象
const target = this.raw
// 与ITERATE_KEY建立响应联系
track(target, ITERATE_KEY)
// 通过原始数据对象调用forEach方法
target.forEach((v, k) => {
// 手动调用callback,用wrap方法包裹value和key后在传给callback,这样实现深相应
callback.call(thisArg, wrap(v), wrap(k), this)
})
},
[Symbol.iterator]: iterationMethod,
entries: iterationMethod,
values: valuesIterationMethod,
keys: keysIterationMethod
}
const bucket = new WeakMap()
// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve() 创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 用一个标志代表是否正在刷新队列
let isFlushing = false
const ITERATE_KEY = Symbol()
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) return
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
// 封装 createReactive函数,接受参数isShallow,代表是否为浅相应,默认为false,即非浅响应(深相应)
// 接受参数isReadonly 代表是否只读,默认为false,表示非只读,即可修改
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
// 如果读取的是raw属性,则返回原始数据对象target
if (key === 'raw') {
return target
}
if (key === 'size') {
// 如果读取的是size属性
// 调用track函数建立响应联系
track(target, ITERATE_KEY)
// 通过指定第三个参数receiver为原始对象target从而修复问题
return Reflect.get(target, key, target)
}
// 读取其他属性的默认行为
// 将方法与原始数据对象target绑定后返回
return mutableInstrumentations[key]
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 如果操作目标target是数组,则使用length属性作为key并建立响应联系
// 将副作用函数与ITERATE_KEY 关联
Array.isArray(target) ? track(target, 'length') : track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 如果数据是只读的,打印警告信息,直接返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return
}
// 先获取旧值
const oldValue = target[key]
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
// type可能是数组或者对象
// 数组:如果设置的索引值小于长度,那么是SET操作,否则是ADD操作
const type = Array.isArray(target) ? Number(key) < target.length ? 'SET' : 'ADD' :
Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// target === receiver.raw 说明receiver就是target的代理对象
if (target === receiver.raw) {
// 比较新值和旧值,只要当不全等的时候才触发相应
if (oldValue !== newValue) {
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue // 使用了Reflect.set设置,不必使用此句
// 增加第四个参数,即触发响应的新值
trigger(target, key, type, newValue)
}
}
return res
},
deleteProperty(target, key) {
// 如果数据是只读的,打印警告信息,直接返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return
}
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
}
function reactive(obj) {
// 优先通过原始对象obj寻找之前创建的代理对象,如果找到了,直接返回已有的代理对象
const existionProxy = reactiveMap.get(obj)
if (existionProxy) {
return existionProxy
}
// 否则,创建新的代理对象
const proxy = createReactive(obj)
// 存储到map中,从而米便重复创建
reactiveMap.set(obj, proxy)
return proxy
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
function readonly(obj) {
return createReactive(obj, false, true)
}
function shallowReadonly(obj) {
return createReactive(obj, true, true)
}
function track(target, key) {
// 没有副作用 直接返回
// 当禁止追踪时,直接返回
if (!activeEffect || !shouldTrack) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type, newValue) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
// 如果操作类型是SET,并且目标对象是Map类型的数据,也应该触发那些与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE' || (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
if (type === 'ADD' || type === 'DELETE' && (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
// 将与MAP_KEY_ITERATE_KEY相关联的副作用函数也添加到effectsToRun
const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
// 当操作类型为ADD并且目标对象是数组时,应该取出并执行属性相关联的副作用函数
if (type === 'ADD' && Array.isArray(target)) {
// 取出与length相关联的副作用函数
const lengthEffects = depsMap.get('length')
lengthEffects && lengthEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
// 如果操作目标是数组,并且修改了数组的length属性
if (Array.isArray(target) && key === 'length') {
// 对于索引大于或者等于新的length值的元素
depsMap.forEach((effects, key) => {
if (key >= newValue) {
effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i];
// 将effectFn 从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps 数组
effectFn.deps.length = 0
}
// 执行区
effect(() => {
for(const value of pro.values()) {
console.log(value)
}
})
pro.set('key2', 'value3')
pro.set('key3', 'value3')
</script>
</html>