手动调用和停止effect - 源码系列5

effect 真的心思缜密啊!

继续进阶版,属性发生变化,相应的_effect 自动执行。这边添加,可以随时手动执行_effect,也可以随时停止_effect 自动执行

目标 effect

先看 effect 的手动调用和停止

js 复制代码
// index.html
import {
  reactive,
  effect,
} from '../../../node_modules/@vue/reactivity/dist/reactivity.esm-browser.prod.js';
// import { reactive, effect } from './reactivity.js';
const obj = reactive({
  name: 'hua',
  age: 4,
});

// 这里默认执行
const runner = effect(() => {
  console.log('effect', obj.name);
});

// 这里,数据没有发生变化,但手动调用执行
runner();
// 这两种写法等同,都是手动调用执行
runner.effect.run();

// 数据发生变化,自动执行effect
obj.name = 'hua2 自动执行';
// 手动停止
runner.effect.stop();
// 数据发生变化,不会自动执行effect
obj.name = 'hua3 effect停止之后 手动执行';
// 但可以手动执行
runner();

继续分析 effect

  • 有返回值 runner,runner 是一个函数,且这个函数等同于 effect.run
  • runner.effect 是当前 effect 实例
  • effect.stop,说明有 stop 方法,stop 之后,属性变化不主动触发 effect 执行,但是手动可以

effect 函数返回 runner

runner 是个函数,等同于_effect.run,注意绑定 this。

js 复制代码
export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
  // runner是个函数,等同于_effect.run,注意绑定this
  const runner = _effect.run.bind(_effect);
  // runner还有effect属性,直接赋值就好
  runner.effect = _effect;
  return runner;
}

index.html 里,走下,手动执行成功了!

effect 实例有 stop 方法

stop 方法执行之后,属性变化的时候,当前 effect 实例不再主动执行,但是可以手动执行。

用一个 flag 来标志,要不要主动执行

ts 复制代码
class ReactiveEffect {
  // 是否主动执行
  active = true;
  run() {
    // 如果不是主动执行,那么只是执行函数,不再赋值activeEffect,就不会触发track,track的第一行判断条件就是,没有activeEffect,直接return。
    if (!this.active) {
      this.fn();
      return;
    }
    // 运行之前,清除依赖
    clearupEffect(this);
    // ...
  }
  stop() {
    if (this.active) {
      // 标记不主动执行
      this.active = false;
      // 清除依赖
      clearupEffect(this);
    }
  }
}

index.html换成自己的文件,发现浏览器挂了

这边注意,进入死循环了!

触发时会进行清理操作(清理 effect),在重新进行收集(收集 effect)。在循环过程中会导致死循环。

ts 复制代码
let effect = () => {};
let s = new Set([effect]);
s.forEach((item) => {
  s.delete(effect);
  s.add(effect);
}); // 这样就导致死循环了

所以trigger的时候,需要浅拷贝下

js 复制代码
export function trigger(target, key) {
  // ....
  // 这里浅拷贝一份执行,这样删了之后,在执行就不会死循环了
  [...dep].forEach((effect) => {
    activeEffect !== effect && effect.run();
  });
}

重新刷新下,就可啦!!!

附上完整effect代码

ts 复制代码
// track的时候,需要拿到effect,所以用下全局变量存放effect
let activeEffect = null;
// 建立类,方便存放fn,和运行
class ReactiveEffect {
  // 是否主动执行
  private active = true
  // 新增deps
  deps = []
  fn
  parent
  constructor(fn) {
    this.fn = fn;

  }

  run() {
    if (!this.active) {
      this.fn()
      return;
    }
    
    this.parent = activeEffect
    activeEffect = this;
    // 运行之前,清除依赖
    clearupEffect(this);
    this.fn();
    activeEffect = this.parent
    this.parent && (this.parent = null);
  }
  stop() {
    if (this.active) {
      // 清除依赖
      clearupEffect(this);
      // 标记不主动执行
      this.active = false;
      
    }
  }



}

// 清除依賴
function clearupEffect(_effect) {
  // deps结构是 [[_effect1,_effect2],[_effect3,_effect2],],假设去掉_effect2
  const deps = _effect.deps
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(_effect)
  }
  // 同时deps置空,保证每次effect运行都是新的属性映射
  _effect.deps.length = 0
}
  


// }
export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
  // runner是个函数,等同于_effect.run,注意绑定this
  const runner = _effect.run.bind(_effect)
  // runner还有effect属性,直接赋值就好
  runner.effect = _effect
  return runner
}

// 本质是找到属性对应的effect,但属性存在于对象里,所以两层映射
// 响应性对象 和 effect的映射,对象属性和effect的映射
// targetMap = { obj:{name:[effect],age:[effect]} }
const targetMap: WeakMap<object, Map<string, Set<ReactiveEffect>>> = new WeakMap();

// 让属性 订阅 和自己相关的effect,建立映射关系
export function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  // 这属性track过了
  if (dep.has(activeEffect)) {
    return;
  }
  // 核心代码,属性 订阅 effect (本质就是建立映射关系),上面一坨就是判断加初始化
  dep.add(activeEffect);
  // 新增deps
  activeEffect.deps.push(dep);
}

// 属性值变化的时候,让相应的effect执行
export function trigger(target, key) {
  console.log('targetMap', targetMap)
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (!dep) {
    return;
  }

  // 核心代码  属性相应的effect 挨个执行(上面一坨也是一样,判断)
  // 注意,这里要浅拷贝,不然set删除effect的时候,就死循环了
  [...dep].forEach((effect) => {
    activeEffect !== effect && effect.run();
  });
}
相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰6 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪6 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪6 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy7 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom7 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom8 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom8 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试