likes
comments
collection
share

# Vue3 响应式系统实现原理学习总结(四)

作者站长头像
站长
· 阅读数 62

随着总结的推进,已经把响应式系统的基础实现,细节问题,还有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()
    }
}

有一个点要注意一下,如果是立即执行,oldValueundefined,因为不存在新旧的对比,只有一个值。

参数支持:flush

flush本质上来说,本质上是控制cb函数的执行时机,有点类似于调度器对于副作用函数的作用。这里不展开说,只演示一下flushpost的情况,也就是将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。可以看出,一般来说,有回调,一般就有闭包出现。

响应式系统实现总结

响应式系统的实现总结,我通过拆成四篇文才总结完,可以看出细节丰富。总结从基础的实现,慢慢到细节问题的解决,最后再到computedwatch的实现。

学习的过程,我花了很多遍去熟悉。第一遍是用纸质卡片做笔记,第二遍是隔了一段时间,看着自己的笔记上的原理解析,动手实现代码。第三遍是自己全盘从零实现一遍全部代码,并总结问题。第四遍也是自己从零实现,做最后的巩固。让我开心的是,第四遍学习的时候,最后输出的代码需要调整的部分只有一处小细节。也就是cleanup函数(不是watch中的),需要最后清空effectFn.deps数组:effectFn.deps.length = 0

当然啦,还有现在的总结。总结全过程,我是对着自己的实现,用自己的话去做的总结,抛开了自己的笔记和书本。

最后,让我最后用一个词总结学习过程吧,「重复」。

转载自:https://juejin.cn/post/7242220704287440954
评论
请登录