这篇文章主要讲解了“Vue3侦听器的实现原理是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Vue3侦听器的实现原理是什么”吧!
侦听响应式对象
前面我们聊到计算属性,它可以自动计算并缓存响应式数据的值。而如果我们仅需要在响应式数据变化时,执行一些预设的操作,就可以使用
watch侦听器。我们还是先来实现一个最简单的例子,然后来一点一点扩充它。
const data = {foo: 1} const obj = reactive(data) watch(obj, () => { console.log('obj已改变') })
在这个例子中,我们使用了
watch侦听器,当
obj的属性被改变时,控制台应该会打印出
obj已改变。基于前面我们对计算属性的实现,这里我们已经有了一个大概的思路。把
watch视为响应式对象的副作用函数,当响应式对象改变时,触发执行该副作用函数。
想要触发副作用函数,必须先收集它,还记得副作用函数是如何收集的吗?对,当响应式数据被get
时,收集副作用函数。所以首先,我们需要让
watch被响应式对象收集到。
function watch(getter, cb) { effect( () => getter.foo ) }
接着,我们还需要让我们预设的方法被执行。当响应式数据被
set时,触发副作用函数。这里我们想触发的是
cb这个传入的回调函数,这里我们就又能用到实现计算属性时的调度器了,当调度器存在时,
set触发的
trigger会先执行调度器中的函数。
function watch(getter, cb) { effect( () => getter.foo, { scheduler() { cb() } } ) }
一个简单的侦听器已经完成了!这里我们为了简单,把功能写死了,仅支持对
obj.foo的侦听。接下来,我们就要想想,如何实现对响应式对象的任意属性进行侦听?
按照前面的思路,想要实现对响应式对象的任意属性的侦听,就需要我们
get到该对象的每一个属性,这就需要我们对响应式对象进行一次递归遍历。
function traverse(value, seen = new Set()) { // (1) if(typeof value !== 'object' || value === null || seen.has(value)) return seen.add(value) for(const key in value) { traverse(value[key], seen) } return value }
为了避免递归遍历对象时,循环引用造成的死循环,我们在
(1)处创建了
Set,当重复出现相同的对象时,直接返回。
侦听属性值
在Vue3中,我们不能直接侦听响应式对象的属性值。如果需要侦听响应式对象的属性值,就需要一个
getter函数,让侦听器能被响应式对象收集到。
const data = { foo: 1 } const obj = reactive(data) watch( () => obj.foo, () => { console.log('obj.foo已改变') })
指定了属性就意味着,当前的侦听器仅会被指定的属性触发,就无需递归遍历整个响应式对象了。
function watch(getter, cb) { if(typeof getter !== 'function') getter = traverse(getter) // (2) effect( () => getter(), { scheduler() { cb() } } ) }
在(2)处,我们增加了一个判断,如果传入的已经是
getter函数,我们直接使用,如果不是
getter函数,则认为是一个响应式对象,就需要进行递归遍历。
侦听获取新值和旧值
在Vue中我们还需要能够在回调函数
cb()中拿到响应式数据更新前后的新值与旧值。
const data = { foo: 1 } const obj = reactive(data) watch( () => obj.foo, (newValue, oldValue) => { console.log(newValue, oldValue) })
接下来的问题是,如何获取
newValue与
oldValue。
newValue好解决,执行完回调函数
cb()得到的就是
newValue,但这里如何获取
oldValue的值呢?要从
watch中拿到旧值,那就不能让副作用函数被立即执行。这里想到了什么?对,在实现计算属性的时候,我们用到过的
lazy,它可以禁止副作用函数自动执行。
function watch(getter, cb) { if(typeof getter !== 'function') getter = traverse(getter) let oldValue const effectFn = effect( () => getter(), { lazy: true, // (3) scheduler() { cb(oldValue) } } ) oldValue = effectFn() // (4) }
在(3)处我们设置了
lazy开关,设置了
lazy后,副作用函数的执行权就交到了我们自己手上。在(4)处,我们手动执行了副作用函数。这里可以需要我们向前回顾一下,前面我们传入的
getter是一个函数
() => obj.foo,而
effect函数的第一个参数就是真正被执行的副作用函数,所以我们手动执行的,其实就是函数
() => obj.foo,这样我们就拿到了旧值。
如何获取新值呢?在响应式数据的值更新后,副作用函数
effect会被触发执行,当调度器属性存在时,执行调度器。在调度器中,我们可以再次执行副作用函数,通过
() => obj.foo拿到改变后的新值。
function watch(getter, cb) { if(typeof getter !== 'function') getter = traverse(getter) let oldValue, newValue const effectFn = effect( () => getter(), { lazy: true, scheduler() { newValue = effectFn() cb(newValue, oldValue) oldValue = newValue // (5) } } ) oldValue = effectFn() }
在(5)处,执行完回调函数
cb(),我们进行了一下善后工作,更新了
oldValue的值,为下一次回调做准备。
有时,我们还希望侦听器可以在创建时就立即执行回调函数。
const data = { foo: 1 } const obj = reactive(data) watch( () => obj.foo, (newValue, oldValue) => { console.log('newValue:', newValue,', oldValue:', oldValue) }, { immediate: true } )
当
immediate的值为
true时,需要立即执行。明确了需求,我们来完善
watch侦听器。
function watch(getter, cb, options = {}) { if(typeof getter !== 'function') getter = traverse(getter) let oldValue, newValue function job() { // (6) newValue = effectFn() cb(newValue, oldValue) oldValue = newValue } const effectFn = effect( () => getter(), { lazy: true, scheduler: job, } ) if(options.immediate) { // (7) job() } else { oldValue = effectFn() } }
在(6)处,我们抽离了回调函数的执行逻辑,当
options.immediate存在时,直接触发执行。
实现效果