解读vue3源码-响应式篇3 effect副作用函数

提示:看到我 请让我滚去学习

文章目录


前言

什么是副作用函数?

在 Vue 3 中,副作用函数(Effect Function)通常指的是那些依赖于响应式数据并在数据变化时重新执行的函数。Vue 3 引入了 ref 和 reactive API 来创建响应式数据,以及 effect 函数来观察和处理这些数据的变化。

当一个函数被标记为副作用函数时,它会被 Vue 的响应式系统跟踪。这意味着每当这个函数依赖的响应式数据发生变化时,Vue 会自动重新执行这个函数,从而触发 UI 的更新或其他相关操作。

ts 复制代码
例如:
import { ref, effect } from 'vue';
const count = ref(0);
// 定义一个副作用函数
effect(() => {
  console.log('Count changed:', count.value);
});
// 更改count的值,副作用函数将被重新执行
count.value++;

effect问题拓展

以我们的精简版本的watchEffect 函数做模本例子,我们将对其进行拓展

ts 复制代码
  function watchEffect (effect) {
    activeEffect = effect
    effect()
  }

全局targetMap存储结构:

分支切换与 cleanup

我们就简单的watchEffect ,会有很多问题,比如下面的示例:

ts 复制代码
  const data = { ok: true, a: 111, b: 222 }
  const obj = reactive(data)
  watchEffect(function effectFn () {
    console.log(obj.ok ? obj.a : obj.b)
  })
  //111
  obj.ok = false  //222
  obj.a = 3 //222

我们使用一个三元表达式,第一次初始化watchEffect函数,因为obj.ok 为true,所以访问的是obj.ok和obj.a,那么全局targetMap如下:

当我们修改obj.ok的值后,重新触发watchEffect函数,再次访问内部依赖,因为obj.ok值改变,所以访问的属性变成了obj.ok和obj.b,这样obj.b就会收集到targetMap中,全局targetMap如下:

这样就有了问题,我们最新的effect其实是ok和b的依赖,但是依赖表中存储了ok、a、b三个属性,所以当我们访问a时,还是会触发watchEffect重新执行。

解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除,要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它。所以我们在响应式数据访问收集副作用函数时,需要给副作用函数添加一个dep[]属性,然后将当前触发依赖收集的属性push到此dep中。

那我们来看vue3源码实现:

其实过程就是:实现双向记忆(运行effect时,找到所有绑定这个effect的属性,删除所有属性上的此effect)

1.首先ReactiveEffect类中定义了属性_depsLength,这个属性是为了记录当前收集的dep是同一个effect中的第几个响应式属性

在run方法中每次执行副作用函数前都会调用preCleanupEffect方法,这个方法就是重置当前effect内部的指针,_trackId用于记录当前effect实例是第几次运行,_depsLength表示当前是第几个dep收集,每次重走effect方法,都会重置_depsLength为0,表示从第1个开始收集

而在track函数执行收集依赖时,我们看下effect是怎么记录dep的

这里有一个简单的diff算法,我们知道_depsLength用来记录当前dep是effect方法中第几个记录的,比如我在上述示例:

ts 复制代码
 watchEffect(function effectFn () {
    console.log(obj.ok ? obj.a : obj.b)
  })

当obj.ok为true时,第一次进入watchEffect,effectFn副作用函数中监听的值为ok和a,那么effectFn的deps[]中就是[ok,a],当我们修改ok的值,就会触发effectFn执行,effectFn中再次访问值为ok和b,这时候会按照顺序将新值和旧值按顺序对比,第一个dep不变是ok,然后用b替换a。

ts 复制代码
  const stop=effect(function effectFn111 () {
        console.log('11111', obj.a, obj.a)
      })

此处_trackId用于记录当前effect副作用函数运行次数,if (dep.get(effect2) !== effect2._trackId) {}。可以过滤到上面这种一个函数中多次使用一个变量的情况

此时还会有一个新问题,那么如果第一次副作用函数绑定了2个属性上,但是第二次运行时只需要绑定1个了,例如

ts 复制代码
 watchEffect(function effectFn () {
    console.log(obj.ok ? obj.a : null)
  })

那么在执行完替代后需要根据新列表数量,删除多余列表长度,实现代码如下:

vue3.0是使用set存储effect依赖,但是set是无序的,而这也是为什么改成有序的map存储effects的原因之一。

嵌套的 effect 与 effect 栈

我们vue3是可以支持组件嵌套的,所以本质effect也是应该要支持嵌套,例如:

ts 复制代码
effect(function effectFn1() {
	effect(function effectFn2() { /* ... */ })
    console.log(pro.a)
 })

但是这样就会有问题,像上面这样写,pro.a绑定的副作用函数是effectFn2并非effectFn1,这是因为我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。

而我们的代码中只需要在effect运行之初记录上次的effect,在副作用函数执行完毕时,恢复上次的effect就行。

解决在副作用函数中同时读取和操作同一属性时无限循环

ts 复制代码
  const data = { ok: true, a: 111, b: 222 }
  const obj = reactive(data)
  watchEffect(function effectFn111 () {
    console.log('11111', obj.a++)
  })
  obj.a = 666

这种在副作用函数中既读取又操作一个属性会爆栈,这是因为首先读取 obj.a的值,这会触发 track 操作,将当前副作用函数收集到"桶"中,接着将其加 1 后再赋值给 obj.a,此时会触发 trigger 操作,即把"桶"中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

解决方案,在trigger触发时判断要执行的副作用函数是否正在执行,正在执行就跳过。

vue3代码通过_running变量控制

effect函数实现

effect函数原本在vue3.0中是没有暴露的,vue官方文档并没有相关说明,那么我们看看它是怎么实现的。

以上就是effect函数的实现,接收两个参数,一个是运行时的副作用函数,另一个options,主要作用如下:

这个函数就是实例化并抛出了ReactiveEffect类,并且根据参数运行一次对象的run方法,那我们进入ReactiveEffect中看看ReactiveEffect是怎么实现的

ReactiveEffect主要方法是包含

初始化方法,

get diety()和set dirty()属性访问器(主要用于记录computed-api是否需要重新计算),

run方法,

stop方法:停止effect,watch函数返回值即stop函数,执行停止监听。

我们先看看构造方法中做了什么

fn:即我们传入的副作用函数,我们将其赋值给class内部属性,可以预见其在我们的run函数触发的时候执行,

shouldSchedule:为调度器,默认为false,如果外部传入,我们用shouldSchedule来执行副作用函数

active:标识创建的effect是响应式的,若为false就不会赋值activeEffect关联属性

dirtyLevel:计算属性使用,记录是否是脏数据

trackId:用于记录当前effect执行了几次

runnings:是否正在执行

deps:当前副作用函数绑定的dep,属性和effect做双向记忆

deplength:用来标识当前属性是同一个effect中收集第几个依赖

我们看run方法,也就是执行方法。这个方法其实主要就是做了2件事:

1.将这个effect副作用函数赋值给全局的activeEffect,在响应式数据触发读取的时候,和数据访问属性进行绑定

2.执行一次副作用函数

至此总体过一遍响应式流程:

例子:effect(function fn(){ proxya.name })

1.effect函数运行创建ReactiveEffect对象,运行其run方法。

2.run方法将ReactiveEffect赋值给全局的activeEffect对对象,并且运行执行方法fn,

3.fn方法中访问proxya.name,触发name属性拦截,触发依赖收集将activeEffect(即fn)和proxya.name绑定。

4.effect运行完毕全局activeEffect赋值为上个堆栈的activeEffect对象(此处为undefined)

5.修改proxya.name值,触发set拦截,触发依赖执行proxya.name绑定的effect(即3中绑定的ReactiveEffect---fn方法)

computed-api 实现图解

总结

至此我们就能大体了解响应式数据的实现,而对于一些衍生的api,如watch、watchEffecte、computed等我们都可自行阅读源码,其本质都是使用RactiveEffect类实现。

相关推荐
GDAL3 分钟前
HTML5中Checkbox标签的深入全面解析
前端·html·html5
Java开发追求者22 分钟前
npm镜像源证书过期的问题解决
前端·npm·node.js·npm镜像源证书过期的问题解决
宝子向前冲30 分钟前
React中九大常用Hooks总结
前端·javascript·react.js
小白小白从不日白1 小时前
react 基础语法
前端·react.js
岸边的风1 小时前
前端Excel热成像数据展示及插值算法
前端·算法·excel
不良人龍木木2 小时前
sqlalchemy FastAPI 前端实现数据库增删改查
前端·数据库·fastapi
c1tenj22 小时前
Jedis,SpringDataRedis
前端
Code成立2 小时前
HTML5中IndexedDB前端本地数据库
前端·数据库·html5·indexeddb
Code成立3 小时前
最新HTML5中的文件详解
前端·html·html5