您现在的位置是:首页 >技术杂谈 >3.2 继续完善的Vue.js响应式系统网站首页技术杂谈

3.2 继续完善的Vue.js响应式系统

GoldenFingers 2024-08-23 12:01:02
简介3.2 继续完善的Vue.js响应式系统

前文提要:
3.0 响应式系统的设计与实现

3.1 一个稍微完善的Vue.js响应式系统

1、解决副作用函数的死循环问题

在解决了分支的切换的问题,此时还有一个代码死循环的问题,其这个死循环很容易触发,如下代码:

const data = {ok: true, text: 'hello world'}
const obj = new Proxy(data, {
    get: (target, key, receiver) => {
        track(target, key, receiver)
        return Reflect.get(target, key, receiver)
    },
    set: (target, key, value, receiver) => {
        Reflect.set(target, key, value, receiver)
        trigger(target, key)
        return true
    }
}); 

effect(()=>{
    console.log(obj.ok)
})
obj.ok = true

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if(!depsMap) return
    const effects = depsMap.get(key)
    // 问题出在这里
    deps && deps.forEach(fn => fn())
} 

其实问题就出在trigger函数执行的过程中,这里对于obj.ok的改变导致了副作用函数执行,在执行的过程中有console.log(obj.ok),这里再次访问了obj.ok,则将副作用函数又插回了副作用函数的执行队列中,效果相当于

const set = new Set();
set.add('one');
set.forEach(e => {
    console.log(e)
    set.delete(e);
    set.add('one')
})

set删除值之后,再插入相同的值也会让forEach一直执行。

其实解决的方案很简单,可以在执行时将deps内的值给取出来,放在新的一个Set中,然后遍历新的Set,这样如果有新加入的值会放入deps中,而不是一边执行一边插入。

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if(!depsMap) return
    const effects = depsMap.get(key)
    // 重新放入,再执行
    const effectsToRun = new Set(effects)
    // 将副作用函数追踪下来,防止出现set在删除时插入新值
    effectsToRun.forEach(effectFn => effectFn())
} 

2、解决在effect函数嵌套时出现的问题

effect函数嵌套是很常见的,比如我们嵌套的组件,父子组件都绑定了副作用函数,如:

// 当发生嵌套时
// Bar组件
const Bar = {
    render() {
        return...
    }
}

// Foo组件为Bar的父组件
const Foo = () => {
    render() {
        return <Bar />
    }
}

// 此时的effect发生了嵌套,相当于
effect(() => {  
    Foo.render() 
    effect(() => {
        Bar.render
    })
})

对于我们上面举例的副作用函数潜逃做个实验,实验代码如下

// 其余代码......

function effect(fn) {
    const effectFn = () => {
      activeEffect = effectFn
      //先清除再执行,自然就形成了分支切换
      cleanup(effectFn)
      fn()
    }
    effectFn.deps = []
    effectFn()
}

// 这里是重点
effect(() => {
    console.log('effect 1 has beeen executed')
    effect(() => {
        console.log('effect 2 has beeen executed')
        temp2 = obj.ok
    })
    temp1 = obj.text
})

obj.text = '1'

此时我们期望执行为触发两次外层的的effect函数,即打印结果为

effect 1 has beeen executed
effect 2 has beeen executed
effect 1 has beeen executed
effect 2 has beeen executed

在建立时能够触发一次外层effect函数,然后obj.text = 1会再次执行外层的effect函数,但是实际的结果是什么呢

effect 1 has beeen executed
effect 2 has beeen executed
effect 2 has beeen executed

这个明显和我们的期望不同,第一次外层effect函数执行输出的前两句是符合预期的,但是第二次修改obj.text触发的却是内层的effect函数。也就是说绑定的obj.text 的副作用函数变成了内层函数。

分析导致这个结果的原因还是看effect实现的过程:

let activeEffect = null

function effect(fn) {
    const effectFn = () => {
      activeEffect = effectFn
      //先清除再执行,自然就形成了分支切换
      cleanup(effectFn)
      fn()
    }
    effectFn.deps = []
    effectFn()
}

在第二层effect执行的时候activeEffect赋值为obj.ok的副作用函数,当第二层effect函数执行完之后,回到第一层时的activeEffect指向的还是第二层的effectFn,所以此时obj.text的副作用函数就成了第二层的effect函数。

其实这个过程就是函数递归时,avtiveEffect的执行也在递归过程中逐步指向内层的副作用函数,但是当递归出来的时候,activeEffect并没有从内层向外层恢复。这个问题实质上就是一个入栈出栈的问题,解决也很轻松,我们可以使用一个栈来记录副作用函数递归时每一层的指向,这样在递归出来时副作用函数指向的就是栈顶元素,代码如下:

const effectStack = []
function effect(fn) {
    const effectFn = () => {
      activeEffect = effectFn
      //先清除再执行,自然就形成了分支切换
      cleanup(effectFn)

      // 用栈记录下当前的辅作用函数  
      effectStack.push(effectFn)
      fn()
      effectStack.pop()
      // 递归出来时改变activeEffect指向
      activeEffect = effectStack[effectStack.length-1]
    }
    effectFn.deps = []
    effectFn()
}

其过程如图:
进栈出栈过程

3、解决无限递归的问题

假设我们副作用函数的代码如下:

effect(() => {
    obj.ok = obj.ok || 1
})

这个副作用函数存在的问题是对obj.ok进行了访问,又对他赋值,这会在effectFn中同时触发triggertrack,在trigger中又会触发effectFn这样下去就形成了一个死递归,报错信息为:RangeError: Maximum call stack size exceeded

解决思路就是在trigger中判断一下,就根据这个死递归的过程,在执行追踪的时候查看副作用函数activeEffect和当前调用函数是否相同,相同则跳过。

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if(!depsMap) return
    const effects = depsMap.get(key)
    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
        // 判断正在执行的副作用函数和当前的即将执行的函数是否相同
        if(effectFn !== activeEffect) 
            effectsToRun.add(effectFn)
    })
    // 将副作用函数追踪下来,防止出现set在删除时插入新值
    effectsToRun.forEach(effectFn => effectFn())
} 
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。