# Vue3 响应式系统实现原理学习总结(四)
随着总结的推进,已经把响应式系统的基础实现,细节问题,还有computed
相关的实现已经总结完。现在集中精力,把最后watch
相关的实现总结完。
watch
实现的步骤比较多,分为基本实现,getter支持,新旧值支持,参数支持,过期副作用处理五部分。内容虽多,但是仍然是循序渐进的过程,也算是学习的乐趣。
watch实现原理
基本实现
要实现一个基本的watch
,只需要递归需要观测的对象,以此触发副作用收集。书中有更基础的实现,就是硬编码观测其中一个属性,触发回调函数。这个非常简单,我就不赘述。
核心代码如下:
function traverse (value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
for (const k in value) {
traverse(value[k], seen)
}
return value
}
function watch (source, cb) {
effect(() => {
traverse(source)
}, {
scheduler() {
cb()
}
})
}
traverse
的过程中,增加seen
以防止死循环。整体代码很清晰,观测对象的本质,就是注册一个递归对象的副作用函数,保证对象每一个属性变更的时候,都会触发这个副作用函数。触发的时候,执行调度器里面的回调函数。这里又是一个调度器函数的应用。watch
的实现需要在computed
的实现之后来总结,调度器需要先实现是原因。
getter支持
如果只想观测响应式对象的某个属性的变更,就可以考虑watch
的第一个参数增加一个getter
,只有getter
返回的值变更的时候,才触发回调函数的执行。
改动也比较简单,直接看代码:
function watch (source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(() => getter(), {
scheduler() {
cb()
}
})
}
新旧值支持
watch
的回调函数需要有新旧值作为参数,以供用户使用。为了实现新值的支持,就需要用到懒执行,这也是watch
的实现需要在computed
的实现之后来总结的原因之一。
懒执行的最重要的作用,是可以初始化获取旧值。
function watch (source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
})
// 别忘了第一次获取
// 为什么这里不触发cb?
// 很简单,effectFn只是触发收集依赖,不是触发依赖
oldValue = effectFn()
}
参数支持:immediate
watch
需要立刻执行回调函数的话,需要通过传入options.immediate
参数来执行。
实现上也比较简单,把调度器中的代码提取到job()
,如果是立即执行的情况下,会执行job()
。
function watch (source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
job()
}
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
有一个点要注意一下,如果是立即执行,oldValue
是undefined
,因为不存在新旧的对比,只有一个值。
参数支持:flush
flush本质上来说,本质上是控制cb
函数的执行时机,有点类似于调度器对于副作用函数的作用。这里不展开说,只演示一下flush
为post
的情况,也就是将job()
加入微任务队列,异步执行job()
。修改的也是调度器的实现。
代码如下:
function watch (source, cb, options = {}) {
...
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
if (options.flush === 'post') {
const promise = Promise.resolve()
promise.then(job)
} else {
job()
}
}
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
过期的副作用处理
简单说说什么是过期的副作用,这个情况一般出现在和回调函数为异步函数的情况下。当异步回调函数的结果,还没有返回的过程中,watch
观测的响应式数据发生了变更,导致有新的异步回调要执行,这时,旧的回调函数的结果已经是过期的结果,即使返回也没有执行后续处理的必要。
为了解决这个问题,要有一个方式可以触发副作用函数的过期状态。刷新状态的时机比较好理解,只要watch
的响应式数据发生了下一次变更的时候,就可以设置上一个回调函数中的状态为过期。
具体实现,则是watch
函数中,给回调函数传递一个onInvalidate
函数,供其注册一个cleanup
函数,用于更新过期状态。
function watch (source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let cleanup
function onInvalidate (fn) {
cleanup = fn
}
// 存储onInvalidate
let oldValue, newValue
const job = () => {
newValue = effectFn()
if (cleanup) cleanup() // 执行回调之前,先cleanup
cb(newValue, oldValue, onInvalidate)
oldValue = newValue
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
if (options.flush === 'post') {
const promise = Promise.resolve()
promise.then(job)
} else {
job()
}
}
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
watch
使用的使用,代码如下:
watch(() => obj.text, async (newVal, oldVal, onInvalidate) => {
let expired = false
onInvalidate(() => {
expired = true
})
const data = await fetchData(2000)
if (!expired) {
console.log(data)
}
})
这里很明显有两个闭包的应用,一个是expired
,一个是cleanup
。可以看出,一般来说,有回调,一般就有闭包出现。
响应式系统实现总结
响应式系统的实现总结,我通过拆成四篇文才总结完,可以看出细节丰富。总结从基础的实现,慢慢到细节问题的解决,最后再到computed
和watch
的实现。
学习的过程,我花了很多遍去熟悉。第一遍是用纸质卡片做笔记,第二遍是隔了一段时间,看着自己的笔记上的原理解析,动手实现代码。第三遍是自己全盘从零实现一遍全部代码,并总结问题。第四遍也是自己从零实现,做最后的巩固。让我开心的是,第四遍学习的时候,最后输出的代码需要调整的部分只有一处小细节。也就是cleanup
函数(不是watch
中的),需要最后清空effectFn.deps数组:effectFn.deps.length = 0
。
当然啦,还有现在的总结。总结全过程,我是对着自己的实现,用自己的话去做的总结,抛开了自己的笔记和书本。
最后,让我最后用一个词总结学习过程吧,「重复」。
转载自:https://juejin.cn/post/7242220704287440954