Vue3 响应式原理之scheduler、stop

简介: 这里引用一下 Vue 官方的经典测试用例来测试 scheduler 功能

scheduler


这里引用一下 Vue 官方的经典测试用例来测试 scheduler 功能

it("scheduler", () => {
  let dummy;
  let run: any;
  const scheduler = jest.fn(() => {
    run = runner;
  });
  const obj = reactive({ foo: 1 });
  const runner = effect(
    () => {
      dummy = obj.foo;
    },
    { scheduler }
  );
  expect(scheduler).not.toHaveBeenCalled();
  expect(dummy).toBe(1);
  // should be called on first trigger
  obj.foo++;
  expect(scheduler).toHaveBeenCalledTimes(1);
  // // should not run yet
  expect(dummy).toBe(1);
  // // manually run
  run();
  // // should have run
  expect(dummy).toBe(2);
});
复制代码

测试用例的执行流程大致为:声明一个 scheduler,使用 reactive 声明一个对象 obj,使用 effect 对 obj.foo 进行依赖收集;此时断言传入的 scheduler 没有被执行,但是 fn 执行,所以 dummy 的值为 1;然后对 obj.foo 加一,此时应该调用一次 scheduler,scheduler 函数内部并没有对 dummy 的赋值, 所以 dummy 此时还是 1;然后执行 runner,对 dummy 进行赋值,此时 dummy 值为 2。

scheduler的作用是:当传入scheduler后,target修改的时候,trigger的时候绕过fn,直接执行传入的scheduler。

这就需要在 effect 里判断是否有 scheduler,如果有则执行 scheduler,否则执行 fn

根据这个思路我们来修改我们之前的 effect 函数

class ReactiveEffect {
  private _fn: Function
  public scheduler?: Function
  constructor(fn: Function, scheduler?: Function) {
    this._fn = fn
    this.scheduler = scheduler
  }
  run() {
    activeEffect = this
    return this._fn()
  }
}
let activeEffect: ReactiveEffect;
export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  return _effect.run.bind(_effect)
}
复制代码

然后我们需要在 trigger 函数中去判断执行 scheduler 还是 fn

export function trigger<T extends object>(target: T, key: keyof T) {
  let depsMap = targetMap.get(target)
  let dep = depsMap!.get(key) as Set<ReactiveEffect>
  for (const effect of dep) {
    if (effect.scheduler)
      effect.scheduler()
    else
      effect.run()
  }
}
复制代码

执行测试

1682565892(1).png


stop


同样的我们还是通过测试用例来完成 stop 功能的实现

it("stop", () => {
  let dummy;
  const obj = reactive({ prop: 1 });
  const runner = effect(() => {
    dummy = obj.prop;
  });
  obj.prop = 2;
  expect(dummy).toBe(2);
  stop(runner);
  obj.prop = 3;
  expect(dummy).toBe(2);
  // stopped effect should still be manually callable
  runner();
  expect(dummy).toBe(3);
});
复制代码

测试用例执行流程:使用 reactive 声明一个对象 obj,使用 effect 为 obj.prop 添加依赖(将 obj.prop的值赋给 dummy),然后给 obj.prop 赋值,此时 dummy 的值应该为 2;然后使用 stop 函数取消 obj.prop 的这个依赖,再给 obj.prop 加 1,此时 dummy 不会再被赋值,值仍然为 2;然后重新执行 runner,此时 dummy 又被重新赋值为 obj.prop。

然后我们来编写 stop 方法,它仍然是 effect 文件导出的一个模块

export function stop(runner: any) {
  runner.effect.stop()
}
复制代码

这个 runner 就是我们之前的 run 方法的包装,我们可以将之前的 effect 绑定在runner 函数之上

export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}
复制代码

然后我们在 ReactiveEffect 上声明 stop 方法和 deps 数组

class ReactiveEffect {
  private _fn: Function
  public scheduler?: Function
  deps: Set<ReactiveEffect>[] = []
  constructor(fn: Function, scheduler?: Function) {
    this._fn = fn
    this.scheduler = scheduler
  }
  run() {
    activeEffect = this
    return this._fn()
  }
  stop() {
    this.deps.forEach(dep => {
      dep.delete(this)
    })
  }
}
复制代码

其次,需要在收集依赖的时候同时将依赖保存到 effect 上

export function track<T extends object>(target: T, key: keyof T) {
  // target -> key -> dep
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }
  // 如果没有 activeEffect 不执行依赖收集
  if (!activeEffect) return;
  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}
复制代码

此时执行测试

1682565919(1).png

可以看到已经完成了测试用例,但是,这时候代码还不够完善,有很大的优化空间。

为了代码的可读性,我们将 stop 的逻辑抽离出来作为一个函数

// 清除依赖 
function cleanupEffect(effect: ReactiveEffect) {
  effect.deps.forEach(dep => {
    dep.delete(effect)
  })
}
复制代码

此外还有一个性能问题,每次当我们调用 stop 的时候,都会遍历清除,但是,只需要清除一次之后就不需要再清除了,所以我么你可以通过提供一个状态来控制 stop 的次数

stop() {
  if (this.active) {
    cleanupEffect(this)
    this.active = false
  }
}
复制代码

再次执行测试,测试仍然成功。

然后,与 stop 相关的还有一个 onStop 事件,我们还是通过测试用例来入手

it("events: onStop", () => {
  const onStop = jest.fn();
  const runner = effect(() => {}, {
    onStop,
  });
  stop(runner);
  expect(onStop).toHaveBeenCalled();
});
复制代码

onStop 是一个回调函数,当触发 stop 时会被调用

export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.onStop = options.onStop
  _effect.run()
  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}
复制代码

然后在 ReactiveEffect 内声明 onStop,在调用 cleanupEffect 之后调用

stop() {
  if (this.active) {
    cleanupEffect(this)
    if (this.onStop) {
      this.onStop()
    }
    this.active = false
  }
}
复制代码

执行测试,通过

1682565947(1).png

这里的 onStop 赋值可以优化一下,后面还有会很多属性复制,所以我们直接将这里的赋值逻辑优化一下

// /src/shared/index.ts
export const extend = Object.assign
// /src/reactivity/effect.ts
export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  // 将 options 的内容合并到 effet 上
  extend(_effect, options)
  _effect.run()
  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}
复制代码

虽然上面的测试用例通过了,但是,如果将测试用例中的 obj.prop = 3改为obj.prop++再执行测试用例会发现测试失败。

(下面的代码在 isReactive 之后完善,所以会有较大变化, 可以看完 isReactive 之后再看这里)

这是因为,obj.prop++的操作是一个 get + set 的操作,完整的表达式应该是obj.prop = obj.prop + 1,在 get 的过程中会重新触发依赖收集,所以导致上面的 stop 失效。

解决这个问题可以通过再声明一个全局变量来控制,当变量为 false 时停止收集依赖

// global
let shouldTrack: boolean;
// class ReactiveEffect
run() {
  if (!this.active)
    // 如果被 stop 直接执行返回
    return this._fn()
  // 否则进行依赖收集
  shouldTrack = true
  activeEffect = this
  const result = this._fn()
  shouldTrack = false // 依赖收集结束之后将状态重置
  return result
}
// track
export function track<T extends object>(target: T, key: keyof T) {
  // target -> key -> dep
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }
  // 如果没有激活的 effect 中断执行
  if (!activeEffect) return;
  // 如果不应该收集依赖, 中断执行
  if (!shouldTrack) return
  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}
复制代码

此时执行测试,测试通过

相关文章
|
5天前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
|
8天前
|
JavaScript 前端开发 算法
高效工作流:用Mermaid绘制你的专属流程图;如何在Vue3中导入mermaid绘制流程图
mermaid是一款非常优秀的基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。非常适合新手学习或者做一些弱交互且自定义要求不高的图表 除了流程图以外,mermaid还支持序列图、类图、状态图、实体关系图等图表可供探索。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
8天前
|
JavaScript 前端开发 API
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
onMounted作为vue3中最常用的钩子函数之一,能够灵活、随心应手的使用是每个Vue开发者的必修课,同时根据其不同写法的特性,来选择最合适最有利于维护的写法。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
18天前
|
资源调度 JavaScript 前端开发
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
|
8天前
|
JavaScript 前端开发 API
管理数据必备;侦听器watch用法详解,vue2与vue3中watch的变化与差异
一篇文章同时搞定Vue2和Vue3的侦听器,是不是很棒?不要忘了Vue3中多了一个可选项watchEffect噢。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
3月前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
177 1
|
18天前
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
94 56
|
8天前
|
数据采集 资源调度 JavaScript
极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图
本文介绍了使用 Vue Flow 绘制流程图的方法与技巧。Vue Flow 是一个灵活强大的工具,适合自定义复杂的流程图。文章从环境要求(Node.js v20+ 和 Vue 3.3+)、基础入门案例、自定义功能(节点与连线的定制、事件处理)到实际案例全面解析其用法。重点强调了 Vue Flow 的高度灵活性,虽然预定义内容较少,但提供了丰富的 API 支持深度定制。同时,文中还分享了关于句柄(handles)的使用方法,以及如何解决官网复杂案例无法运行的问题。最后通过对比 mermaid,总结 Vue Flow 更适合需要高度自定义和复杂需求的场景,并附带多个相关技术博客链接供进一步学习。
|
8天前
|
存储 数据采集 供应链
属性描述符初探——Vue实现数据劫持的基础
属性描述符还有很多内容可以挖掘,比如defineProperty与Proxy的区别,比如vue2与vue3实现数据劫持的方式有什么不同,实现效果有哪些差异等,这篇博文只是入门,以后有时间再深挖。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
1月前
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。

热门文章

最新文章