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

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


前言

什么是副作用函数?

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

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

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

effect问题拓展

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

  function watchEffect (effect) {
    activeEffect = effect
    effect()
  }

全局targetMap存储结构:
在这里插入图片描述

分支切换与 cleanup

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

  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方法中第几个记录的,比如我在上述示例:

 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。

  const stop=effect(function effectFn111 () {
        console.log('11111', obj.a, obj.a)
      })

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

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

 watchEffect(function effectFn () {
    console.log(obj.ok ? obj.a : null)
  })

那么在执行完替代后需要根据新列表数量,删除多余列表长度,实现代码如下:
在这里插入图片描述
vue3.0是使用set存储effect依赖,但是set是无序的,而这也是为什么改成有序的map存储effects的原因之一。

嵌套的 effect 与 effect 栈

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

effect(function effectFn1() {
	effect(function effectFn2() { /* ... */ })
    console.log(pro.a)
 })

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

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

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

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答你的问题。首先,我们需要了解Vue3的响应式系统是如何工作的。Vue3使用了一个名为`Reactive`的函数来实现响应式。 `Reactive`函数的作用是将一个普通的JavaScript对象转换成响应式的对象。当响应式对象的属性被修改时,所有依赖该属性的地方都会自动更新。 下面是`Reactive`函数的实现: ```javascript function Reactive(obj) { const handlers = { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); track(target, prop); return isObject(value) ? Reactive(value) : value; }, set(target, prop, value, receiver) { const oldValue = Reflect.get(target, prop, receiver); let result = true; if (oldValue !== value) { result = Reflect.set(target, prop, value, receiver); trigger(target, prop); } return result; }, deleteProperty(target, prop) { const result = Reflect.deleteProperty(target, prop); trigger(target, prop); return result; } }; return new Proxy(obj, handlers); } ``` `Reactive`函数接受一个普通的JavaScript对象作为参数,返回一个响应式的对象。在实现中,我们使用了ES6的Proxy对象来实现响应式。 在`get`处理器中,我们使用了`track`函数来收集依赖。`track`函数的作用是将当前正在执行的计算函数添加到依赖列表中。 在`set`处理器中,我们首先获取旧值,然后判断新值是否与旧值相同。如果不同,我们使用`trigger`函数来触发更新。`trigger`函数的作用是遍历依赖列表,执行所有计算函数。 在`deleteProperty`处理器中,我们使用`trigger`函数来触发更新,因为删除属性也可能导致依赖更新。 在以上代码中,我们还使用了`isObject`函数来判断一个值是否为对象。该函数的实现如下: ```javascript function isObject(value) { return typeof value === 'object' && value !== null; } ``` 这个函数非常简单,它只是判断一个值是否为对象。如果是对象,我们就递归调用`Reactive`函数来将该对象转换成响应式。 总之,这就是Vue3的响应式系统的实现原理。通过`Reactive`函数和Proxy对象,我们可以将一个普通的JavaScript对象转换成响应式的对象,并实现自动更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值