第四章-响应系统的作用与实现

一、响应式数据与副作用函数

副作用函数:

  • effect函数执行时, 但除了effect函数之外的任何函数都可以读取或者设置body的文本内容

    javascript 复制代码
    function effect() {
       document.body.innerText = "hello vue3"
    }
  • effect函数修改了全局变量, 但除了effect函数之外的任何函数都可以读取或者设置该变量

    javascript 复制代码
    var a = 1
    function effect() {
       a = 2;
    }

响应式数据:

javascript 复制代码
const obj = { text: "hello world" };
function effect() {
   document.body.innerText = obj.text
}
obj.text = "hello vue3"

如果修改obj.text的值能够触发effect函数的执行, 那么obj就是响应式数据

二、响应式数据的基本实现

关键点: 属性的读取和设置

  • 当副作用函数effect执行的时候, 会触发字段obj.text的读取操作

    (当读取操作发生时, 将副作用函数收集到桶中)

  • 当修改obj.text的值时, 会触发字段obj.text的设置操作

    (当设置操作发生时,从桶中出副作用函数并执行)

javascript 复制代码
const bucket = new Set();
const data = {text: "hello world"};
const obj = new Proxy(data, {
  get(target, p) {
     bucket.add(effect);
     return target[p];
  },
  set(target, p, value) {
    target[p] = value;
    bucket.forEach(fn => fn());
    return true;
  }
})
function effect() {
  console.log("执行了")
  document.body.innerText = obj.text;
}
effect();
setTimeout(() => {
  obj.text = "hello vue3"
}, 3 * 1000);

三、设计一个完善的响应系统

1、注册副作用函数

2、副作用函数与被操作字段之间建立明确的而联系(target, key, effectFn)

复制代码
01 target
02     └── text1
03         └── effectFn
04     └── text2
05         └── effectFn

01 target1
02     └── text1
03         └── effectFn1
04 target2
05     └── text2
06         └── effectFn2
javascript 复制代码
// 注册副作用函数
let activeEffect
function effect(fn) {
  activeEffect = fn;
  fn()
  activeEffect = "";
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

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

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  effects && effects.forEach(fn => fn(key));
}

// 代理配置
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}

// 测试示例
const data = {text1: "hello world", text2: "hello vue3"}
const obj = new Proxy(data, proxyOption)
effect(() => console.log(obj.text1));
effect(() => console.log(obj.text2,));

setTimeout(() =>{
    obj.text1 = "text1";
    obj.text2 = "text2";
}, 1000)

ps: weakMap与map之间的区别

  • 键的引用类型 : map可以使用任何类型的值作为键, 包括基本类型和引用类型, 而weakMap只能使用引用类型作为键值, 不能使用基本类型
  • 垃圾回收机制map中的键值对在不被引用时不会被自动删除, 除非手动删除, 而weakMap的键是弱引用, 不会组织垃圾回收器回收键对象
  • 迭代和大小map具有size属性, 可以轻松获取其键值对的数量, 并且可以使用forEach、keys、values、entries进行遍历, 而weakMap没有size属性, 也没有内置的迭代器方法, 因此无法直接获取其大小或进行迭代
  • 性能和内存占用 : 由于weakMap的键是弱引用,不会阻止垃圾回收器回收键对象, 这意味着weakMap在某些情况下可能比map更高效, 因为它不会导致内容泄露, 然后由于垃圾回收的开销, weakMap的性能可能会稍微降低

四 、分支切换和cleanup

示例

javascript 复制代码
01 const data = { ok: true, text: 'hello world' }
02 const obj = new Proxy(data, { /* ... */ })
03
04 effect(function effectFn() {
05   document.body.innerText = obj.ok ? obj.text : 'not'
06 })

此时的副作用函数与响应式数据之间的联系

arduino 复制代码
01 data
02     └── ok
03         └── effectFn
04     └── text
05         └── effectFn

当obj.ok设置false时候, 理想情况下, 副作用函数effect不应该被字段obj.text所对应的依赖集合收集, 建立的联系如下

kotlin 复制代码
01 data
02     └── ok
03         └── effectFn

**解决方案:**每次副作用函数执行时, 可以将该副作用从所有与之关联的依赖集合中删除, 当副作用函数执行完成之后, 会重新建立联系, 但在新的联系不会包含遗留的副作用函数

代码设计:

effectFn.deps: 用来储存所有与该副作用函数相关联的依赖集合

cleanup: 副作用函数执行前进行依赖集合的清除

代码执行顺序: 影响数据发生改变→trigger函数执行, 遍历副作用函数(该副作用函数拷贝了原数据, 原数据的删除与修改跟当前遍历无关)→副作用函数执行时cleanup删除 , 后执行fn进行副作用函数的收集

javascript 复制代码
// 注册副作用函数
let activeEffect
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    fn()
    activeEffect = "";
  }
  effectFn.deps = [];
  effectFn();
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++){
    let deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

// 跟踪函数
function track(target, key) {
  if(!activeEffect)  return;
  let depsMap = bucket.get(target);
  if(!depsMap) bucket.set(target, depsMap = new Map());
  let deps = depsMap.get(key);
  if(!deps) depsMap.set(key, deps = new Set())
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(item => {
    effectsToRun.add(item);
  })
  effectsToRun.forEach(fn => fn(key));
}

// 代理配置
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}


// 测试示例
const data = { ok: true, text: "hello vue3"}
const obj = new Proxy(data, proxyOption)
effect(() => {
  document.body.innerText = obj.ok ? obj.text : "测试数据"
  console.log("执行了多少次")
})

setTimeout(() =>{
  obj.ok = false;
}, 1000)

setTimeout(() =>{
  obj.text = "hello world"
}, 2000)

知识点 : 如果不对trigger函数中的effects进行拷贝, 会触发报错, 程序循环执行, 原因:

javascript 复制代码
let set = new Set([1]);
set.forEach(item => {
 set.delete(1);
 set.add(1);
 console.log(item);
})

语言规范有说明: 在调用forEach遍历set集合时, 如果一个值已经被访问过了, 但该值被删除并重新添加到集合, 如果此时forEach遍历没有结束, 那么该值会重新被访问, 因为上面代码会无限执行。

五、嵌套的effect和effect栈

5.1、业务上: 组件嵌套

javascript 复制代码
01 // Bar 组件
02 const Bar = {
03   render() { /* ... */ },
04 }
05 // Foo 组件渲染了 Bar 组件
06 const Foo = {
07   render() {
08     return <Bar /> // jsx 语法
09   },
10 }

相当于
01 effect(() => {
02   Foo.render()
03   // 嵌套
04   effect(() => {
05     Bar.render()
06   })
07 })

5.2、不符合的测试例子:

javascript 复制代码
const data = { foo: true, bar: true}
const obj = new Proxy(data, proxyOption)

effect(() => {
  console.log("执行了effect1");
  effect(() => {
    console.log("执行了effect2")
    console.log(obj.foo)
  })
  console.log(obj.bar)
})
setTimeout(() =>{
  obj.bar = false;
}, 1000)

结果:

arduino 复制代码
执行了effect1
执行了effect2 true
true

与期望不符的原因:

javascript 复制代码
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    fn()
    activeEffect = ""; 
  }
  effectFn.deps = [];
  effectFn();
}
  • 执行effect1, activeEffect为effect1
  • 执行effect2,activeEffect为effect2
  • effect2执行完成, 回到effect1中, 此时activeEffect为空
  • effect1访问了obj.bar, 这时候需要跟踪bar的关联事件, 但是activeEffect为空,没有事件可关联
  • 更改obj.bar, 没有事件可以触发,故控制台没有输出

5.3、修改后的代码

javascript 复制代码
let activeEffect // 注册副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn()
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = [];
  effectFn();
}

// 副作用函数依赖合集的删除
function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++){
    let deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

// 跟踪函数
function track(target, key) {
  if(!activeEffect)  return;
  let depsMap = bucket.get(target);
  if(!depsMap) bucket.set(target, depsMap = new Map());
  let deps = depsMap.get(key);
  if(!deps) depsMap.set(key, deps = new Set())
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(item => {
    effectsToRun.add(item);
  })
  effectsToRun.forEach(fn => fn(key));
}

// 代理属性
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}

// 测试示例
const data = { foo: true, bar: true}
const obj = new Proxy(data, proxyOption)
effect(() => {
  console.log("执行了effect1");
  effect(() => {
    console.log("执行了effect2")
    console.log(obj.foo)
  })
  console.log(obj.bar)
})
setTimeout(() =>{
  obj.bar = false;
}, 1000)

执行结果:

arduino 复制代码
执行了effect1
执行了effect2 true
true
执行了effect1
执行了effect2 true
false

六、避免无限递归循环

6.1、不符合的测试例子

ini 复制代码
// 测试示例
const data = { i: 1}
const obj = new Proxy(data, proxyOption)
effect(() => {
   obj.i ++ // 相当于obj.i = obj.i + 1;
})

结果:

arduino 复制代码
Uncaught RangeError: Maximum call stack size exceeded

与期望不符合的原因:

  • obj.i + 1, 读取了obj中i属性, 此时将副作用函数effect收集到'桶'中,
  • obj.i = obj.i + 1, 设置了obj中i属性, 此时触发了'桶'中的副作用的函数的执行
  • 读取, 收集
  • 设置, 触发
  • 循环往复

解决方法 :

如果trigger触发执行的副作用函数和当前正则执行的副作用函数相同, 则不触发执行

5.3、修改后的代码

javascript 复制代码
let activeEffect // 注册副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn()
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = [];
  effectFn();
}

// 副作用函数依赖合集的删除
function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++){
    let deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

// 跟踪函数
function track(target, key) {
  if(!activeEffect)  return;
  let depsMap = bucket.get(target);
  if(!depsMap) bucket.set(target, depsMap = new Map());
  let deps = depsMap.get(key);
  if(!deps) depsMap.set(key, deps = new Set())
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    // 如果trigger触发执行的副作用函数和当前正则执行的副作用函数相同, 则不触发执行
    if(effectFn !== activeEffect){ 
      effectsToRun.add(effectFn);
    }
  })
  effectsToRun.forEach(fn => fn(key));
}

// 代理配置
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}


// 测试示例
const data = { i: 1}
const obj = new Proxy(data, proxyOption)
effect(() => {
   obj.i ++
})

七、调度执行

可调度性: 当trigger动作触发副作用函数重新执行时, 有能力决定副作用函数执行的时机、次数以及方式

示例:

javascript 复制代码
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
effect(() => {
  console.log(obj.foo)
})
obj.foo++;
console.log('结束了')

结果:

复制代码
1
2
结束了

如果期望输出下面结果

复制代码
1
结束了
2

修改:

为effect函数设计一个选项参数options, 允许用户指定调度器

javascript 复制代码
effect(() => {
  console.log(obj.foo)
}, { 
  scheduler(fn){
    Promise.resolve().then(() => {
       fn();
    })
  }
})

effect函数内部需要把options挂载到对应的副作用函数上

javascript 复制代码
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options) {
  const effectFn = () => {
    cleanup(effectFn);
    // 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
    activeEffect = effectFn;
    // 调用副作用函数之前将副作用函数压入栈
    effectStack.push(effectFn);
    fn()
    // 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  }
  // 将options挂载到effectFn上
  effectFn.options = options;
  // 用来所有与该副作用函数相关的依赖集合
  effectFn.deps = [];
  effectFn();
}

trigger函数中触发副作用函数重新执行时, 就可以直接调用用户传递的调度器函数, 从而把控制权交给用户

javascript 复制代码
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    if(effectFn !== activeEffect){
      effectsToRun.add(effectFn);
    }
  })
  effectsToRun.forEach(effectFn => {
    if(effectFn.options.scheduler){
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
}

整体代码

javascript 复制代码
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    // 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
    activeEffect = effectFn;
    // 调用副作用函数之前将副作用函数压入栈
    effectStack.push(effectFn);
    fn()
    // 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  }
  // 将options挂载到effectFn上
  effectFn.options = options;
  // 用来所有与该副作用函数相关的依赖集合
  effectFn.deps = [];
  effectFn();
}

// 副作用函数依赖合集的删除
function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++){
    let deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

// 跟踪函数
function track(target, key) {
  if(!activeEffect)  return;
  let depsMap = bucket.get(target);
  if(!depsMap) bucket.set(target, depsMap = new Map());
  let deps = depsMap.get(key);
  if(!deps) depsMap.set(key, deps = new Set())
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    if(effectFn !== activeEffect){
      effectsToRun.add(effectFn);
    }
  })
  effectsToRun.forEach(effectFn => {
    if(effectFn.options.scheduler){
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
}

// 代理配置
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}

测试示例1: 调换事件执行顺序

javascript 复制代码
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    Promise.resolve().then(() => {
      fn();
    })
  }
})
obj.foo++;
console.log('结束了')

// 执行结果: 
1
结束了
2

测试示例2 : 多次触发属性修改, 只执行一次副作用函数, 对应到vue中多次修改响应式数据, 但只会触发一次template的更新

javascript 复制代码
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
// 定义一个任务队列
const jobQueue = new Set();
// 一个标志代表是否刷新队列
let isFlushing = false;
function flushJob() {
  // 如果队列正在刷新, 则什么都不做
  if(isFlushing)  return;
  // 设置为true, 代表正在刷新
  isFlushing = true;
  // 在微任务中执行jobQueue队列
  Promise.resolve().then(() => {
    jobQueue.forEach(fn => fn());
  }).finally(() => {
    // 重置isFlushing
    isFlushing = false;
  })
}
effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    jobQueue.add(fn);
    flushJob();
  }
})
obj.foo++;
obj.foo++;

// 执行结果: 
1
3

八、计算属性computed与lazy

computed: 不调用不执行计算得到值 值缓存, 相关属性不更改使用旧值相关属性更改触发值的更新

  • 不调用不执行: 在options中添加lazy属性, 返回effectFn让用户手动执行

    ini 复制代码
    let activeEffect;
    let effectStack = [];  
    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 = [];
      if(!options.lazy){
        effectFn();
      }
      return effectFn;
    }
  • 计算得到值

    ini 复制代码
    let activeEffect  
    let effectStack = []; 
    function effect(fn, options = {}) {
      const effectFn = () => {
        cleanup(effectFn);
        activeEffect = effectFn;
        effectStack.push(effectFn);
        let res =  fn()
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
        return res;
      }
      effectFn.options = options;
      effectFn.deps = [];
      if(!options.lazy){
        effectFn();
      }
      return effectFn;
    }
  • 此时可以得到基本的computed方法

    javascript 复制代码
    function computed(getter) {
      let effectFn = effect(getter, {lazy:  true});
      let obj;
      obj = {
        get value() {
          return effectFn();
        }
      }
      return obj;
    }
    
    // 测试示例
    const data = { foo: 1, bar: 2 }
    const obj = new Proxy(data, proxyOption)
    let sum = computed(() => obj.foo + obj.bar);
    console.log(sum.value); 
    // 执行结果: 
    3
  • 值缓存, 相关属性不更改使用旧值

    ini 复制代码
    function computed(getter) {
      let effectFn = effect(getter, {
        lazy:  true,
        scheduler() {
          dirty = true;  //相关属性发生更改的时候,重新进行值的计算
          // 按照上面的逻辑, 惯性思维上应该执行effectFn(), 属性发生修改, 则触发相关副作用函数,
          // 但是computed中只有调用到computed的值才会触发计算
          // 所以这里不要执行effectFn, 而是交由 obj { get value () { 这里执行effectFn } }
        }
      });
      let obj;
      let value;
      let dirty = true;
      obj = {
        get value() {
          if(dirty) {
            value = effectFn();
            dirty =false;
          }
          return value
        }
      }
      return obj;
    }
    
    // 测试示例
    const data = { foo: 1, bar: 2 }
    const obj = new Proxy(data, proxyOption)
    let sum = computed(() => {
      console.log("执行computed方法")
      return obj.foo + obj.bar;
    });
    console.log(sum.value);
    console.log(sum.value);
    obj.foo = 2;
    console.log(sum.value);
    
    // 执行结果
    执行computed方法
    3
    3
    执行computed方法
    4
  • 相关属性更改触发值的更新

    下面的例子是不会触发副作用函数, 原因这个computed返回的值不是代理对象, 没有该属性进行读取储存,更改触发的配置

    ini 复制代码
    const data = { foo: 1, bar: 2 }
    const obj = new Proxy(data, proxyOption)
    let sum = computed(() => {
      return obj.foo + obj.bar;
    });
    effect(() => {
      console.log(sum.value)
    })
    obj.foo = 4;
    
    // 执行结果
    3

    故需要手动进行属性的跟踪和触发

    ini 复制代码
    function computed(getter) {
      let effectFn = effect(getter, {
        lazy:  true,
        scheduler() {
          dirty = true;
          trigger(obj, "value"); // 进行属性的触发
        }
      });
      let obj;
      let value;
      let dirty = true;
      obj = {
        get value() {
          if(dirty) {
            track(obj, "value");   // 获取值进行属性的跟踪
            value = effectFn();   
            dirty = false;
          }
          return value
        }
      }
      return obj;
    }

    手动建立了下面的联系关系

    此时再执行一遍测试例子

    ini 复制代码
    const data = { foo: 1, bar: 2 }
    const obj = new Proxy(data, proxyOption)
    let sum = computed(() => {
      return obj.foo + obj.bar;
    });
    effect(() => {
      console.log(sum.value)
    })
    obj.foo = 4;
    
    // 执行结果
    3
    4

九、watch的实现原理

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

javascript 复制代码
watch(obj, () => {
  console.log("数据遍历")
})
obj.foo ++;

本质上利用了effect以及options.scheduler的选项, 如果option中存在scheduler中选项, 当响应式数据发生变化时, 会触发scheduler调度函数的执行, 而非直接触发副作用函数的执行,下面是watch的实现

ini 复制代码
function traverse(value, seen = new Set()) {
  if(typeof value !== "object" || value === null || seen.has(value)) return value;
  // 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环
  seen.add(value)
  // 这里只考虑到了对象, 没有考虑到数组等结构体
  for(let key in value) {
    traverse(value[key], seen)
  }
  return value;
}

function watch(source, cb) {
   let getter;
   if(typeof source === "function") {
     getter = source;
   } else {
     // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
     // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
     getter = () => traverse(source);
   }
   let newValue, oldValue;
   let effectFn = effect(() => getter(), {
      lazy: true,
      scheduler() {
        newValue = effectFn();
        cb(newValue, oldValue)
        oldValue = newValue;
      }
   })
   oldValue = effectFn();
}

// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, (newValue, oldValue) => {
  console.log("foo发生了变更", newValue, oldValue);
});
obj.foo++;

十、立即执行的watch与回调执行时机

  • 立即执行的回调函数, 其实就是立即执行cb

    scss 复制代码
    function traverse(value, seen = new Set()) {
      if(typeof value !== "object" || value === null || seen.has(value)) return value;
      // 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环
      seen.add(value)
      // 这里只考虑到了对象, 没有考虑到数组等结构体
      for(let key in value) {
        traverse(value[key], seen)
      }
      return value;
    }
    
    function watch(source, cb, options = {}) {
       let getter;
       if(typeof source === "function") {
         getter = source;
       } else {
         // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
         // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
         getter = () => traverse(source);
       }
       let newValue, oldValue;
       const job = () => {
         newValue = effectFn();
         cb(newValue, oldValue)
         oldValue = newValue;
       }
       let effectFn = effect(() => getter(), {
          lazy: true,
          scheduler() {
             job();
          }
       })
       if(options.immediate) {
         job();
       } else {
         oldValue = effectFn();
       }
    }
    
    // 测试示例
    const data = { foo: 1}
    const obj = new Proxy(data, proxyOption)
    watch(() => obj.foo, (newValue, oldValue) => {
      console.log("foo发生了变更", newValue, oldValue);
    }, {
      immediate: true,
    });
    obj.foo++;
  • 回调函数的执行时机

    watch方法options选项存在flush选项, 用于指定调度函数的执行时间

    • pre: 默认值, 组件更新前执行
    • post:代表调度函数需要将副作用函数放到一个微任务队列中, 等待DOM更新结束轴再执行
    • async: 下面的直接执行job() ,本质上相当于'async'的效果, 即同步执行
    ini 复制代码
    function watch(source, cb, options = {}) {
       let getter;
       if(typeof source === "function") {
         getter = source;
       } else {
         // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
         // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
         getter = () => traverse(source);
       }
       let newValue, oldValue;
       const job = () => {
         newValue = effectFn();
         cb(newValue, oldValue)
         oldValue = newValue;
       }
       let effectFn = effect(() => getter(), {
          lazy: true,
          scheduler() {
            if(options.flush === "post") {
              Promise.resolve().then(() => {
                 job();
              })
            } else {
              job();
            }
          }
       })
       if(options.immediate) {
         job();
       } else {
         oldValue = effectFn();
       }
    }

十一、过期的副作用

在下面的例子中

ini 复制代码
let finalData 
watch(obj, async () => {
   const res = await featch("/path/to/request");
   finalData = res;
})

可能存在下面的情况, 书中称之为竞态问题

![图片转存失败,建议将图片保存下来直接上传](C:\Users\fangyueming\AppData\Roaming\Typora\typora-user-images\image-20231025152239027.png

与我们期望的当B返回时,请求A已经过期,其中A产生的结果应该视为无效

解决方法, watch函数的回调函数接受第三个参数onInvalidate, 它是一个函数, 类似事件监听器, 我们可以使用onInvalidate函数注册一个回调, 这个回调函数会在当前副作用函数过期时执行

javascript 复制代码
function watch(source, cb, options = {}) {
  let getter;
  if(typeof source === "function") {
    getter = source;
  } else {
    // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
    // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
    getter = () => traverse(source);
  }
  let newValue, oldValue;
  let cleanUp;
  function onInvalidate(cb) {
    cleanUp = cb;
  }
  const job = () => {
    newValue = effectFn();
    if(cleanUp) {
      cleanUp();
    }
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue;
  }
  let effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      if(options.flush === "post") {
        Promise.resolve().then(() => {
          job();
        })
      } else {
        job();
      }
    }
  })
  if(options.immediate) {
    job();
  } else {
    oldValue = effectFn();
  }
}

// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, async (newValue, oldValue, onInvalidate) => {
  let expired = false;
  onInvalidate(() => {
    expired = true;
  })
  const res = await getFetchData()
  if(!expired) {
    console.log(res)
  }
}, {
  immediate: true,
  flush: "post"
});
obj.foo++;
function getFetchData() {
  const time = Math.random() * 1000 + 1000;
  return new Promise(resolve =>  {
    setTimeout(() => {
      console.log("fetch", time)
      resolve(time);
    }, time)
  })
}

思路:

1、立刻执行的时候, 执行第一次回调函数, 调用onInvalidate注册了cleanup,然后发送请求, 等待接口的返回

2、此时执行了obj.foo++, 触发了cleanup的执行 ,第一次回调函数的expired被设置为了true, 跟着第二次回调函数的执行, 调用onInvalidate注册了cleanup, 然后发送请求, 等待接口的返回

第一种情况

3、第一次请求返回数据,expired为true, 不会触发console.log

4、第二次请求返回数据,expired为false, 触发console.log

第一次情况

第二种情况

3、第二次请求返回数据,注册的cleanup函数没有被调用过, expired为true, 触发console.log

4、第一次请求返回数据,expired为false, 不会触发console.log

代码归总:

javascript 复制代码
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    // 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
    activeEffect = effectFn;
    // 调用副作用函数之前将副作用函数压入栈
    effectStack.push(effectFn);
    let res =  fn()
    // 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    return res;
  }
  // 将options挂载到effectFn上
  effectFn.options = options;
  // 用来所有与该副作用函数相关的依赖集合
  effectFn.deps = [];
  if(!options.lazy){
    effectFn();
  }
  return effectFn;
}

// 副作用函数依赖合集的删除
function cleanup(effectFn) {
  for(let i = 0; i < effectFn.deps.length; i++){
    let deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();

// 跟踪函数
function track(target, key) {
  if(!activeEffect)  return;
  let depsMap = bucket.get(target);
  if(!depsMap) bucket.set(target, depsMap = new Map());
  let deps = depsMap.get(key);
  if(!deps) depsMap.set(key, deps = new Set())
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

// 触发函数
function trigger(target, key) {
  let depsMap =  bucket.get(target);
  if(!depsMap) return;
  let effects = depsMap.get(key);
  let effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    if(effectFn !== activeEffect){
      effectsToRun.add(effectFn);
    }
  })
  effectsToRun.forEach(effectFn => {
    if(effectFn.options.scheduler){
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
}

// 代理配置
let proxyOption = {
  get(target, key, receiver) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  }
}

function computed(getter) {
  let effectFn = effect(getter, {
    lazy:  true,
    scheduler() {
      dirty = true;
      trigger(obj, "value")
    }
  });
  let obj;
  let value;
  let dirty = true;
  obj = {
    get value() {
      if(dirty) {
        track(obj, "value");
        value = effectFn();
        dirty = false;
      }
      return value
    }
  }
  return obj;
}

function traverse(value, seen = new Set()) {
  if(typeof value !== "object" || value === null || seen.has(value)) return value;
  // 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环
  seen.add(value)
  // 这里只考虑到了对象, 没有考虑到数组等结构体
  for(let key in value) {
    traverse(value[key], seen)
  }
  return value;
}

function watch(source, cb, options = {}) {
  let getter;
  if(typeof source === "function") {
    getter = source;
  } else {
    // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
    // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
    getter = () => traverse(source);
  }
  let newValue, oldValue;
  let cleanUp;
  function onInvalidate(cb) {
    cleanUp = cb;
  }
  const job = () => {
    newValue = effectFn();
    if(cleanUp) {
      cleanUp();
    }
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue;
  }
  let effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      if(options.flush === "post") {
        Promise.resolve().then(() => {
          job();
        })
      } else {
        job();
      }
    }
  })
  if(options.immediate) {
    job();
  } else {
    oldValue = effectFn();
  }
}

// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, async (newValue, oldValue, onInvalidate) => {
  let expired = false;
  onInvalidate(() => {
    expired = true;
  })
  const res = await getFetchData()
  if(!expired) {
    console.log(res)
  }
}, {
  immediate: true,
  flush: "post"
});
obj.foo++;
function getFetchData() {
  const time = Math.random() * 1000 + 1000;
  return new Promise(resolve =>  {
    setTimeout(() => {
      console.log("fetch", time)
      resolve(time);
    }, time)
  })
}
相关推荐
中微子1 小时前
React 状态管理 源码深度解析
前端·react.js
加减法原则2 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele2 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4532 小时前
React移动端开发项目优化
前端·react.js·前端框架
你的人类朋友3 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir3 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴3 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子3 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead4 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
Xiaouuuuua4 小时前
一个简单的脚本,让pdf开启夜间模式
java·前端·pdf