VUE3之响应系统

VUE3之响应系统

前言
最近在学习VUE3的新特性,记录一下学习成果。

副作用函数

什么是副作用函数?
会产生副作用的函数,或者说会直接或间接影响其他函数的执行结果。
举个简单的例子:一个函数改变了全局变量,这个函数就是副作用函数。

响应式数据

响应式数据的实现思路

拦截对象的读取和设置操作,在读取的时候把相关联的副作用函数存储起来,设置的时候递归执行相关联的副作用函数。
vue3用proxy和reflect实现的,用weakMap、map、set收集依赖集合(与响应式数据相关联的副作用函数)

把get拦截函数里把副作用函数收集起来的逻辑单独封装到一个track函数中,把set拦截函数里把触发副作用函数执行的逻辑单独封装到trigger函数中。

调度执行

可调度是指指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

// 定义一个任务队列
 const jobQueue = new Set()
 // 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

 // 一个标志代表是否正在刷新队列
 let isFlushing = false
 function flushJob() {
  // 如果队列正在刷新,则什么都不做
if (isFlushing) return
 // 设置为 true,代表正在刷新
 isFlushing = true
  // 在微任务队列中刷新 job?ueue 队列
 p.then(() => {
  jobQueue.forEach(job => job())
  }).finally(() => ?
   // 结束后重置 isFlushing
   isFlushing = false
  })
}


 effect(() => {
  console.log(obj.foo)
 }, {
  scheduler(fn) {
    // 每次调度时,将副作用函数添加到 jobQueue 队列中
   jobQueue.add(fn)
    // 调用 flushJob 刷新队列
     flushJob()
  }
 })

 obj.foo++
obj.foo++

可能你已经注意到了,这个功能有点类似于在 Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器,思路与上文介绍的相同。

computed

computed函数接收一个getter函数作为参数,把getter函数作为副作用函数,当响应式数据发生改变时,就会执行该副作用函数,将计算结果作为值范围。

缓存实现思路

新增两个变量:value和dirty。value是缓存上一次计算的值,dirty是一个标识,代表是否需要重新计算。
当响应式变量发生改变时,dirty = true,访问计算属性值的时候就会调用该副作用函数(getter参数),重新计算并返回值。

watch

watch其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
watch 的实现本质上就是利用了 effect 以及options.scheduler 选项。

effect(() => 
	console.log(obj.foo)
	}, {
		scheduler() {
			//当obj.foo的值发生变化时,执行scheduler调度函数
		}
})

如果获取新旧值?
充分利用effect函数的lazy

function watch(source, cb) {
	let getter
	if (typeof source === 'function') {
		getter = source
	} else {
		getter = () => traverse(source)
	}
	//定义新值与旧值
	let oldValue, newValue
	//使用effect注册副作用函数 时,开启lazy选项,并把返回值存储到effectFn中,以便后续手动调用
	const effectFn = effect (
		() => getter(),
		{
			lazy: true,
			scheduler() {
				//在scheduler中重新执行副作用函数,得到的是新值
				newValue = effectFn()
				//将旧值和新值作为回调函数的参数
				cb(newValue, oldValue)
				//更新旧值,不然下次会得到错误的旧值
				oldValue = newValue
				
			}
		}
	)
	//手动调用副作用函数,拿到的值就是旧值
	oldValue = effectFn()
}

回调执行的时机:flush
pre:默认值,创建时立即执行(组件更新前)
post:异步延迟执行,把副作用函数放到一个微任务(promise实现)队列中,等到dom更新结束后执行(组件更新后)
sync:同步执行

过期的副作用:

let finalData
watch(obj, async () => {
	//发送并等待网络请求
	const res = await fetch('/request')
	//将请求结果赋值给 finalData
	finalData = res
)

在上面的代码片段中,如果在第一次发送请求A的结果返回之前,改变了obj的字段,就会触发第二次请求B的发送,这个时候请求A的结果就是过期的副作用。为了避免竟态问题导致的错误结果,需要一个让副作用过期的手段,具体解决思路如下:

watch(obj, async (newValuw, oldValue, onInvalidate) => {
	//定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
	let expired = false
	//调用onInvalidate() 函数注册一个过期回调
	onInvalidate(() => {
		//过期时,将expired设置为true
		expired = true
	})
	//发送网络请求
	const res = await fetch ('/request')
	//只有当该副作用函数的执行没有过期时,才会执行后续操作
	if (!expired) {
		finalData = res
	}
})

非基本数据类型的响应式reactive

在vue3中,响应式数据是基于Proxy和Reflect实现的,它允许我们拦截并重新定义一个对象的基本操作。

Proxy

const obj = {
            foo: 'Secret',
            get secret() {
                return this.foo;
            }
        };

        const p = new Proxy(obj, {
            get(target, key, recevier){
                return key + '11'
            },
        })
        console.log(p.foo) //输出foo11

proxy可以拦截读取或者设置的方法。
拦截方法:
obj.*:用的是get方法拦截
key in obj:用的是has方法拦截,内部方法是hasProperty
for…in: 用的是ownKeys拦截
delete:用的是deleteProperty拦截

Reflect

	const obj = {
            foo: 'Secret',
            get secret() {
                return this.foo;
            }
        };

        // 使用 Reflect.get() 获取 secret 属性的值,并传递 obj 作为 receiver
        const secretValue = Reflect.get(obj, 'secret', obj);
        console.log(secretValue); // 输出: 'Secret'

        // 如果没有传递 receiver,或者 receiver 不是 obj,getter 函数中的 this 可能不会指向正确的对象
        const incorrectSecretValue = Reflect.get(obj, 'secret', {});
        console.log(incorrectSecretValue); // 输出: undefined,因为 _private 属性在 {} 上不存在

Reflect.get(target, key, receiver) 中的receiver可以简单理解为函数调用中的this,this由原始对象obj变成第三个参数receiver。

reactive代码片段

function reactive (obj) {
	return new Proxy (obj, {
		get (target, key, reveiver) {
			//代理对象通过raw属性访问原始数据
			if (key === 'raw') {
				return target
			}
			track(target, key)
			return Reflect.get (target, key, reveiver)
		},
		set (target, key, receiver) {
			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
		}
	})
}

只有当receiver是target的代理对象时才触发更新,这样就能屏蔽由原型引起的更新,从而避免不必要的更新操作。

浅响应和深响应

深响应:在get拦截函数里面加一个判断

const res = Reflect.get(target, key, receiver)
if (typeof res === 'object') && res !== null) {
	//调用reactive将结果包装成响应式数据并返回
	return reactive(res)
}

并非所有情况都需要深响应的,这就催生了shallowReactive,即浅响应。

//封装createReactive 函数,接受一个参数isShallow,代表是否浅响应,默认为false,即深响应
function createReactive(obj, isShallow = false) {
	return new Proxy(obj, {
		get(target, key, receiver) {
			//省略部分逻辑

			//新增一个判断:如果是浅响应,则直接返回原始值
			if (isShallow) {
				return res
			}
		}
	})
}

只读和浅只读

只读:拦截set和deleteProperty函数,如果是只读,则打印警告信息并返回。拦截get函数,并在get函数内递归调用readonly将数据包装成只读的代理对象,并将其返回。如果一个数据是只读,那么就没有必要为只读数据建立响应联系了。也不需要调用track函数追踪响应,不需要调用trigger函数执行想关联的副作用函数了。

function createReactive (obj, isShallow = false, isReadonly = false) {
	return new Proxy(obj, {
		get (target, key, receiver) {
			//省略其他逻辑的代码
			if (!isReadonly) {
				track(traget, 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)
			}
		},
		set(target, key, receiver) {
		//省略其他逻辑代码
			if (isReadonly) {
				console.warn(`属性${key}是只读的`)
				return true
			}
		},
		deleteProperty(target, key) {
			//省略其他逻辑代码
			if (isReadonly) {
				console.warn(`属性${key}是只读的`)
				return true
			}
		}
	})
}
//深只读
fuunction readonly (obj) {
	return createReactive(obj, false, true)
}

浅只读:shallowReadonly,只需要修改createReadonly的第二个参数即可

function shallowReadonly(obj) {
	return createReactive(obj, true, true)
}

基本数据类型的响应式ref

javaScript中的proxy无法提供对基本数据类型的代理,对于基本数据类型的代理的解决办法:
用一个对象去包裹基本数据类型。

let str = '123'  //无法代理str

//可以使用proxy代理wrapper,间接实现对基本数据类型的拦截
const wrapper = {
	str: '123'
}
const name = reactive(wrapper)

但是这样用户每创建一个响应式的基本数据类型,就要创建一个包裹对象。为此,我们可以封装ref函数

function ref(val) {
	const wrapper = {
		value: val
	}
	Object.defineProperty(wrapper, '__v_isRef', {
		value: true
	})
	return reactive(wrapper)
}

使用Object.defineProperty为包裹的wrapper定义一个不可以枚举且不可以写的属性__v_isRef,它的值为true,代表这是一个ref对象,而非普通对象。

响应丢失问题

用reactive定义的响应式数据,解构赋值后会变成普通数据,就会产生响应丢失的问题。
可以封装toRefs函数来解决此问题

//obj是响应式数据, key 是键值
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
}

function toRefs(obj) {
	const ret = {}
	//使用for...in 循环遍历对象
	for (const key in obj) {
		//调用toRef函数完成数据的转换
		ret[key] = toRef(obj, key)
	}
	return ret
}

ref数据模板自动解包

ref数据需要通过value属性访问值,在vue的模板中,会自动解包,可以直接调用。
实现思路:在get拦截函数中加一个判断,用__v_isRef属性判断,是ref数据就返回value.value

function proxyRefs(target) {
	return new Proxy(target, {
		get(target, key ,receiver) {
			const vlaue = Reflect.get(target, key, receiver)
			return value.__v_isRef ? value.value : value
		},
		set(target, key, receiver) {
			//通过target读取真实值
			const value = target[key]
			//如果是ref,则设置其对应的value属性
			if (value.__v_isRef) {
				value.value = newValue
				return true
			}
			return Reflect.set(target, key, newValue, receiver)
		}
	})
}

在编译模板的时候,组件中的setup函数返回的数据会传递给proxyRefs函数进行处理。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值