手动调用和停止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();
  });
}
相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui