vue.js设计与分析(三) 阅读有感

本文深入探讨了Vue中计算属性和watch的实现机制。通过讲解`lazy`选项,展示了如何控制副作用函数的执行时机。计算属性通过getter函数实现懒计算和缓存,确保值更新时的高效性。同时,详细解析了watch的工作流程,包括监听响应式数据变化和回调执行。文章还提供了简易实现的代码示例,帮助理解这两个核心概念。
摘要由CSDN通过智能技术生成
计算属性与 lazy

当我们不希望effect中的副作用函数立即执行时,我们可以在options添加一个变量来让用户控制它执行与否

//我们希望通过
effect(()=>{
    console.log('哈哈哈')
},{
    lazy:true
})
//我们就要在 effect 函数中修改 effectFn
function effect(fn,options = {}){
    const effectFn = ()=>{
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.options = options
    effectFn.deps = []
    //只有当lazy为false的时候执行
    if(!options.lazy){
        effectFn()
    }
    // 否则,不执行。将当前这个副作用函数返回
    return effectFn
}

但仅仅时这样的话,意义不是很大,如果我们将传递给effect的函数作为一个getter,这个getter可以返回任何值。这就要求我们在手动调用副作用函数时,就能够拿到其返回值,但我们目前的effectFn是没有返回值的(undefined)

    const effectFn = ()=>{
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        const res = fn() //新增
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res  // 新增
    }
//当lazy为true时,effect返回值就为 ()=>obj.foo + obj.bar 。与effectFn对应
const effectFn = effect(()=>obj.foo + obj.bar,{lazy:true})
// 执行effectFn ,就能拿到这个 getter 的返回值
const value = effectFn()

此时,我们的计算属性就能够大致实现了

function computed(getter){
    //当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
    const effectFn = effect(getter,{
        lazy:true
    })
    const obj = {
        //当读取 obj 中的值的时候,在调用副作用函数
        get value(){
            return effectFn()
        }
    }
    return obj
}

但此时只做到了懒计算,也就是当读取对象值时才触发。没有缓存,多次访问,副作用函数仍然会执行

function computed(getter){
    //用来缓存上一次的值
    let value
    // 用来标识是否要重新计算值,为true意味着 '脏',需要重新计算
    let dirty = true
    //当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
    const effectFn = effect(getter,{
        lazy:true,
        // 当 obj中有值发生变化时,将dirty 脏值设置为true即可。
        scheduler(){
            dirty = true
        }
    })
    const obj = {
        //当读取 obj 中的值的时候,在调用副作用函数
        get value(){
            if(dirty){
                //当值为脏的时候,就需要重新计算
                value = effectFn()
                // 计算完后,将 dirty为false
                dirty = false
            }else{
                return value
            }
        }
    }
    return obj
}

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

目前,还有个缺陷

//当我们在另外一个effect中读取计算属性的值时
const sum = computed(()=>obj.foo + obj.bar)
effect(()=>{
    //在该副作用函数中读取sum.value
    console.log(sum.value)
})
// 修改obj.foo
obj.foo++

//我们希望修改obj.foo,副作用函数就重新执行。但是我们会发现修改obj.foo的值并不会触发副作用函数的执行

本质上这就是一个effect的嵌套,每个计算属性都有自己的effect,并且时懒执行的,当加载的时候再执行。对于计算属性的getter来说,它里面的只会把自己内部的effect收集为依赖集合,当把计算属性用于另外一个effect,就会发生effect嵌套,计算属性内部的effect不会被外层中的收集为依赖集合

//当读取计算属性的值的时候,我们手动调用track追踪,当依赖的响应式数据发生变化的时候,手动调用trigger函数触发响应
function computed(getter){
    //用来缓存上一次的值
    let value
    // 用来标识是否要重新计算值,为true意味着 '脏',需要重新计算
    let dirty = true
    //当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
    const effectFn = effect(getter,{
        lazy:true,
        // 当 obj中有值发生变化时,
        // 将dirty 脏值设置为true即可。 手动调用trigger 触发执行副作用函数
        scheduler(){
            dirty = true
            trigger(obj,'value')
        }
    })
    const obj = {
        //当读取 obj 中的值的时候,在调用副作用函数
        get value(){
            if(dirty){
                //当值为脏的时候,就需要重新计算
                value = effectFn()
                // 计算完后,将 dirty为false
                dirty = false
            }else{
                //读取的时候,调用track追踪
                track(obj,'value')
                return value
            }
        }
    }
    return obj
}

watch实现

watch本质就是观测一个响应式数据,当数据发生变化时通知并执行响应的回调函数

我们可以很简单的利用,effect和scheduler来实现一个简单的watch

function watch(source,cb){
    //触发读取操作,建立联系
    effect(()=>souce.foo,{
        scheduler(){
            //当数据发生变化的时候,执行cb
            cb()
        }
    })
    
}

const data = {foo:1}
const obj = new Proxy(data,{/*...*/})
watch(obj,()=>{
    console.log('数据变化了')
})
obj.foo++
// 目前只能观测obj.foo的改变。所以我们还需要继续封装一个递归操作
function watch(source,cb){
    effect(()=>traverse(source),{
        scheduler(){
            cb()
        }
    })
}
function traverse(value,seen = new Set){
    if(typeof value != 'object' || value == null || seen.has(value)) return 
    seen.add(value)
    for(const i in value){
        traverse(value[i],seen)
    }
    return value
}
//上述 traverse函数,递归的对传入的对象身上的属性进行了读取操作,从而当属性发生变化时触发回调函数执行。

利用lazy属性,获取watch中的newValue与oldValue

function watch(source,cb){
    //这里的source也不一定是对象,也可以是getter函数
    let getter
    if(typeof source == 'function'){
        getter = source
    }else{
        getter = ()=>traverse(source)
    }
    let newValue,oldValue
    const effectFn = effect(()=>getter(),{
        lazy:true,
        scheduler(){
            //在scheduler中执行的是新值,因为这个函数之所以调用是因为值发生了改变。所以此处是新值
            newValue = effectFn()
            //将新值与旧值作为参数传递出去
            cb(newValue,oldValue)
            //更新旧值,不然下一次的旧值不对
            oldValue = newValue
        }
    })
    //手动调用副作用函数,拿到值为旧值
    oldValue = effectFn()
}

默认我们的watch第一次不执行,如果想要第一次执行的话,需要设置immediate:true

watch(obj,()=>{
    console.log('变化了')
},{
    immediate:true
})
//
function watch(source,cb,options){
    //这里的source也不一定是对象,也可以是getter函数
    let getter
    if(typeof source == 'function'){
        getter = source
    }else{
        getter = ()=>traverse(source)
    }
    let newValue,oldValue
    const job = ()=>{
            //在scheduler中执行的是新值,因为这个函数之所以调用是因为值发生了改变。所以此处是新值
            newValue = effectFn()
            //将新值与旧值作为参数传递出去
            cb(newValue,oldValue)
            //更新旧值,不然下一次的旧值不对
            oldValue = newValue
    }
    const effectFn = effect(()=>getter(),{
        lazy:true,
        scheduler:job
    })
    if(options.immediate){
        job()
    }else{
        //手动调用副作用函数,拿到值为旧值
        oldValue = effectFn()
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值