vue.js设计与分析(观后感)二

解决这个问题,我们需要每次 副作用函数执行前,把 它从相关联的依赖集合中删除。 当副作用函数执行完毕之后,读取值的时候又会重新建立联系。在新建立的联系中不会包含遗留的副作用函数了。

        let activeEffect
        function effect(fn){
            const effectFn = ()=>{
                activeEffect = effectFn
                fn()
            }
            //函数上面是可以添加属性的哦
            //activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
            effect.deps = []
            effectFn()
        }
        // 然后在 track 函数中添加
        //deps就是与当前副作用函数存在关系的依赖集合(Set) 也就是建立起互相对应的关系
        activeEffect.deps.push(deps)
        //至此,我们就可以在副作用函数执行的时候,断开副作用函数与依赖集合的关系
        let activeEffect2
        function effect2(fn){
            const effectFn = ()=>{
                //调用cleanup函数 完成清除
                cleanup(effectFn)
                activeEffect = effectFn
                fn()
            }
            effect2.deps = []
            effectFn()
        }
       //cleanup函数接收副作用函数作为参数,遍历effectFn.deps数组中的依赖集合,将传入的effectFn副作用函数从对应的effectFn.deps数组中移除
        function cleanup(effectFn){
            for(let i=0;i<effectFn.deps.length;i++){
                const deps = effectFn.deps[i]
                // 将effectFn从依赖集合中删除
                deps.delete(effectFn)
            }
            //最后重置 effectFn.deps 数组
            effectFn.deps.length = 0
        }

此时,我们的响应系统已经可以避免副作用函数产生的遗留问题了。但是如果运行代码时,会发现 trigger 函数无限循环。这是因为依赖集合时一个Set数据结构,当副作用函数执行时,会调用cleanup进行清除,但是副作用函数的执行,会导致其被重新收集起来,而对于Set数据结构的依赖集合,会仍然在遍历

        // 可以用以下代码来表示
        const set = new Set([1])
        set.forEach(item=>{
            set.delete(1)
            set.add(1)
            console.log('函数仍然在执行')
        })
        //上述代码会导致循环执行
        // 语言规范中,对此有明确的说明,在调用forEach遍历Set时,如果一个值已经被访问过,但该值被删除并重新添加至集合中,此时forEach没有结束,就会被重新            访问。因此,导致无限执行。
        // 所以我们可以重新构建一个set遍历它即可
        const set1 = new Set([1])
        const newSet = new Set(...set1)
        newSet.forEach(item=>{
            set1.delete(1)
            set1.add(1)
            console.log('不会无限循环')
        })

所以我们的trigger函数也可以用同样办法修改

function trigger(target,key){
    const depMaps = bucket.get(target)
    if(!depMaps) return 
    const effects = depMaps.get(key)
    if(!effects) return 
    const newEffects = new Set(...effects)
    newEffects.forEach(effect=>effect())
}

effect是可以嵌套的

实际上Vue.js的渲染函数就是在一个effect中执行的。因为组件是互相嵌套的,例如我们就是在App.vue中写其他组件的

//类似于这样
effect(function effectFn(){
    App.render()
    effect(function effectFn1(){
        //其他组件的 render 嵌套执行
    })
})

这就说明我们effect要设置为可嵌套的。但目前我们的effect并不支持嵌套,可以用下述代码测试一下

//原始数据
const data = {foo:true,bar:true}
//代理对象
const obj = new Proxy(data,{/*...*/})
//全局变量
let temp1,temp2
effect(function effect1(){
    console.log('effectFn1 执行')
    effect(function effect2(){
        console.log('effect2 执行')
        temp2 = obj.bar
    })
    temp1 = obj.foo
})

**这段代码 effect1中嵌套了effect2 。effect1的执行会导致effect2的执行。此时foo属性对应effect1 , bar属性对应effect2 **

当我们修改foo字段的值会发现输出为

'effectFn1 执行'
'effectFn2 执行'
'effectFn2 执行'

我们试着分析一下代码的执行。首先注册effect函数会先执行一遍,也就是

'effectFn1 执行'
'effectFn2 执行'

当我们修改foo字段,会触发foo字段对应的effect1执行. effect1执行不是会输出 'effectFn1 执行' 'effectFn2 执行'

这里问题就出在我们的全局变量activeEffect。当副作用函数发生嵌套时,内部的副作用函数会覆盖activeEffect的值,并且不会恢复到原来的值

所以,我们需要将嵌套的effect加入到栈中,执行时压入栈,执行完弹出栈。用activeEffect记录当前激活的effect函数

//记录当前激活状态的副作用函数
let activeEffect
// effect栈
const stack = []
function effect(fn){
    const effectFn = ()=>{
        //清除与当前副作用函数相关联的依赖集合
        clean(fn)
        activeEffect = fn
        stack.push(effectFn)
        fn()
        stack.pop()
        activeEffect = stack[stack.length-1]
     }
    effectFn.deps = []
    effectFn()
}

避免循环执行

effect(()=>obj.foo++)

当我们在effect函数中注册上述副作用函数,会发现产生了栈溢出。这句话拆开看可以看为

effect(()=>obj.foo = obj.foo + 1)

可以看到在副作用函数中执行了对值的读取以及设置,当读取时,会加入依赖集合中,设置时,会执行。会产生无限递归调用自己,于是产生栈溢出

我们需要判断 当前活跃的activeEffect和传进来的副作用函数是不是同一个函数,如果是同一个就不执行了

function trigger(target,key){
    const depsMap = bucket.get(target)
    if(!depsMap) return 
    const effects = depsMap.get(key)
    if(!effects) return 
    // 创建 一个新的Set 集合,避免使用Set进行forEach时,无限递归
    const newEffects = new Set()
    effects.forEach(effect => {
        //如果trigger触发的副作用函数与当前执行的effect不同,就加入newEffects中,然后执行
        if(effect !== activeEffect){
            newEffects.push(effect)
        }
    })
    newEffects.forEach(effectFn=>effectFn())
}

调度执行

可调度性时响应式系统非常重要的特性,可调度性就是指有能力决定副作用函数执行的时机,次数,以及方式

首先,我们先实现副作用函数执行的时机,让用户自己去决定
//我们可以为effect设置一个选项参数 options 
effect(
   ()=>{
       console.log('哈哈哈哈')
   },
    // options  。 scheduler是为用户提供的 指定调度器 。参数为副作用函数,用户可以指定什么时候调用
   {
       scheduler(fn){
           /* ... */
       }
   }
)

首先,我们应该将options放在effectFn上

//在定义 effect 函数时,与定义依赖集合 deps 一样
effectFn.options = options

然后在 trigger 函数中,决定effectFn的执行

newEffects.forEach(effectFn=>{
    //新增
    if(effectFn.options.scheduler){
        // 有的话,交给用户去执行
        effectFn.options.scheduler(effectFn)
    }else{
        //没有的话,直接调用副作用函数
        effectFn()
    }
})

此时,我们就可以在调度函数中,加入setTimeout来让副作用函数延迟执行,或者其他操作


控制执行次数

当我们不关心执行过程,只关心结果时,基于调度器我们能够实现此功能

// 定义一个任务队列
const queue = new Set()
//使用Promise.resolve 创建一个promise实例,用于添加至微任务队列
const p = Promise.resolve()
// 此处 用到了类似节流的功能 ,定义一个标志表示是否在刷新
let isFlag = false
function flushJob(){
    // 如果正在刷新,就什么也不做
    if(isFlag) return 
    isFlag = true
    // 开启微任务队列,执行副作用函数
    p.then(()=>{
        queue.forEach(job=>job())
    }).finally(()=>{
        // finally无论promise状态为成功或者失败,都是可以执行的。当队列执行完毕,就表示此时没在刷新
        isFlag = false
    })
}

当我们注册 effect 函数时,就可以通过调度器来实现

effect(()=>{
    console.log('zzz')
},{
    scheduler(fn){
        //每次调度时,将副作用函数添加到queue队列中
        queue.add(fn)
        //调用flushJob刷新队列
        flushJob()
    }
})

// 测试一下
const data = {foo:1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{
    console.log(obj.data)
})
obj.foo++
obj.foo++
//在没有实现上述函数前,这段代码会执行三次,分别为
1
2
3
//实现上述函数,以及调度器后,控制台只会输出两次
1
3

首先,我们利用了Set的自动去重,并且用到了异步的知识。当往队列中添加相同副作用函数时,队列中只会有一个,这个函数的执行是在promise.then中执行,所以,会先执行同步代码,obj.foo++会执行两次,此时obj.foo已经为3。同步代码执行完毕,此时执行异步代码,执行副作用函数,输出obj.foo。也就是3。第一次effect自动执行一次输出1,最后一次输出3。


计算属性与 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()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值