Vue3响应式核心之effect怎么使用

广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买

Vue3响应式核心之effect怎么使用

通常情况下我们是不会直接使用effect的,因为effect是一个底层的API,在我们使用Vue3的时候Vue默认会帮我们调用effect。 effect翻译为作用,意思是使其发生作用,这个使其的其就是我们传入的函数,所以effect的作用就是让我们传入的函数发生作用,也就是执行这个函数。 执行过程简图如下:

接下来先通过例子了解effect的基本用法,然后再去了解原理。

一、effect用法1、基本用法
const obj = reactive({count: 1})const runner = effect(() => {  console.log(obj.count)})obj.count++
登录后复制

结果会先打印1, 然后在obj.count++之后打印出2。

流程简图如下:

运行effect(fun)

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}
登录后复制

console.log(obj.count)track依赖收集 结构如下:

obj.count++触发依赖,执行runner.run(), 实际运行的是

() => {  console.log(obj.count)}
登录后复制

所以又打印出2

2、lazy属性为true

此值为 true 时,只有在第一次手动调用 runner 后,依赖数据变更时,才会自动执行 effect 的回调,可以理解为 effect 的是在手动调用 runner 后才首次执行

const obj = reactive({count: 1})const runner = effect(() => {  console.log(obj.count)}, {  lazy: true})runner()obj.count++
登录后复制

只会打印出2

原因是effect源码中有如下逻辑:

3、options中包含onTrack
let events = []const onTrack = (e) => {  events.push(e)}const obj = reactive({ foo: 1, bar: 2 })const runner = effect(  () => {    console.log(obj.foo)  },  { onTrack })console.log('runner', runner)obj.foo++console.log("events", events)
登录后复制

看下events的打印结果:

[  {    effect: runner,  // effect 函数的返回值    target: toRaw(obj),  // 表示的是哪个响应式数据发生了变化    type: TrackOpTypes.GET,  // 表示此次记录操作的类型。 get 表示获取值    key: 'foo' }]
登录后复制二、源码分析1、effect方法的实现
// packages/reactivity/src/effect.tsexport interface ReactiveEffectOptions extends DebuggerOptions {  lazy?: boolean  scheduler?: EffectScheduler  scope?: EffectScope  allowRecurse?: boolean  onStop?: () => void}export function effect<T = any>(  fn: () => T, // 副作用函数  options?: ReactiveEffectOptions // 结构如上): ReactiveEffectRunner {  // 如果 fn 对象上有 effect 属性  if ((fn as ReactiveEffectRunner).effect) {    // 那么就将 fn 替换为 fn.effect.fn    fn = (fn as ReactiveEffectRunner).effect.fn  }  // 创建一个响应式副作用函数  const _effect = new ReactiveEffect(fn)  if (options) {    // 将配置项合并到响应式副作用函数上    extend(_effect, options)    // 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)    if (options.scope) recordEffectScope(_effect, options.scope)  }  if (!options || !options.lazy) { // options.lazy 不为true    _effect.run() // 执行响应式副作用函数 首次执行fn()  }  // _effect.run作用域绑定到_effect  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner  // 将响应式副作用函数赋值给 runner.effect  runner.effect = _effect  return runner}
登录后复制

核心代码:

创建一个响应式副作用函数const _effect = new ReactiveEffect(fn),其运行结果如下:

非lazy状态执行响应式副作用函数_effect.run()

if (!options || !options.lazy) { // options.lazy 不为true  _effect.run() // 执行响应式副作用函数 首次执行fn()}
登录后复制

_effect.run作用域绑定到_effect

// _effect.run作用域绑定到_effect  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
登录后复制

返回副作用函数runner

2、ReactiveEffect函数源码
export class ReactiveEffect<T = any> {  active = true  deps: Dep[] = [] // 响应式依赖项的集合  parent: ReactiveEffect | undefined = undefined  /**   * Can be attached after creation   * @internal   */  computed?: ComputedRefImpl<T>  /**   * @internal   */  allowRecurse?: boolean  /**   * @internal   */  private deferStop?: boolean  onStop?: () => void  // dev only  onTrack?: (event: DebuggerEvent) => void  // dev only  onTrigger?: (event: DebuggerEvent) => void  constructor(    public fn: () => T,    public scheduler: EffectScheduler | null = null,    scope?: EffectScope  ) {    // 记录当前 ReactiveEffect 对象的作用域    recordEffectScope(this, scope)  }  run() {    // 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果    if (!this.active) {      return this.fn()    }    // 寻找当前 ReactiveEffect 对象的最顶层的父级作用域    let parent: ReactiveEffect | undefined = activeEffect    let lastShouldTrack = shouldTrack // 是否要跟踪    while (parent) {      if (parent === this) {        return      }      parent = parent.parent    }    try {      // 记录父级作用域为当前活动的 ReactiveEffect 对象      this.parent = activeEffect      activeEffect = this  // 将当前活动的 ReactiveEffect 对象设置为 “自己”      shouldTrack = true // 将 shouldTrack 设置为 true (表示是否需要收集依赖)      // effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1      trackOpBit = 1 << ++effectTrackDepth      if (effectTrackDepth <= maxMarkerBits) {        // 初始依赖追踪标记        initDepMarkers(this)      } else {        // 清除依赖追踪标记        cleanupEffect(this)      }      // 返回副作用函数执行结果      return this.fn()    } finally {      // 如果 effect调用栈的深度 没有超过阈值      if (effectTrackDepth <= maxMarkerBits) {        // 确定最终的依赖追踪标记        finalizeDepMarkers(this)      }      // 执行完毕会将 effectTrackDepth 减 1      trackOpBit = 1 << --effectTrackDepth      // 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”      activeEffect = this.parent      // 将 shouldTrack 设置为上一个值      shouldTrack = lastShouldTrack      // 将父级作用域设置为 undefined      this.parent = undefined      // 延时停止,这个标志是在 stop 方法中设置的      if (this.deferStop) {        this.stop()      }    }  }  stop() {    // stopped while running itself - defer the cleanup    // 如果当前 活动的 ReactiveEffect 对象是 “自己”    // 延迟停止,需要执行完当前的副作用函数之后再停止    if (activeEffect === this) {      // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法      this.deferStop = true    } else if (this.active) {// 如果当前 ReactiveEffect 对象处于活动状态      cleanupEffect(this) // 清除所有的依赖追踪标记      if (this.onStop) {         this.onStop()      }      this.active = false // 将 active 设置为 false    }  }}
登录后复制

run方法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖;

stop方法的作用就是停止当前的ReactiveEffect对象,停止之后,就不会再收集依赖了;

activeEffect和this并不是每次都相等的,因为activeEffect会跟着调用栈的深度而变化,而this则是固定的;

三、依赖收集相关1、如何触发依赖收集

在副作用函数中, obj.count就会触发依赖收集

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}0
登录后复制

触发的入口在get拦截器里面

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}1
登录后复制2、track源码
// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}2
登录后复制

shouldTrack在上面也讲过,它的作用就是控制是否收集依赖;

activeEffect就是我们刚刚讲的ReactiveEffect对象,它指向的就是当前正在执行的副作用函数;

track方法的作用就是收集依赖,它的实现非常简单,就是在targetMap中记录下target和key;

targetMap是一个WeakMap,它的键是target,值是一个Map,这个Map的键是key,值是一个Set;

targetMap的结构伪代码如下:

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}3
登录后复制

以上是最原始的depMap

dev环境为增加响应式调试会增加eventInfo

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}4
登录后复制

eventInfo结构如下:

trackEffects(dep, eventInfo)

如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去, 作用就把对象的属性操作与副作用函数建立关联,接下来看trackEffects

3、trackEffects(dep, eventInfo)源码解读
// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}5
登录后复制

dep.add(activeEffect!) 如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去

最终生成的depTarget结构如下:

四、触发依赖

比如例子中代码obj.count++就会触发set拦截,触发依赖更新

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}6
登录后复制1、trigger依赖更新
// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}7
登录后复制

const depsMap = targetMap.get(target) 获取 targetMap 中的 depsMap targetMap结构如下:

执行以上语句之后的depsMap结构如下:

将 depsMap 中 key 对应的 ReactiveEffect 对象添加到 deps 中deps.push(depsMap.get(key))之后的deps结构如下:

triggerEffects(deps[0], eventInfo)

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}8
登录后复制

trigger函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。

在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给triggerEffects函数去执行,接下来看看triggerEffects函数。

2、triggerEffects(deps[0], eventInfo)
// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}9
登录后复制

主要步骤

const effects = isArray(dep) ? dep : [...dep]获取effects

triggerEffect(effect, debuggerEventExtraInfo)执行effect,接下来看看源码

3、triggerEffect(effect, debuggerEventExtraInfo)
() => {  console.log(obj.count)}0
登录后复制

effect.run()就是执行副作用函数

以上就是Vue3响应式核心之effect怎么使用的详细内容,更多请关注9543建站博客其它相关文章!

广告:SSL证书一年128.66元起,点击购买~~~

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。
作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:探讨uniapp应用中“确认删除”功能的设计理念和实现方案
下一篇:vue输入框标签点击取消

发表评论

关闭广告
关闭广告