Skip to content

响应系统

前置学习

响应式数据与副作用函数

副作用: 是指在函数调用过程中对函数外部的状态进行修改的行为

js
// 全局变量
let val = 1
function effect() {
  val = 2 // 修改全局变量,产生副作用
}

响应式数据(Reactive Data):是一种编程范式,其中数据模型会自动反映其变化,从而使得视图和数据之间保持同步 目前 不能实现 修改 obj.text 同步实现 body 变化

js
 const obj = { text: 'hello world' }
 function effect() {
   // effect 函数的执行会读取 obj.text
   document.body.innerText = obj.text
 }

响应数据基本实现

响应数据对于上面代码来说,修改 obj 的值时,重新执行 effect

我们可以观察代码发现

  • 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
  • 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。

实现思路:副作用读取值,将副作用放在一个“桶”里,修改对象值时,从桶里取出,然后执行副作用

在 ES2015之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现(vue3)

js
const bucket = new Set();

// 原始数据
const data = { text: 'hello world' };

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    bucket.add(effect);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    bucket.forEach((fn) => fn());
    // 返回 true 代表设置操作成功
    return true;
  },
});

// 副作用函数
function effect() {
  document.body.innerText = obj.text;
}
// 执行副作用函数,触发读取
effect();
// 1 秒后修改响应式数据
setTimeout(() => {
  obj.text = 'hello vue3';
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./effect.js"></script>

</html>

设计一个完善的响应系统

提供一个用来注册副作用函数的机制,解决硬编码 副作用函数名称

js
const bucket = new Set();

// 原始数据
const data = { text: 'hello world' };

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    if (activeEffect) {
      bucket.add(activeEffect);
    }
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    bucket.forEach((fn) => fn());
    // 返回 true 代表设置操作成功
    return true;
  },
});

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}
// 一个匿名的副作用函数
effect(() => {
  document.body.innerText = obj.text;
});
// 删除原来 副作用函数
// function effect() {
//   document.body.innerText = obj.text;
// }
// 执行副作用函数,触发读取
// effect();
// 1 秒后修改响应式数据
setTimeout(() => {
  obj.text = 'hello vue3';
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./1_effect.js"></script>

</html>

新问题,如果给响应数据添加一个不存在的值,依旧会触发

js
const bucket = new Set();

// 原始数据
const data = { text: 'hello world' };

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    if (activeEffect) {
      bucket.add(activeEffect);
    }
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    bucket.forEach((fn) => fn());
    // 返回 true 代表设置操作成功
    return true;
  },
});

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}
// 一个匿名的副作用函数
effect(() => {
  // 执行两次
  console.log('effect执行');
  document.body.innerText = obj.text;
});

// 1 秒后修改响应式数据
setTimeout(() => {
  // 正常情况 修改 abc 应该不执行 effect
  obj.abc = 'hello vue3';
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./2_run2_effect.js"></script>

</html>

为了解决这个问题,我们需要重新设计一个桶的数据结构,由于之前桶是 set 作为数据存储,没有保存副作用函数和操作目标之间的关系 先观察下面代码

js
effect(function effectFn() {
   document.body.innerText = obj.text
})
  • 被操作(读取)的对象 obj
  • 被操作(读取)的对象属性 text
  • 注册的副作用方法 effectFn

如果用 target 来表示一个代理对象所代理的原始对象,用 key 来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数,那么可以为这三个角色建立如下关系

target
    └── key
        └── effectFn
js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据
const data = { text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return target[key];
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  effects && effects.forEach((fn) => fn());
}

// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}
// 一个匿名的副作用函数
effect(() => {
  // 执行两次
  console.log('effect执行');
  document.body.innerText = obj.text;
});

// 1 秒后修改响应式数据
setTimeout(() => {
  // 正常情况 修改 abc 应该不执行 effect
  obj.text = 'hello vue3';
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./3_fix_run2_effect.js"></script>

</html>

其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。它们的关系如图 alt text

分支切换和 cleanup

明确分支切换的定义(值变化会导致执行代码分支变化)

js
const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })
effect(function effectFn() {
  document.body.innerText = obj.ok ? obj.text : 'not'
})

联系示意图

data
    └── ok
        └── effectFn
    └── text
        └── effectFn

alt text 出现的问题:当修改 ok 为 false 的时候,再修改 text 应该不在影响系统,不应该再执行副作用函数,但目前还做不到

js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { ok: true, text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return target[key];
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  effects && effects.forEach((fn) => fn());
}

// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}
// 一个匿名的副作用函数
effect(() => {
  console.log('effect执行');
  // 改为条件判断
  document.body.innerText = obj.ok ? obj.text : 'not'; 
});

// 1 秒后修改响应式数据
setTimeout(() => {
  // 会正常触发
  obj.ok = false;
}, 1000);

// 2 秒后修改响应式数据
setTimeout(() => {
  // 正常情况 修改 text 应该不执行 effect 但还是执行了
  obj.text = 'hello vue3';
}, 2000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./4_branch_switch_effect.js"></script>

</html>

解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除 如果我们能做到每次副作用函数执行前,将其从相关联的依赖集合中移除,那么问题就迎刃而解了

js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { ok: true, text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return;
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps); // 新增
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  // 使用一个新 set 避免死循环
  const effectsToRun = new Set(effects); // 新增
  effectsToRun.forEach((effectFn) => effectFn()); // 新增
  //  effects && effects.forEach((fn) => fn()); // 删除
}


function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i];
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn);
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0;
}

function effect(fn) {
  // activeEffect = fn;
  // fn();
  const effectFn = () => {
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn;
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn); // 新增
    fn();
  };
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 执行副作用函数
  effectFn();
}

// 一个匿名的副作用函数
effect(() => {
  console.log('effect执行');
  // 改为条件判断
  document.body.innerText = obj.ok ? obj.text : 'not';
});

// 1 秒后修改响应式数据
setTimeout(() => {
  // 会正常触发
  obj.ok = false;
}, 1000);

// 2 秒后修改响应式数据
setTimeout(() => {
  // 正常情况 修改 text 应该不执行 effect 但还是执行了
  obj.text = 'hello vue3';
}, 2000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./5_branch_switch_effect.js"></script>

</html>

alt text


支持嵌套

默认不支持嵌套,主要由于多层嵌套会导致 全局参数 activeEffect 被覆盖,所以新加字段 effectStack 来存储

js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: true, bar: true };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = []; // 新增

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return;
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps); // 新增
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  // 使用一个新 set 避免死循环
  const effectsToRun = new Set(effects);
  effectsToRun.forEach((effectFn) => effectFn());
}
function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i];
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn);
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0;
}

function effect(fn) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn);
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn;
    // 用一个全局变量存储当前激活的 effect 函数
    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn); // 新增
    fn();
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop(); // 新增
    activeEffect = effectStack[effectStack.length - 1]; // 新增
  };
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 执行副作用函数
  effectFn();
}

// 全局变量
let temp1, temp2;
// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {
  console.log('effectFn1 执行');

  effect(function effectFn2() {
    console.log('effectFn2 执行');
    // 在 effectFn2 中读取 obj.bar 属性
    temp2 = obj.bar;
  });
  // 在 effectFn1 中读取 obj.foo 属性
  temp1 = obj.foo;
});

// 1 秒后修改响应式数据
setTimeout(() => {
  // 会正常触发
  obj.bar = false;
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./6_嵌套_effect.js"></script>

</html>

避免无限循环

如果在副作用函数内修改响应数据,目前代码会死循环,原因就是 在读取 foo 的同时,还要修改 foo 导致不断循环

 const data = { foo: 1 }
 const obj = new Proxy(data, { /*...*/ })
 effect(() => obj.foo++)

添加判断 修改运行的副作用和当时的不是一个才可以运行(如果嵌套还是有问题,需要遍历数组)

js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: 1, foo1: 1 };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return;
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps); // 新增
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  // 使用一个新 set 避免死循环
  const effectsToRun = new Set(effects);
  effectsToRun.forEach((effectFn) => {

    effectFn !== activeEffect &&
      effectStack.every((effectFn) => effectFn !== activeEffect) &&
      effectFn();
  });
}

function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i];
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn);
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0;
}

function effect(fn) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn);
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn;
    // 用一个全局变量存储当前激活的 effect 函数
    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn); // 新增
    fn();
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop(); // 新增
    activeEffect = effectStack[effectStack.length - 1]; // 新增
  };
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 执行副作用函数
  effectFn();
}

effect(function effectFn1() {
  console.log('effectFn1 执行');
  obj.foo++; 
  effect(function effectFn2() {
    console.log('effectFn2 执行');
    obj.foo1++; 
  });
});

// 1 秒后修改响应式数据
setTimeout(() => {
  obj.foo++;
}, 1000);
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./7_循环_effect.js"></script>

</html>

执行调度

决定副作用函数执行的时机、次数以及方式

js
// 存储副作用函数的桶  Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: 1, foo1: 1 };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);

    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) {
    return;
  }
  // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
  // 里面存储着所有与当前 key 相关联的副作用函数:effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到“桶”里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps); // 新增
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap,它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据 key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  // 执行副作用函数
  // 使用一个新 set 避免死循环
  const effectsToRun = new Set(effects);
  effectsToRun.forEach((effectFn) => {
    // effectFn !== activeEffect &&
    //   effectStack.every((effectFn) => effectFn !== activeEffect) &&
    //   effectFn();
    if (
      effectFn !== activeEffect &&
      effectStack.every((effectFn) => effectFn !== activeEffect)
    ) {
      // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
      if (effectFn.options.scheduler) {
        // 新增
        effectFn.options.scheduler(effectFn); // 新增
      } else {
        // 否则直接执行副作用函数(之前的默认行为)
        effectFn(); // 新增
      }
    }
  });
}

function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i];
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn);
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0;
}

// 添加配置参数 options
function effect(fn, options = {}) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn);
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn;
    // 用一个全局变量存储当前激活的 effect 函数
    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn); // 新增
    fn();
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop(); // 新增
    activeEffect = effectStack[effectStack.length - 1]; // 新增
  };

  // 将 options 挂载到 effectFn 上
  effectFn.options = options;
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 执行副作用函数
  effectFn();
}

// 定义一个任务队列
const jobQueue = new Set();
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve();

// 一个标志代表是否正在刷新队列
let isFlushing = false;
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return;
  // 设置为 true,代表正在刷新
  isFlushing = true;
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach((job) => job());
  }).finally(() => {
    // 结束后重置 isFlushing
    isFlushing = false;
  });
}

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

obj.foo++;
obj.foo++;

// effect(
//   function effectFn1() {
//     console.log('effectFn1 执行');
//     console.log(obj.foo);
//   }, // options
//   {
//     // 调度器 scheduler 是一个函数
//     scheduler(fn) {
//       // 将副作用函数放到宏任务队列中执行
//       setTimeout(fn);
//     },
//   }
// );

// obj.foo++;
// console.log('完成');
html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>

<body>
</body>
<script src="./8_调度_effect.js"></script>

</html>