likes
comments
collection
share

响应式对象的实现

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

基本的响应式功能

// 创建一个用于存储依赖的Set
let bucket = new Set()

const data = { text: 'hello world' }
// 代理对象
const obj = new Proxy(data, {
    get (target, key) {
        bucket.add(effect)
        return target[key]
    },
    set (target, key, val) {
        if (target[key] === val) return
        target[key] = val
        bucket.forEach(effectFn => {
            effectFn()
        })
    }
})

function effect () {
    console.log(obj.text)
}

effect()
setTimeout(() => {
    obj.text = 'hello vue3'
}, 1000)

上面代码演示了一个最基本的响应式对象,但缺点也很明显,每次收集依赖都需要对应函数的名称,一旦我们换个函数名称代码便失效了。为了不受函数名称限制,现在来改进一下,每次effect函数执行时先把传入的函数赋值给一个全局变量activeEffect,再执行传入的函数。

const obj = new Proxy(data, {
    get (target, key) {
        if (!activeEffect) return
        bucket.add(activeEffect)
        return target[key]
    },
    set (target, key, val) {
        if (target[key] === val) return
        target[key] = val
        bucket.forEach(effectFn => {
            effectFn()
        })
    }
})

// 声明一个全局变量来表示当前依赖
let activeEffect
// 创建一个依赖函数生成器
function effect (fn) {
    activeEffect = fn
    fn()
}

effect(() => {
    console.log(obj.text)
})

setTimeout(() => {
    obj.text = 'hello vue3'
}, 1000)

到目前为止,我们已经可以通过调用依赖函数生成器来生成依赖,并在get拦截中收集依赖,但仍存在一个致命问题,即不管访问对象的任何属性都会触发所有的依赖。因此我们需要修改依赖存储桶的数据结构,为每个对象的每个属性创建单独的存储桶。

// WeakMap --> Map --> Set
// WeakMap对象的key为target对象,value为一个Map对象
// Map对象的key为target对象的属性,value为Set数据结构,作为各个对象下各个属性的
let bucket = new WeakMap()

const obj = new Proxy(data, {
    get (target, key) {
        if (!activeEffect) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
        return target[key]
    },
    set (target, key, val) {
        if (target[key] === val) return
        target[key] = val
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const deps = depsMap.get(key)
        deps && deps.forEach(effectFn => {
            effectFn()
        })
    }
})

到这里,我们就成功将各个对象下各个属性的依赖集合区分开。之所以用WeakMap作为bucket的第一层数据结构,就是为了利用它的弱引用性,使得浏览器的垃圾回收机制可以在对象调用结束后将其回收。最后我们再把getset里的拦截操作抽离成两个函数,也即track()trigger(),完整代码如下:

let bucket = new WeakMap()

const data = { text: 'hello world' }
const obj = new Proxy(data, {
    get (target, key) {
        track(target, key)
        return target[key]
    },
    set (target, key, val) {
        if (target[key] === val) return
        target[key] = val
        trigger(target, key)
    }
})
// 收集依赖
function track (target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
}
// 触发依赖
function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    deps && deps.forEach(effectFn => {
        effectFn()
    })
}

// 声明一个全局变量来表示当前依赖
let activeEffect
// 创建一个依赖函数生成器
function effect (fn) {
    activeEffect = fn
    fn()
}

effect(() => {
    console.log(obj.text)
})

setTimeout(() => {
    obj.text = 'hello vue3'
}, 1000)

分支切换带来的额外开销

分支切换是代码中很常见的情景,但这些情况很有可能产生不必要的依赖更新,示例如下

const data = { text: 'hello world', ok: true }
const obj = new Proxy(data, {...})

effect(() => {
    console.log(obj.ok ? obj.text ? 'no')
})

setTimeout(() => {
    obj.ok = false
}, 1000)
setTimeout(() => {
    obj.text = 'hello vue3'
}, 2000)

我们可以观察到如下结果,调用effect生成器时第一次打印hello world,这时data对象下textok属性的依赖存储桶内都增加了一个依赖;接着我们更改ok属性的值为false,执行它内部的依赖打印出第二个结果no,此时打印结果已经与obj.text的值无关,结果会一直为no;最后text属性变化时我们依然观察到依赖触发了,打印出no,这显然触发了不必要的依赖更新。 响应式对象的实现 为了避免产生这种因分支切换带来的不必要开销,我们可以在每次依赖执行前把该依赖从所有与之相关联的依赖存储桶删除,执行完后再重新收集依赖,确保没有不必要的依赖产生。

function track (target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    // add
    activeEffect.deps.add(deps)
}

function effect (fn) {
    const effectFn = () => {
        clear(effectFn)
        activeEffect = effectFn
        fn()
    }
    // 存储相关联的依赖存储桶
    effectFn.deps = new Set()
    effectFn()
}
// 从依赖存储桶中删除依赖
function clear (effectFn) {
    effectFn.deps.forEach(effectSet => {
        effectSet.delete(effectFn)
    })
    effectFn.deps.clear()
}

上面代码为每个依赖创建了一个Set数据结构用来存储与依赖相关联的依赖存储桶,并且在触发依赖之前把依赖从这些存储桶中删除,实现了依赖存储桶中只存储必要依赖的效果,这时我们再去执行代码,会发现产生了无限循环。原因很简单,我们的操作本质上就是Set在每次遍历中先删除后添加,即

const set = new Set([1])
set.forEach(item => {
    set.delete(1)
    console.log(item)
    set.add(1)
})

根据MDNSet关于forEach的描述:每个值都访问一次,除非在forEach()完成之前删除并重新添加它,在访问之前删除的值不会调用callback,在forEach()完成之前添加的新值将被访问。要解决这种无限循环也很简单,只需复制一份Set即可

function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    // add
    const effectsToRun = new Set([...deps])
    effectsToRun.forEach(effectFn => {
        effectFn()
    })
}

响应式对象的实现

effect嵌套

接下来我们考虑effect嵌套场景。下面这个例子中首先执行外层依赖函数,遇到内层effect,把内层函数添加到bar属性对应的存储桶中,内层effect执行完毕,继续往下执行,把依赖添加到foo属性对应的存储桶中最后再修改obj.foo的值。

const data = { foo: 'foo', bar: 'bar' }
const obj = new Proxy(data, {...})

effect(() => {
    effect(() => {
        console.log('effect_2', obj.bar)
    })
    console.log('effect_1', obj.foo)
})

setTimeout(() => {
    obj.foo = 'new foo'
}, 1000)

预想中我们应该在间隔一秒后打印出先后打印出 effect_2 bar 和 effect_1 new foo,但实际结果却是只打印出 effect_2 bar,明明想触发的是foo属性下的依赖,却执行了bar属性的依赖。细想之下不难发现,这是由于activeEffect在执行完内层effect后变为了内层函数,等到添加外层依赖时我们就把内层函数添加到了foo属性对应的存储桶中。

为了解决这一问题,我们设置一个activeEffect的存储栈,每次执行依赖前把依赖入栈,执行完后再出栈,并且让activeEffect指向栈顶,这样就达到了复原activeEffect的目的。

let activeEffect
// add
let activeEffectStack = []

function effect (fn) {
    const effectFn = () => {
        clear(effectFn)
        activeEffect = effectFn
        // add
        activeEffectStack.push(activeEffect)
        fn()
        // add
        activeEffectStack.pop()
        activeEffect = activeEffectStack[activeEffectStack.length - 1]
    }
    effectFn.deps = new Set()
    effectFn()
}

响应式对象的实现

无限循环

对于依赖函数中同时存在读取和设置的操作时,例如最常见的自增自减操作,会出现Maximum call stack size exceeded栈溢出错误。

const data = { num: 0 }
const obj = new Proxy(data, {...})

effect(() => {
    obj.num++
})

在上述代码中,自增等价于obj.num = obj.num + 1。先读取num属性的值,往num属性对应的存储桶中添加依赖,然后设置num属性的值,开始遍历存储桶中的依赖并执行,这就导致了无限循环的出现。为了避免这一情况的出现,我们需要在trigger()方法上遍历依赖时增加额外判断。

function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    // add
    const effectsToRun = new Set()
    deps && deps.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(effectFn => {
        effectFn()
    })
}

调度函数

为了增强依赖函数的可自定义性,我们可以给effect生成器传入第二个参数,在里面进行一些自定义操作,从而达到更改执行顺序、执行次数等目的。下面通过在options参数中设置一个scheduler调度函数,把依赖函数作为参数传递给调度函数,在trigger触发依赖更新时执行调度函数。

function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    const effectsToRun = new Set()
    deps && deps.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(effectFn => {
        // add
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

function effect (fn, options = {}) {
    const effectFn = () => {
        clear(effectFn)
        activeEffect = effectFn
        activeEffectStack.push(activeEffect)
        fn()
        activeEffectStack.pop()
        activeEffect = activeEffectStack[activeEffectStack.length - 1]
    }
    // add
    effectFn.options = options
    effectFn.deps = new Set()
    effectFn()
}

控制执行顺序

本质是通过异步任务来调整执行顺序

effect(() => console.log(obj.text), {
    scheduler (fn) {
        setTimeout(fn)
    }
})
console.log(1)
obj.text = 'hello vue3'
console.log(3)

控制执行次数

下面例子中我们执行了三次自增操作触发三次依赖执行,但其实我们知道前两次只是一个过渡状态,需要的只有最终结果。

const data = { foo: 0 }
const obj = new Proxy(data, {...})

let taskQueue = new Set()
let isFlush = false
function flushJob () {
    if (isFlush) return
    isFlush = true
    Promise.resolve().then(() => {
        taskQueue.forEach(task => task())
    }).finally(() => {
        isFlush = false
    })
}
effect(() => {
    console.log(obj.foo)
}, {
    scheduler (fn) {
        taskQueue.add(fn)
        flushJob()
    }
})
obj.foo++
obj.foo++
obj.foo++

结果如下,修改了三次foo属性的值,只在最后一次修改完成后触发依赖。 响应式对象的实现

Computed计算属性

懒执行

计算属性具有懒执行特点,定义时不会执行,只有在使用到计算属性的时候才会执行计算过程。首先在包装好的依赖函数中获取回调函数的结果并返回,然后在options上定义lazy属性,为false时立即执行依赖函数,否则就把依赖函数返回等待自定义执行。

const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {...})

function effect (fn, options = {}) {
    const effectFn = () => {
        clear(effectFn)
        activeEffect = effectFn
        activeEffectStack.push(activeEffect)
        // add
        const res = fn()
        activeEffectStack.pop()
        activeEffect = activeEffectStack[activeEffectStack.length - 1]
        // add
        return res
    }
    effectFn.options = options
    effectFn.deps = new Set()
    // add
    if (!options.lazy) {
        effectFn()
    } else {
        return effectFn
    }
}

function computed (getter) {
    const effectFn = effect(getter, {
        lazy: true
    })
    const obj = {
        get value () {
            const res = effectFn()
            return res
        }
    }
    return obj
}

const sum = computed(() => obj.foo + obj.bar)

上面代码中往computed生成器中传递一个getter函数,首先获取effect生成器返回的依赖函数,然后定义一个对象,设置名为value的访问器属性,访问器内部返回依赖函数的执行结果,这样就实现了通过.value调用计算属性的效果。

缓存

细看上面的代码,可以发现每次调用计算属性都会重新计算结果,不管相关的属性值是否发生变化,缺少了计算属性的关键特性:缓存。

function computed (getter) {
    // 缓存值
    let cacheValue
    // 是否为脏数据,即是否需要重新计算结果
    let dirty = true
    const effectFn = effect(getter, {
        lazy: true,
        // add
        scheduler () {
            if (!dirty) {
                dirty = true
            }
        }
    })
    const obj = {
        get value () {
            // add
            if (dirty) {
                cacheValue = effectFn()
                dirty = false
            }
            return cacheValue
        }
    }
    return obj
}

修改部分中新增了两个变量,一个用来存储缓存值,一个用来判断是否为脏数据,初始为true。在执行完第一次计算后,cacheValue存储依赖函数的执行结果,并把dirty置为false,后续再继续调用计算属性时,不会再执行依赖函数而是直接返回缓存值。直到与计算属性相关的属性发生改变时,才会通过设置的调度函数把dirty置为true,并重新计算结果。

响应式

当计算属性用在依赖函数上时,我们希望同proxy代理对象一样产生响应式的效果。然而已有的代码进行测试,foo属性值的变化未能触发依赖函数的执行。

const sum = computed(() => obj.foo + obj.bar)

effect(() => {
    console.log(sum.value)
})
obj.foo++

本质上,effect内部调用计算属性就是effect的嵌套。在内部effect中访问了obj代理对象,会收集内部依赖;外部effect中访问sum计算属性,但由于没有对sum进行代理拦截,并没有收集依赖和触发依赖的行为产生。因此我们需要主动去进行收集和触发的操作,在get访问器属性返回前收集依赖,在调度函数执行时触发依赖。

function computed (getter) {
    // 缓存值
    let cacheValue
    // 是否为脏数据,即是否需要重新计算结果
    let dirty = true
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            if (!dirty) {
                dirty = true
                // add
                trigger(obj, 'value')
            }
        }
    })
    const obj = {
        get value () {
            if (dirty) {
                cacheValue = effectFn()
                dirty = false
            }
            // add
            track(obj, 'value')
            return cacheValue
        }
    }
    return obj
}

watch监听器

基本监听功能

监听器可以监听数据的变化,一旦变化发生便执行相应的回调函数。在vue中,watch接收三个参数sourcecboptions,分别表示数据源、回调和监听器设置,下面先来实现基本监听功能。

function watch (source, cb, options = {}) {
    effect(
        // 遍历对象的所有属性,收集依赖
        () => traversal(source),
        {
            scheduler () {
                cb()
            }
        }
    )
}

// 遍历对象,seen是为了防止循环引用
function traversal (obj, seen = new Set()) {
    if (typeof obj !== 'object' || obj === null || seen.has(obj)) return
    seen.add(obj)
    for (let key in obj) {
        traversal(obj[key], seen)
    }
    return JSON.parse(JSON.stringify(obj))
}

source

监听器接收的数据源不仅可以是对象,也可以是一个函数。

function watch (source, cb, options = {}) {
    // add
    let getter
    if (typeof source === 'object') {
        getter = () => traversal(source)
    } else if (typeof source === 'function') {
        getter = source
    } else {
        throw new TypeError('wrong type of source')
    }
    
    effect(getter, {
        scheduler () {
            cb()
        }
    })
}

传递新旧值

监听器触发时,会把数据源的新旧值传递给回调函数作为参数,现在问题就变为了要如何要获取这两个值?答案很简单:执行依赖函数拿到返回值。首先,options设置lazytrue以获取依赖函数,并立即执行拿到第一个oldVal也即初始值;然后在scheduler函数体中再次执行依赖函数获取newVal,传递给回调函数作为参数;最后需要把旧值赋为新值以待下次监听器触发时使用。

function watch (source, cb, options = {}) {
    let getter
    if (typeof source === 'object') {
        getter = () => traversal(source)
    } else if (typeof source === 'function') {
        getter = source
    } else {
        throw new TypeError('wrong type of source')
    }
    // 传递给回调的参数
    let oldVal, newVal
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            // add
            newVal = effectFn()
            cb(newVal, oldVal)
            oldVal = newVal
        }
    })
    // add
    oldVal = effectFn()
}

options

watch可以接收第三个参数options,我们可以通过传入options来控制回调,options的选项包括immediateflush等。

立即执行的watch

watch默认是懒执行的,仅当数据源发生变化时才会执行回调。某些情况下我们需要在创建监听器时立即执行一遍回调,通过immediate: true来强制回调立即执行。

function watch (source, cb, options = {}) {
    let getter
    if (typeof source === 'object') {
        getter = () => traversal(source)
    } else if (typeof source === 'function') {
        getter = source
    } else {
        throw new TypeError('wrong type of source')
    }
    // add
    const execute = () => {
        newVal = effectFn()
        cb(newVal, oldVal)
        oldVal = newVal
    }
    // 传递给回调的参数
    let oldVal, newVal
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            execute()
        }
    })
    // add
    if (options.immediate) {
        execute()
    } else {
        oldVal = effectFn()
    }
}

回调的触发时机

设置flush: post可以延迟回调的执行

function watch (source, cb, options = {}) {
    // ...
    
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            // add
            if (options.flush === 'post') {
                Promise.resolve().then(execute)
            } else {
                execute()
            }
        }
    })
    
    // ...
}

竞态问题

实际项目中,watch的回调时常会出现异步任务,在连续触发回调的情况下就有可能出现竟态问题,下面通过例子来解释一下什么是竟态问题。

// 创建两个异步任务
const task1 = () => {
    console.log('task1')
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('task1')
        }, 1000)
    })
}
const task2 = () => {
    console.log('task2')
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('task2')
        }, 500)
    })
}

const data = { foo: 1 }
const obj = new Proxy(data, {...})

let final
watch(obj, async () => {
    let res
    if (obj.foo === 1) {
        res = await task1()
    } else {
        res = await task2()
    }
    final = res
}, { immediate: true })

setTimeout(() => {
    obj.foo++
}, 200)
setTimeout(() => {
    console.log('final ==>', final)
}, 1500)

我们首先创建两个异步任务,然后在watch回调中根据obj对象的值来决定执行哪个任务。设置immediate: true触发第一次回调,此时执行task1task1执行完成后返回值赋给final;通过setTimeout修改obj.foo的值触发第二次回调,此时执行task2task2执行完成后返回值赋给final。由于task1task2消耗的时间更多,尽管task2后执行却先拿到返回值,赋给final;等到task1结束后也把结果赋给final,所以我们取得的最终值是task1

响应式对象的实现 通常情况下,我们想要拿到的值都是后执行任务的返回结果,那么应该如何避免这种竟态问题呢?我们需要在后执行任务执行完成后让之前的任务处于“失效”状态,不再把返回值赋值给final,由此引入了watch中回调函数的第三个参数onInvalidate

function watch (source, cb, options = {}) {
    let getter
    if (typeof source === 'object') {
        getter = () => traversal(source)
    } else if (typeof source === 'function') {
        getter = source
    } else {
        throw new TypeError('wrong type of source')
    }
    // add
    let cleanup
    const onInvalidate = (fn) => {
        cleanup = fn
    }
    const execute = () => {
        newVal = effectFn()
        // add
        if (cleanup) {
            cleanup()
        }
        cb(newVal, oldVal, onInvalidate)
        oldVal = newVal
    }
    // 传递给回调的参数
    let oldVal, newVal
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            execute()
        }
    })
    if (options.immediate) {
        execute()
    } else {
        oldVal = effectFn()
    }
}

onInvalidate参数接收一个函数作为参数,函数内部仅是把传入的函数赋值给cleanup,然后在watch的回调触发前调用cleanup函数。

let final
watch(obj, async (newVal, oldVal, onInvalidate) => {
    let res, isExpire = false
    onInvalidate(() => {
        isExpire = true
    })
    if (obj.foo === 1) {
        res = await task1()
    } else {
        res = await task2()
    }
    if (!isExpire) {
        final = res
    }
}, { immediate: true })

setTimeout(() => {
    obj.foo++
}, 200)
setTimeout(() => {
    console.log('final ==>', final)
}, 1500)

上述代码中首先声明了isExpire标识是否失效,接着往onInvalidate传入了一个更改isExpire变量的函数。执行第一次回调前,cleanup为空,执行完成后cleanup为一个函数,isExpirefalse;执行第二次回调前,执行cleanup函数,把第一个回调函数置为“失效”状态,而后执行第二次回调,task2结束后把值赋给final;接着等到task1完成,由于第一个回调函数内的isExpire已经为true,所以不会把返回值赋给final响应式对象的实现

总结

本文主要是为了总结近期的学习内容,从基础的proxy响应式系统出发,一步步对其进行完善,进而实现简易版的computed计算属性和watch监听器,其中仍有许多不足,如Reflect的使用、深度监听等等都还有待实现。

完整代码如下:

let bucket = new WeakMap()

const data = { text: 'hello world', ok: true, foo: 1, bar: 2 }
const obj = new Proxy(data, {
    get (target, key) {
        track(target, key)
        return target[key]
    },
    set (target, key, val) {
        if (target[key] === val) return
        target[key] = val
        trigger(target, key)
    }
})
// 收集依赖
function track (target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    activeEffect.deps.add(deps)
}
// 触发依赖
function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    const effectsToRun = new Set()
    deps && deps.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

// 声明一个全局变量来表示当前依赖
let activeEffect
// 当前依赖存储栈
let activeEffectStack = []
// 依赖生成器
function effect (fn, options = {}) {
    const effectFn = () => {
        clear(effectFn)
        activeEffect = effectFn
        activeEffectStack.push(activeEffect)
        const res = fn()
        activeEffectStack.pop()
        activeEffect = activeEffectStack[activeEffectStack.length - 1]
        return res
    }
    effectFn.options = options
    effectFn.deps = new Set()
    if (!options.lazy) {
        effectFn()
    } else {
        return effectFn
    }
}
// 从依赖存储桶中删除依赖
function clear (effectFn) {
    effectFn.deps.forEach(effectSet => {
        effectSet.delete(effectFn)
    })
    effectFn.deps.clear()
}

// computed
function computed (getter) {
    // 缓存值
    let cacheValue
    // 是否为脏数据,即是否需要重新计算结果
    let dirty = true
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            if (!dirty) {
                dirty = true
                trigger(obj, 'value')
            }
        }
    })
    const obj = {
        get value () {
            if (dirty) {
                cacheValue = effectFn()
                dirty = false
            }
            track(obj, 'value')
            return cacheValue
        }
    }
    return obj
}

// watch
function watch (source, cb, options = {}) {
    let getter
    if (typeof source === 'object') {
        getter = () => traversal(source)
    } else if (typeof source === 'function') {
        getter = source
    } else {
        throw new TypeError('wrong type of source')
    }
    let cleanup
    const onInvalidate = (fn) => {
        cleanup = fn
    }
    const execute = () => {
        newVal = effectFn()
        if (cleanup) {
            cleanup()
        }
        cb(newVal, oldVal, onInvalidate)
        oldVal = newVal
    }
    // 传递给回调的参数
    let oldVal, newVal
    const effectFn = effect(getter, {
        lazy: true,
        scheduler () {
            execute()
        }
    })
    if (options.immediate) {
        execute()
    } else {
        oldVal = effectFn()
    }
}

// 遍历对象,seen是为了防止循环引用
function traversal (obj, seen = new Set()) {
    if (typeof obj !== 'object' || obj === null || seen.has(obj)) return
    seen.add(obj)
    for (let key in obj) {
        traversal(obj[key], seen)
    }
    return JSON.parse(JSON.stringify(obj))
}
转载自:https://juejin.cn/post/7187673192624816183
评论
请登录