Vue.js 设计与实现—ref原始值的响应式方案

132 阅读4分钟

Vue.js 设计与实现—ref原始值的响应式方案

参考《Vuejs设计与实现 —— ref 原始值的响应式方案》

什么是原始值

原始值指的是Boolean、Number、BigInt、String、Symbol、undefined、null等类型的值,在javascript中,原始值是按值传递的,引用类型是按引用传递的,这就意味着,如果一个函数接收了一个原始值作为参数,那么形参和实参之间是没有引用关系的,他们是完全独立的两个值。

为什么要引入ref

javascriptproxy无法对原始值提供代理,因此想要将原始值变成响应式数据,就必须对原始值做一层包裹,这就是ref要做的事。

用户自定义包裹对象的问题

let _name = `hello CQ`
// 包裹对象
const nameWrapper = {
    value: 'hello CQ'
}
// 响应式数据
const name = reactive(nameWrapper)
name.value = 'hello CQ!'

自定义创建包裹对象看起来没什么问题,但其实是有问题的:

  • 用户为了创建一个响应式的原始值,不得不创建一个包裹对象
  • 用户自定义包裹对象,意味着不规范不统一,可以随意创建、随意命名

封装统一的ref 函数

为了解决自定义包裹对象出现的问题,将创建包裹对象的工作封装到ref函数中,

// 封装ref函数
function ref(val) {
	// 创建包裹对象
	const wrapper = {
		value: val,
	}
	// 创建响应式数据
	return reactive(wrapper)
}

优化ref函数

上面的代码已经可以实现最初的需求了,但是还存在一些缺陷,就是无法区分是原始值的包裹对象、还是非原始值的响应式数据?

// 封装ref函数
function ref(val) {
	// 创建包裹对象
	const wrapper = {
		value: val,
	}
	// 使用Object.defineProperty在wrapper对象上定义一个不可枚举,不可写的属性__v_isRef:true
	Object.defineProperty(wrapper, '__v_isRef', {
		value: true,
	})
	// 创建响应式数据
	return reactive(wrapper)
}

通过 Object.definedProperty 方法为包裹对象 wrapper 设置一个不可枚举、不可写的属性 __v_isRef 且值为 true,用于去代表该对象是一个 ref ,而非原始值的响应式数据对象。

ref 用于解决响应式丢失问题

什么是响应式丢失?

export default {
    setup(){
        const state = reactive({foo: 1, bar: 2})
        return { ...state }
    }
}

这里使用了扩展运算符(...)的方式将响应式数据暴露到外部,这么做会丢失响应式:

return { ...state }
// 等价于
return {foo: 1, bar: 2}

可以看出响应式丢失的原因是向外暴露了一个普通对象,它不具有响应式能力

实现 toRef 函数处理响应式丢失

实际上就是通过返回一个类似ref结构的wrapper对象,但通过为wrapper对象实现getter、setter函数,并在其中返回具体的响应式数据,即保证与原响应式数据之间的联系

function toRef(obj, key) {
	const wrapper = {
		get value() {
			return obj[key]
		},
		set value(val) {
			obj[key] = val
		},
	}
	Object.defineProperty(wrapper, '__v_isRef', {
		value: true,
	})
	return wrapper
}

const state = reactive({foo: 1, bar: 2})
const foo = toRef(state, 'foo')
const bar = toRef(state, 'bar')
effect(() => {
    console.log(state.foo)
})
foo.value++

封装 toRefs 函数

toRef函数存在不足,就是只能对单个key进行处理,如果传入的响应式数据键非常多,效率就很低了,于是通过封装toRefs函数实现批量转换:

function toRefs(obj) {
	const ret = {}
	for (const key in obj) {
		ret[key] = toRef(obj, key)
	}
	return ret
}

const state = reactive({ foo: 1, bar: 2 })
const { foo, bar } = toRefs(state)

为什么需要自动脱 ref

toRefs 带来的问题

toRefs函数解决了响应式丢失的问题,但也带来了新的问题,那就是它会把响应式数据的第一层属性值转换为ref,意味着必须要通过value属性去访问值,这增加了用户的心智负担,因此,需要提供自动脱ref的能力。

自动脱 ref

自动脱ref指的是属性的访问行为,即如果读取的属性是一个ref,则直接将该ref对应的value属性值返回。

可以基于前面定义的__v_isRef标识,通过使用proxytoRefs返回的对象创建代理对象,即通过代理来实现目标:

// 自动脱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[key]访问真实值
			const value = target[key]
			if (value.__v_isRef) {
				value.value = newValue
				return true
			}
			return Reflect.set(target, key, newValue, receiver)
		},
	})
}

实际上,在编写Vue组件时,组件中的setup函数所返回的数据会传递给proxyRefs函数进行处理,这也就是为什么在模板中直接访问ref时,不需要通过value属性访问和设置。