原始值的响应方案 - ref 的基本实现
由于 JavaScript 中的 Proxy 无法代理原始值,因此 Vue3 中引入了 ref 的概念。ref 本质上是一个“包裹对象”,从而间接实现原始值的响应式方案。
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
由于“包裹对象”本质上与普通对象没有任何区别,因此为了区分 ref 与普通响应式对象,我们还为“包裹对象”定义了一个值为 true 的特有标识,即__v_isRef。可以通过该标识来判断一个数据是否是 ref 。
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
ref 的基本实现的完整实现如下:
// 存储副作用函数的桶
const bucket = new WeakMap()
const ITERATE_KEY = Symbol()
// 增加第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 检查被操作的属性是否是对象自己的属性
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
},
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
// 拦截读取操作
get(target, key, receiver) {
if (key === 'raw') {
return target
}
if (!isReadonly) {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// target === receiver.raw 说明 receiver 就是 target 的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
has(target, key) {
console.error('拦截 in 操作符')
track(target, key)
return Reflect.has(target, key)
}
})
}
function reactive(obj) {
return createReactive(obj)
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
function readonly(obj) {
return createReactive(obj, false, true /* 只读 */)
}
function shallowReadonly(obj) {
return createReactive(obj, true /* shallow */, true)
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合 deps 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
// 取得与 key 相关联的副作用函数
const effects = depsMap.get(key)
const effectsToRun = new Set()
// 将与 key 相关联的副作用函数添加到 effectsToRun
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) { // 新增
effectsToRun.add(effectFn)
}
})
// 当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
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 函数
let activeEffect
// effect 栈
const effectStack = []
// effect 函数用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
// 创建原始值的响应式数据
const refVal = ref(1)
effect(() => {
// 在副作用函数内通过 value 属性读取原始值
console.log(refVal.value)
})
// 修改值能够触发副作用函数重新执行
refVal.value = 2
原始值的响应方案 - 解决响应丢失问题
ref 除了能够用于原始值的响应式方案之外,还能用来解决响应丢失问题。所谓响应丢失,就是通过解构等方式,将响应式数据解构成原始值,从而使数据失去响应性。为了解决该问题,Vue3 实现了 toRef 以及 toRefs 这两个函数。它们本质上是对响应式数据做了一层包装,或者叫作“访问代理”。
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
// 允许设置值
set value(val) {
obj[key] = val
}
}
// 定义 __v_isRef 属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
function toRefs(obj) {
const ret = {}
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key)
}
return ret
}
解决响应丢失问题的完整实现如下:
// 存储副作用函数的桶
const bucket = new WeakMap()
const ITERATE_KEY = Symbol()
// 增加第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 检查被操作的属性是否是对象自己的属性
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
},
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
// 拦截读取操作
get(target, key, receiver) {
if (key === 'raw') {
return target
}
if (!isReadonly) {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// target === receiver.raw 说明 receiver 就是 target 的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
has(target, key) {
console.error('拦截 in 操作符')
track(target, key)
return Reflect.has(target, key)
}
})
}
function reactive(obj) {
return createReactive(obj)
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
function readonly(obj) {
return createReactive(obj, false, true /* 只读 */)
}
function shallowReadonly(obj) {
return createReactive(obj, true /* shallow */, true)
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合 deps 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
// 取得与 key 相关联的副作用函数
const effects = depsMap.get(key)
const effectsToRun = new Set()
// 将与 key 相关联的副作用函数添加到 effectsToRun
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) { // 新增
effectsToRun.add(effectFn)
}
})
// 当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
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 函数
let activeEffect
// effect 栈
const effectStack = []
// effect 函数用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
// 允许设置值
set value(val) {
obj[key] = val
}
}
// 定义 __v_isRef 属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
function toRefs(obj) {
const ret = {}
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key)
}
return ret
}
const obj = reactive({ foo: 1, bar: 2 })
const refFoo = toRef(obj, 'foo')
effect(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(refFoo.value)
})
refFoo.value = 100
原始值的响应方案 - 自动脱 ref 的实现
ref 有一个缺点,就是获取 ref 的值需要通过 .value 的形式获得,为了减轻用户的心智负担,我们自动对暴露到模板中的响应式数据进行脱 ref 处理。这样,用户在模板中使用响应式数据时,就无须关心一个值是不是 ref 了,即不需要通过 .value 的形式获取 ref 的值。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key]
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
}
自动脱 ref 的实现的完整实现如下:
// 存储副作用函数的桶
const bucket = new WeakMap()
const ITERATE_KEY = Symbol()
// 增加第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 检查被操作的属性是否是对象自己的属性
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
},
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
// 拦截读取操作
get(target, key, receiver) {
if (key === 'raw') {
return target
}
if (!isReadonly) {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// target === receiver.raw 说明 receiver 就是 target 的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
has(target, key) {
console.error('拦截 in 操作符')
track(target, key)
return Reflect.has(target, key)
}
})
}
function reactive(obj) {
return createReactive(obj)
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
function readonly(obj) {
return createReactive(obj, false, true /* 只读 */)
}
function shallowReadonly(obj) {
return createReactive(obj, true /* shallow */, true)
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合 deps 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
// 取得与 key 相关联的副作用函数
const effects = depsMap.get(key)
const effectsToRun = new Set()
// 将与 key 相关联的副作用函数添加到 effectsToRun
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) { // 新增
effectsToRun.add(effectFn)
}
})
// 当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
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 函数
let activeEffect
// effect 栈
const effectStack = []
// effect 函数用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
// 允许设置值
set value(val) {
obj[key] = val
}
}
// 定义 __v_isRef 属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
function toRefs(obj) {
const ret = {}
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key)
}
return ret
}
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key]
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
}
const obj = reactive({ foo: 1, bar: 2 })
// 调用 proxyRefs 函数创建代理
const newObj = proxyRefs({ ...toRefs(obj) })
newObj.foo = 100
console.log(newObj.foo) // 100
console.log(newObj.bar) // 2