解读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类实现。

相关推荐
每天都要进步哦9 分钟前
Node.js中的fs模块:文件与目录操作(写入、读取、复制、移动、删除、重命名等)
前端·javascript·node.js
布兰妮甜34 分钟前
Three.js 渲染技术:打造逼真3D体验的幕后功臣
javascript·3d·three.js·幕后
仿生狮子1 小时前
CSS Layer、Tailwind 和 sass 如何共存?
javascript·css·vue.js
在路上`1 小时前
vue3使用AntV X6 (图可视化引擎)历程[二]
javascript·vue.js
brzhang1 小时前
开源了一个 Super Copy Coder ,0 成本实现视觉搞转提示词,效率炸裂
前端·人工智能
diaobusi-881 小时前
HTML5-标签
前端·html·html5
我命由我123452 小时前
CesiumJS 案例 P34:场景视图(3D 视图、2D 视图)
前端·javascript·3d·前端框架·html·html5·js
就是蠢啊2 小时前
封装/前线修饰符/Idea项目结构/package/impore
java·服务器·前端
SunnyRivers2 小时前
JavaScript动态渲染页面爬取之Selenium
javascript·selenium
小盼江2 小时前
智能服装推荐系统 协同过滤余弦函数推荐服装 Springboot Vue Element-UI前后端分离
大数据·数据库·vue.js·spring boot·ui·毕业设计