Skip to content

1. 利用effect实现watch的基础功能

watch的核心原理是利用ReactiveEffect来追踪响应式数据的变化,用到了scheduler调度器,把用户传递的source转换一个函数,因为effect需要依赖一个函数来收集依赖:

ts
function watch(source, cb, options) {
    // 1. 创建一个getter函数,用于获取监听的值
    let getter
    if(isRef(source)) {
        getter = () => source.value
    } else if(isReactive(source)) {
        getter = () => source
    } else if(isFunction(source)) {
        getter = source
    }

    // 2. 创建一个effect实例
    const effect = new ReactiveEffect(getter)

    // 3. 设置调度器,当依赖变化时触发回调
    effect.scheduler = job

    // 4. 先执行一次,收集依赖
    effect.run()

    function job() {
        const newValue = effect.run()
        cb(newValue, oldValue)
        oldValue = newValue
    }
}
  • 根据不同的监听源(ref,reactive对象或函数)创建对应的getter
  • 使用ReactiveEffect追踪getter的执行
  • getter依赖的响应式发生变化时,通过scheduler在依赖变化时,执行用户传递的回调

2. 如何获取到oldValue

oldValue可以在首次执行effect收集依赖的时候,拿到它:

ts
let oldValue
function job() {
    const newValue = effect.run()
    cb(newValue, oldValue)
    oldValue = newValue
}
oldValue = effect.run()
  • 首次运行effect.run()获取初始值并保存为oldValue
  • 每次依赖更新时,通过effect.run()获取新值
  • 在回调中传入新旧值,并更新oldValue

3. 停止监听功能

ts
function watch(source, cb, options) {
    const effect = new ReactiveEffect(getter)

    function stop() {
        effect.stop()
    }

    return stop
}
  • ReactiveEffect
ts
class ReactiveEffect {
    // 表示当前是否激活
    active = true
    run() {
        if(!this.active) {
            // 如果未激活,则不收集依赖,直接调用fn
            return this.fn()
        }
    }

    stop() {
        if(this.active) {
            // 开始追踪,会把depsTail设置为undefined
            startTrack(this)
            endTrack(this)
            this.active = false
        }
    }
}
  • 返回一个stop函数
  • stop函数内部调用effect.stop()停止响应式追踪

4. immediate功能实现

ts
if(!immediate) {
    job() // 立即执行一次
} else {
    oldValue = effect.run()
}
  • 如果设置了immediate,立即执行一次job函数
  • 否则只运行effect.run()获取初始值

5. once功能实现

ts
if(once) {
    const _cb = cb
    cb = (...args) {
        _cb(...args)
        stop()
    }
}
  • 保存原始回调
  • 包装新的回调函数,在执行后立即调用stop

6. deep功能实现

ts
if(deep) {
    // 如果 deep 为 true,则调用 traverse 函数,递归触发 getter
    const baseGetter = getter
    // 此处需要考虑递归的层级问题,如果用户传递了层级,根据用户传递的监听层级进行遍历
    const depth = deep === true ? Infinity : deep
    getter = () => traverse(baseGetter(), depth)
}
function traverse(value, depth = Infinity, seen = new Set()) {
    // 如果不是一个对象,或者监听层级到了,直接返回 value
    if (!isObject(value) || depth <= 0) {
        return value
    }

    // 如果之前访问过,那直接返回,防止循环引用栈溢出
    if (seen.has(value)) {
        return value
    }

    // 层级 -1
    depth--
    // 加到 seen 中
    seen.add(value)

    for (const key in value) {
        // 递归触发 getter
        traverse(value[key], depth, seen)
    }
    return value
}
  • 通过traverse函数递归访问对象的所有属性
  • 使用seen Set防止循环引用
  • 支持设置遍历深度

7. onCleanup功能实现

ts
let cleanup = null

function onCleanup(cb) {
    cleanup = cb
}

function job() {
    if (cleanup) {
        cleanup()
        cleanup = null
    }

    const newValue = effect.run()
    cb(newValue, oldValue, onCleanup)
}
  • 提供onCleanup函数给用户注册清理函数
  • 在每次触发回调前执行清理函数
  • 执行完成函数后重置cleanup

8.完整代码实现

ts
function watch(source, cb, options) {
    let { immediate, once, deep } = immediate || {}

    // 1. 创建一个 getter 函数,用于获取监听源的值
    let getter
    if((source)) {
        getter = () => source.value
    } else if (isReactive(source)) {
        getter = () => source
    } else if (isFunction(source)) {
        // 如果source是一个函数,那么getter就等于source
        getter = source
    }

    if(once) {
        // 如果once传了,那就保存一份,新的cb等于直接调用原来的,加上stop停止监听
        const _cb = cb
        cb = (...args) => {
            _cb(...args)
            stop()
        }
    }

    if(deep) {
        const baseGetter = getter
        const depth = deep === true ? Infinity : deep
        getter = () => traverse(baseGetter(), depth)
    }

    let oldValue

    let cleanup = null
    function onCleanup(cb) {
        cleanup = cb
    }

    function job() {
        if (cleanup) {
            // 看一下是否需要清理上一次的副作用,如果有,则清理,并置空
            cleanup()
            cleanup = null
        }
        // 执行effect.run,拿到getter的返回值,不能直接执行,getter。因为要收集依赖
        const newValue = effect.run()
        // 执行用户回调
        cb(newValue, oldValue, onCleanup)
        // 下一次oldValue 就等于这一次的newValue
        oldValue = newValue
    }

    const effect = new ReactiveEffect(getter)

    effect.scheduler = job

    if (immediate) {
        job()
    } else {
        // 拿到oldValue,并且收集依赖
        oldValue = effect.run()
    }

    function stop() {
        effect.stop()
    }

    return stop
}

function traverse(value, depth = Infinity, seen = new Set()) {
    // 如果不是个对象,或者监听层级到了,直接返回value
    if (!isObject(value) || depth <= 0) {
        return value
    }

    // 如果直接前访问过,直接返回,防止循环引用栈溢出
    if (seen.has(value)) {
        return value
    }

    // 层级-1
    depth--

    // 加到seen
    seen.add(value)

    // 递归触发getter
    for (const key in value) {
        traverse(value[key], depth, seen)
    }

    return value
}

Released under the MIT License.