Vue3响应式系统原理:无限递归循环处理

问题

如果一个副作用函数中,既涉及到响应式对象的读操作,又涉及到响应式对象的写操作,就会出现无限递归循环的问题。

js 复制代码
const data = {
  value: 1,
};

const obj = new Proxy(data, ...)

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

上面的代码中,副作用函数既有响应式对象的读操作,又有响应式对象的写操作。 完整代码如下:

js 复制代码
// 原始对象,包含两个属性
const data = {
  value: 1,
};

// 存放副作用函数的集合容器。之所以是用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new WeakMap();

// 表示当前正在运行的副作用函数
let activeEffect = null;

// 副作用栈
let effectStack = [];
// 用于执行副作用函数的函数
function effect(fn) {
  const effectFn = () => {
    // 清除依赖
    cleanup(effectFn);
    // 执行副作用函数
    activeEffect = effectFn;

    effectStack.push(activeEffect)
    fn();

    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  };
  // 存储该副作用哦函数相关联的依赖
  effectFn.deps = []
  effectFn();
}

// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
  get(target, key) {
    if(!activeEffect) return target[key]
    track(target, key);
    return target[key];
  },
  set(target, key, val) {
    target[key] = val;
    trigger(target, key);
    return true;
  },
});

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

  activeEffect.deps.push(deps);
}

function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if(!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set(effects)
  effectsToRun && effectsToRun.forEach(fn => fn())
}


////////////////////////////////////

effect(() => {
  obj.value = obj.value + 1
});


// 输出控制台报错:
// RangeError: Maximum call stack size exceeded

分析

我们分析下为什么报超过内存栈容量的错误。

  1. 当运行effect方法,触发obj.value的读操作,从而触发track方法。
  2. 在track方法中,当前副作用函数会被添加到obj.value对应的deps中;
  3. 有由于effect中又涉及到obj.value的写操作,从而触发了trigger方法。
  4. trigger方法会从deps中取出所有的副作用函数并执行。
  5. 在第4步中执行所有副作用函数,一定会包含当前正在运行的那个副作用函数。因此也就出现了当前的函数运行中,又调用了自己的情况。从而会返回第1步的流程,进入了无限循环。

解决上面的问题思路很简单,在trigger函数中,如果发现当前deps将要执行的函数和当前正在执行的函数是同一个,那么就跳过不执行它。

js 复制代码
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if(!depsMap) return
  const effects = depsMap.get(key)

  // const effectsToRun = new Set(effects)
  const effectToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 当前的副作用函数和activeEffect不一样,才会添加到执行集合中
    if(effectFn !== activeEffect) {
      effectToRun.add(effectFn)
    }
  })
  effectToRun && effectToRun.forEach(fn => fn())
}

总结

如果一个副作用函数中,既涉及到响应式对象的读操作,又涉及到响应式对象的写操作,就会出现无限递归循环的问题。解决的思路就是在trigge函数中,如果发现将要执行的副作用函数和当前正在执行的副作用函数是同一个的时候,跳过不执行这个函数。

代码

github.com/wdskuki/js-...

参考

  1. 《Vue设计与实现》,作者:霍春阳,ISBN: 9787115583864
相关推荐
2501_9159184111 小时前
Web 前端可视化开发工具对比 低代码平台、可视化搭建工具、前端可视化编辑器与在线可视化开发环境的实战分析
前端·低代码·ios·小程序·uni-app·编辑器·iphone
程序员的世界你不懂11 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
索迪迈科技11 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
gnip12 小时前
JavaScript二叉树相关概念
前端
attitude.x12 小时前
PyTorch 动态图的灵活性与实用技巧
前端·人工智能·深度学习
β添砖java13 小时前
CSS3核心技术
前端·css·css3
空山新雨(大队长)13 小时前
HTML第八课:HTML4和HTML5的区别
前端·html·html5
猫头虎-前端技术13 小时前
浏览器兼容性问题全解:CSS 前缀、Grid/Flex 布局兼容方案与跨浏览器调试技巧
前端·css·node.js·bootstrap·ecmascript·css3·媒体
阿珊和她的猫13 小时前
探索 CSS 过渡:打造流畅网页交互体验
前端·css