浅谈vue3中effect与computed的亲密关系


Posted in Javascript onOctober 10, 2019

在我刚看完vue3响应式的时候,心中就有一个不可磨灭的谜团,让我茶不思饭不想,总想生病。那么这个谜团是什么呢?就是在响应式中一直穿行在tranger跟track之间的effect。如果单纯的响应式原理根本就用不上effect,那么effect到底是干什么的呢?

船到桥头自然直,柳岸花明又一村。苦心人天不负,偶然间我看到了effect测试代码用例!

it('should observe basic properties', () => {
 let dummy
 const counter = reactive({ num: 0 })
 effect(() => (dummy = counter.num))

 expect(dummy).toBe(0)
 counter.num = 7
 expect(dummy).toBe(7)
})

解释一下,这段代码

  • 首先声明dummy变量,然后在effect的回调中把已响应的对象counter的num属性赋值给dummy
  • 然后做断言判断 dummy是否等于 0
  • 将 counter.num 赋值 7 ,然后 dummy 也变成了 7 !

这,,,让我想到了什么??

这就是computed的吗?

赶紧看下 computed 的测试用例!!

const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)

哈哈哈

阿哈哈哈哈

hhhhhhhhhhhhhhhhhhhh

忍不住想仰天长啸!!

果然跟我猜想的一样!!!我终于直到effect是个什么鬼了,顾名思义effect是副作用的意思,也就是说它是响应式副产品,每次触发了 get 时收集effect,每次set时在触发这些effects。这样就可以做一些响应式数据之外的一些事情了,比如计算属性computed。

让我们用effect实现一个computed 可能会更清晰一点

我就不写一些乱七八糟的判断了,让大家能够看的更加清楚

function computed (fn) {
 let value = undefined
 const runner = effect(fn, {
  // 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
  // 而我们要实现computed显然是不需要的
  lazy: true
 })
 // 为什么要使用对象的形式,是因为我们最后需要得到computed的值
 // 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed() 
 return {
  get value() {
   return runner()
  }
 }
}

// 使用起来是这样的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

这也太简单了吧,那么重点来了,effect怎么实现的呢?

别着急,我们先捋一下逻辑

  1. 首先 如果 effect 回调内有已响应的对象被触发了 get 时,effect就应该被储存起来
  2. 然后,我们需要一个储存effect的地方,在effect函数调用的时候就应该把effect放进这个储存空间,在vue中使用的是一个数组activeReactiveEffectStack = []
  3. 再后,每个target被触发的时候,都可能有多个effect,所以每个target需要有一个对应的依赖收集器 deps,等到 set 时遍历 deps 执行 effect()
  4. 然而,这个依赖收集器 deps 不能放在 target 本身上,这样会使数据看起来不是很简洁,还会存在多余无用的数据,所以我们需要一个 map 集合来储存 target 跟 deps 的关系, 在vue中这个储存集合叫 targetMap 。

几个概念

track 追踪器,在 get 时调用该函数,将所有 get 的 target 跟 key 以及 effect 建立起对应关系

// 比如 const react = reactive({a: { b: 2 })
// react.a 时 target -> {a: { b: 2 } key -> a 
// targetMap 储存了 target --> Map --> key --> Set --> dep --> effect 
// 当调用 react.a.b.c.d.e 时 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
 if (effect) {
  let depsMap = targetMap.get(target);
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key!);
  if (!dep) {
   depsMap.set(key!, (dep = new Set()));
  }
  if (!dep.has(effect)) {
   dep.add(effect);
   effect.deps.push(dep);
  }
 }
}

trigger 触发器,这个就比较好理解了,拿到target key下的对应的所有 effect,然后遍历执行 effect()

export function trigger(target: any, key?: string | symbol) {
 const depsMap: any = targetMap.get(target);
 const effects: any = new Set()
 if (depsMap && depsMap.get(key)) {
  depsMap.get(key).forEach((dep: any) => {
   effects.add(dep)
  });
  effects.forEach((e: any) => e())
 }
}

effect 函数实现

// 暴露的 effect 函数
export function effect(
 fn: Function,
 options: any = EMPTY_OBJ
): any {
 if ((fn as any).isEffect) {
  fn = (fn as any).raw
 }
 const effect = createReactiveEffect(fn, options)
 // 如果不是 lazy,则会立即执行一次
 if (!options.lazy) {
  effect()
 }
 return effect
}

// 创建 effect
function createReactiveEffect(
 fn: Function,
 options: any
): any {
 const effect = function effect(...args: any): any {
  return run(effect as any, fn, args)
 } as any
 effect.isEffect = true
 effect.active = true
 effect.raw = fn
 effect.scheduler = options.scheduler
 effect.onTrack = options.onTrack
 effect.onTrigger = options.onTrigger
 effect.onStop = options.onStop
 effect.computed = options.computed
 effect.deps = []
 return effect
}

// 执行函数,执行完之后会将储存的 effect 删除
// 这是函数 effect 的所有执行,所经历的完整的声明周期
function run(effect: any, fn: Function, args: any[]): any {
 if (!effect.active) {
  return fn(...args)
 }
 if (activeReactiveEffectStack.indexOf(effect) === -1) {
  try {
   activeReactiveEffectStack.push(effect)
   return fn(...args)
  } finally {
   activeReactiveEffectStack.pop()
  }
 }
}

一口气写了这么多,最后总结一下。在大家看源码的时候,如果发现有哪个地方无从下手的话,可以先从测试用例开始看。因为测试用例可以很清楚的知道这个函数想要达到什么效果,然后从效果上想,为什么这么做,如果我自己写的话应该怎么写,这样一点点就能揣摩出作者的意图了。再根据源码结合自己的想法你就能够学到很多。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
神奇的代码 通杀各种网站-可随意修改复制页面内容
Jul 17 Javascript
Jquery 实现Tab效果 思路是js思路
Mar 02 Javascript
javascript,jquery闭包概念分析
Jun 19 Javascript
js 判断checkbox是否选中的操作方法
Nov 09 Javascript
javascript利用apply和arguments复用方法
Nov 25 Javascript
moment.js轻松实现获取当前日期是当年的第几周
Feb 05 Javascript
详解VUE的状态控制与延时加载刷新
Mar 27 Javascript
JavaScrpt中如何使用 cookie 设置查看与删除功能
Jul 09 Javascript
JavaScript中的回调函数实例讲解
Jan 27 Javascript
小程序开发踩坑:页面窗口定位(相对于浏览器定位)(推荐)
Apr 25 Javascript
es6数组includes()用法实例分析
Apr 18 Javascript
微信小程序学习总结(一)项目创建与目录结构分析
Jun 04 Javascript
JavaScript如何把两个数组对象合并过程解析
Oct 10 #Javascript
Node使用Selenium进行前端自动化操作的代码实现
Oct 10 #Javascript
Vue 开发必须知道的36个技巧(小结)
Oct 09 #Javascript
浅谈vue项目用到的mock数据接口的两种方式
Oct 09 #Javascript
Vue3.0数据响应式原理详解
Oct 09 #Javascript
Vue分页插件的前后端配置与使用
Oct 09 #Javascript
vue3修改link标签默认icon无效问题详解
Oct 09 #Javascript
You might like
深入phpMyAdmin的安装与配置的详细步骤
2013/05/07 PHP
PHP的password_hash()使用实例
2014/03/17 PHP
PHP 获取ip地址代码汇总
2015/07/05 PHP
php根据用户语言跳转相应网页
2015/11/04 PHP
PHP实现的多维数组去重操作示例
2018/07/21 PHP
PHP使用Session实现上传进度功能详解
2019/08/06 PHP
CL vs ForZe BO5 第五场 2.13
2021/03/10 DOTA
检测jQuery.js是否已加载的判断代码
2011/05/20 Javascript
js arguments,jcallee caller用法总结
2013/11/30 Javascript
我用的一些Node.js开发工具、开发包、框架等总结
2014/09/25 Javascript
浅析Javascript中“==”与“===”的区别
2014/12/23 Javascript
js style.display=block显示布局错乱问题的解决方法
2016/09/21 Javascript
详解使用angular-cli发布i18n多国语言Angular应用
2017/05/20 Javascript
js实现鼠标拖拽多选功能示例
2017/08/01 Javascript
元素全屏的设置与监听实例
2017/11/28 Javascript
移动端H5页面返回并刷新页面(BFcache)的方法
2018/11/06 Javascript
[05:09]第二届DOTA2亚洲邀请赛决赛日比赛集锦:iG 3:0 OG夺冠
2017/04/05 DOTA
通过数据库对Django进行删除字段和删除模型的操作
2015/07/21 Python
Django自定义manage命令实例代码
2018/02/11 Python
安装python时MySQLdb报错的问题描述及解决方法
2018/03/20 Python
儿童学习python的一些小技巧
2018/05/27 Python
Python快速查找list中相同部分的方法
2018/06/27 Python
Python3连接SQLServer、Oracle、MySql的方法
2018/06/28 Python
详解Python3序列赋值、序列解包
2019/05/14 Python
基于Python新建用户并产生随机密码过程解析
2019/10/08 Python
基于Python数据分析之pandas统计分析
2020/03/03 Python
python实现扑克牌交互式界面发牌程序
2020/04/22 Python
Python爬虫逆向分析某云音乐加密参数的实例分析
2020/12/04 Python
玩具公司的创业计划书
2013/12/31 职场文书
运动会广播稿50字
2014/01/26 职场文书
犯错检讨书
2014/02/21 职场文书
六查六看个人剖析材料
2014/10/14 职场文书
个人工作决心书
2015/09/22 职场文书
2016关于学习党章的心得体会
2016/01/15 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
MySQL数据库配置信息查看与修改方法详解
2022/06/25 MySQL