Vue3响应系统的实现(二)

news2025/7/8 21:21:55

前言

继上一篇文章,我们已经能够实现一个简单的响应系统了,但是仍然存在很多缺陷,本篇文章将具体叙述一下存在的缺陷以及如何解决,最后实现一个较为完善的响应式系统

解决副作用函数硬编码问题

从上一篇文章中我们不难发现响应系统的工作流程大致如下:

  1. 当读取操作发生时,将副作用函数收集到“桶”中;
  2. 当设置操作发生时,从“桶”中取出副作用函数并执行;

而上一篇文章中我们实现的响应式系统存在硬编码问题,而我们希望的是,哪怕副作用函数是个匿名函数,也能够被正确地收集到“桶”中。

为了实现这个需求,我们需要提供一个用来注册副作用函数的机制,如: 

//用一个全局变量存储被注册的副作用函数
let activeEffect
//effect 函数用于注册副作用函数
function effect(fn){
    //当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
    activeEffect=fn
    //执行副作用函数
    fn()
}

如上代码,首先,定义了一个全局变量 activeEffect ,初始值是 undefined ,它的作用是存储被注册的副作用函数。接着重新定义了 effect 函数,它变成了一个用来注册副作用函数的函数, effect 函数接收一个参数 fn ,即要注册的副作用函数。

同时需要修改代理函数:

const bucket = new Set()

let data = {text:"hello world"}
let obj = new Proxy(data,{
  get(target,key){
    //将activeEffect中存储的副作用函数收集到“桶”中
    if(activeEffect){
        //将副作用函数effect添加到存储副作用函数的桶中
        bucket.add(activeEffect)
    }
    return target[key]
  },
  set(target,key,newVal){
    target[key]=newVal
    //把副作用函数从桶中取出来并执行
    bucket.forEach(fn=>fn())
    return true
  }
})

这样我们就解决了副作用函数硬编码的问题,我们可以写一个简单的测试代码来测试一下,如:

effect(
    ()=>{
        console.log("我是匿名函数")
        document.body.innerText=obj.text
    }
  )
  setTimeout(()=>{
    obj.text="你好 世界"
  },1000)

在浏览器中运行上段代码,会发现结果与我们预期的一样。

但是如果我们运行如下代码,猜猜会发生什么:

  effect(
    ()=>{
        console.log("我是匿名函数")
        document.body.innerText=obj.text
    }
  )
  setTimeout(()=>{
    //副作用函数中并没有读取abc属性的值
    obj.abc="你好 世界"
  },1000)

我们知道,在匿名副作用函数内并没有读取 obj . abc 属性的值,所以理论上,字段 obj . abc 并没有与副作用建立响应联系,因此,定时器内语句的执行不应该触发匿名副作用函数重新执行。但如果我们执行上述这段代码就会发现,定时器到时后,匿名副作用函数却重新执行了,这是不正确的。

导致这个问题的根本原因是,我们没有在副作用函数与被操作的目标字段之间建立明确的联系

我们之前设计的响应系统会在读取属性时,无论读取的是什么属性都会把副作用函数收集到“桶”中,设置属性时,无论设置的是哪一个属性,都会把“桶”中的副作用函数取出来执行;副作用函数与被操作的字段之间没有联系

为了解决这个问题,我们需要重新设计“桶”结构

树形结构的“桶”

树形结构的“桶”可以很好的解决上面的问题

例如我们有这么一段代码:

effect(function fn1(){
  obj.text1
  obj.text2
})

那么它们的依赖关系如下:

 又如这段代码:

effect(function fn1(){
  obj.text
})
effect(function fn2(){
  obj.text
})

它们的依赖关系如下:

 

出于性能考虑,我们用WeakMap代替Set作为“桶”的数据结构,然后修改get/set拦截器代码,如下:

//存储副作用函数的桶
const bucket = new WeakMap()

//原始数据
let data = {text:"hello world"}
//对原始数据的代理
let obj = new Proxy(data,{
  //拦截读取操作
  get(target,key){
    //没有activeEffect,直接return
    if(!activeEffect) return target[key]
    //根据target从“桶”中取得depsMap,它也是一个Map类型:key => effects
    let depsMap=bucket.get(target)
    //如果不存在depsMap,那么新建一个Map并与target关联
    if(!depsMap){
        bucket.set(target,(depsMap=new Map()))
    }
    //再根据key从depsMap中取出deps,它是一个Set类型,里面
    //存储着所有与当前key相关联的副作用函数:effects
    let deps=depsMap.get(key)
    //如果deps不存在,同样新建一个Set并与key相关联
    if(!deps){
        depsMap.set(key,(deps=new Set()))
    }
    //最后将当前激活的副作用函数添加到桶中
    deps.add(activeEffect)
    return target[key]
  },
  //拦截设置操作
  set(target,key,newVal){
    //设置属性值
    target[key]=newVal
    //根据target从“桶”中取出depsMap
    const depsMap=bucket.get(target)
    if(!depsMap)return
    //根据key从depsMap中取得所有的副作用函数effects
    const effects=depsMap.get(key)
    //执行副作用函数
    effects&&effects.forEach(fn=>fn())
    return true
  }
})

这样使得get/set拦截器非常复杂,可以将这些代码做一些封装处理,如:

let obj = new Proxy(data,{
    //拦截读取操作
    get(target,key){
      //将副作用函数activeEffect添加到存储副作用函数的桶中
      track(target,key)
      //返回属性值
      return target[key]
    },
    //拦截设置操作
    set(target,key,newVal){
      //设置属性值
      target[key]=newVal
      //把副作用函数从桶中取出来并执行
      trigger(target,key)
      //返回true代表设置操作成功
      return true
    }
  })

  function track(target,key){
     //没有activeEffect,直接return
     if(!activeEffect) return 
     let depsMap=bucket.get(target)
     if(!depsMap){
         bucket.set(target,(depsMap=new Map()))
     }
     let deps=depsMap.get(key)
     if(!deps){
         depsMap.set(key,(deps=new Set()))
     }
     //最后将当前激活的副作用函数添加到桶中
     deps.add(activeEffect)
  }
  function trigger(target,key){
    target[key]=newVal
    const depsMap=bucket.get(target)
    if(!depsMap)return
    const effects=depsMap.get(key)
    effects&&effects.forEach(fn=>fn())
  }

遗留的副作用函数

上面的代码仍然有问题,比如副作用函数中有一个三元表达式,两个分支分别读取了同一个对象的不同属性,而执行的时候只能用上其中一个,我们需要的是指在执行时用上的那个属性进行副作用函数依赖绑定,也就是修改了那个属性才会触发副作用函数执行而另一个属性修改了不执行,但是我们的代码不管是哪个属性修改了,副作用函数都会执行,显然是不合理的,解决方法为:

let activeEffect;
function effect(fn){
    const effectFn=()=>{
    //调用cleanup函数完成清除工作
    cleanup(effectFn)
    //当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
    activeEffect=effectFn
    //执行副作用函数
    fn()
  }
  //存储所有与该副作用函数有关的依赖集合
  effectFn.deps=[]
  effectFn()
}

const data={foo:true,bar:true}
let obj = new Proxy(data,{
    //拦截读取操作
    get(target,key){
      //将副作用函数activeEffect添加到存储副作用函数的桶中
      track(target,key)
      //返回属性值
      return target[key]
    },
    //拦截设置操作
    set(target,key,newVal){
      //设置属性值
      target[key]=newVal
      //把副作用函数从桶中取出来并执行
      trigger(target,key)
      //返回true代表设置操作成功
      return true
    }
  })
//存储副作用函数的桶
const bucket = new WeakMap()

  function track(target,key){
     //没有activeEffect,直接return
     if(!activeEffect) return 
     let depsMap=bucket.get(target)
     if(!depsMap){
         bucket.set(target,(depsMap=new Map()))
     }
     let deps=depsMap.get(key)
     if(!deps){
         depsMap.set(key,(deps=new Set()))
     }
     //最后将当前激活的副作用函数添加到桶中
     deps.add(activeEffect)
     //deps就是一个与当前副作用函数存在关联的依赖集合
     activeEffect.deps.push(deps)
  }
  function trigger(target,key){
    //target[key]=newVal
    const depsMap=bucket.get(target)
    if(!depsMap)return
    const effects=depsMap.get(key)
    const effectsToRun=new Set(effects)
    effectsToRun.forEach(effectFn=>effectFn())
    // effects&&effects.forEach(fn=>fn())
  }

  function cleanup(effectFn){
    for(let i=0;i<effectFn.deps.length;i++){
        const deps=effectFn.deps[i]
        //将effectFn从依赖集合中移除
        deps.delete(effectFn)
    }
    //最后需要重置effectFn.deps的值
    effectFn.deps.length=0
  }

每次执行副作用函数前,根据 effectFn.deps获取所有相关联的依赖集合,进而将副作用函数从依赖中移除

避免无限递归循环

当我们执行如下这段代码时,会发现程序报错

  effect(()=>{
    obj.foo=obj.foo+1
  })

在这个语句中,既会读取obj.foo的值又会设置它的值,从而导致无限的递归调用,导致栈溢出

为了解决这个问题,我们分析上一段代码发现:

读取和设置是在同一个副作用函数内进行的,此时无论是track时收集的副作用函数还是trigger时触发执行的副作用函数,都是activeEffect

基于此,我们将trigger函数修改为:

 function trigger(target,key){
    // target[key]=newVal
    const depsMap=bucket.get(target)
    if(!depsMap)return
    const effects=depsMap.get(key)
    const effectsToRun=new Set()
    effects&&effects.forEach(effectFn=>{
      if(effectFn!==activeEffect){
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn=>effectFn())
    // effects&&effects.forEach(fn=>fn())
  }

我们在trigger动作发生时增加守卫条件:如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/9667.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

概率 | 【提神醒脑】重难点专题突破 自用笔记

本文总结参考于 kira 2023概率提神醒脑技巧班 中 —— 重难点专题。 笔记均为自用整理。加油!ヾ(◍∇◍)ノ゙ 一研为定! 一、条件均匀 / 指数 / 二项…分布 -------------------------------------------------------------------------------------------------------------…

基于inquirer实现一个控制台多级选择交互功能

说在前面 &#x1f388;在前端脚手架工具中经常会看到控制台输入参数等操作。例如Vue-cli中初始化项目会提示选择一些参数等。所以在开发脚手架工具或者node控制台程序&#xff0c;就需要用到inquirer工具或者类似的工具。但是使用过inquirer工具的同学应该知道&#xff0c;目前…

SpringSecurity整合SSM和SpringBoot完成方法级权限控制

初识权限管理 权限管理&#xff0c;一般指根据系统设置的安全规则或者安全策略&#xff0c;用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面&#xff0c;前提是需要有用户和密码认证的系统。 在权限管理的概念中&#xff0c;有两个非常重要的名词&…

SpringBoot SpringBoot 开发实用篇 1 热部署 1.4 关闭热部署功能

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇1 热部署1.4 关闭热部署功能1.4.1 关闭热部署1.4.2 小结1.4.3 总结1 …

大佬神作!Spring Security从应用到源码,这份手册都讲全了,已三刷

简介 平时我们写 Web 项目&#xff0c;都需要用户登录时验证&#xff0c;以及权限管理之类的操作&#xff0c;以前使用过滤器&#xff0c;拦截器等进行管理&#xff0c;原生代码较多。 所以出现了安全框架以供我们使用&#xff0c;安全框架在 Web 应用的主要功能是&#xff1a…

金九失足,10月喜提“Java高分指南”,11月冲击大厂

蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#xff0c;…

VS2017安装教程(详细版)

1.首先下载好安装包 百度网盘下载链接 链接&#xff1a;https://pan.baidu.com/s/1HW8hrLMazRsBkPvkDHkD1Q?pwdz4jg 提取码&#xff1a;z4jg 2.下载到桌面以管理员身份运行 点击继续 3.进入后更改安装位置&#xff0c;选择安装路径&#xff0c;千万不要安装到C盘 4.改完后…

Opencv——直方图、掩膜、直方图均衡化详细介绍及代码实现

一、图像直方图 1.1 定义&#xff1a; 图像直方图是图像的基本属性之一&#xff0c;也是反映图像像素数据分布的统计学特征&#xff0c;其横坐标代表了图像像素点在[0,255]范围中&#xff0c;纵坐标代表图像像素点出现的个数或百分比。如图&#xff1a; 1.2 函数&#xff1a;…

Go程序内存泄露问题快速定位

前几天有同学反馈了 cgo 内存泄露问题&#xff0c;自己也针对这个问题探索了一番&#xff0c;算是为以后解决类似问题提前攒点经验吧。也趁机整理了一下 go 开发过程中内存泄露问题的一些常用排查方法&#xff0c;也希望对新接触 go 的同学有所帮助。整理之余&#xff0c;bcc 工…

面试问题:MD5属于哪种加密算法?千万别踩这些坑

一些小伙伴吐槽&#xff0c;去面试的时候经常听到面试官问这样的问题&#xff0c;“对称加密”、“非对称加密”、“MD5加密”&#xff0c;那么MD5到底属于哪种加密算法&#xff1f;遇到这样的问题&#xff0c;一定要小心&#xff0c;这是面试官给你挖的坑呢&#xff0c;可别傻…

C++——pair用法总结

C——pair用法总结1.pair概述&#xff08;在标头 <utility> 定义&#xff09;2.pair使用2.1成员函数&#xff08;构造函数、赋值函数&#xff09;2.2非成员函数2.3辅助类使用1.pair概述&#xff08;在标头 定义&#xff09; std::pair 是类模板&#xff0c;提供在一个单…

Kotlin MVVM之Jetpack系列ViewModel、LiveData的简单使用

一、MVVM是什么&#xff1f; MVVM分为Model&#xff0c;View&#xff0c;ViewModel 三个部分 Model:数据层&#xff0c;包含数据实体和对数据实体的操作 View:UI层&#xff0c;对应于Activity&#xff0c;XML&#xff0c;负责数据显示以及用户交互。 ViewModel&#xff1a;…

22-Redux-1

//npm init //npm install redux //1 导入redux&#xff08;不能通过es6的方式&#xff09; // commonjs一种 -> node.jsconst redux require(redux)const initialState {counter: 0 } // reducer function reducer(state initialState, action) {switch(action.type) {c…

5 步!用阿里云 Serverless 搭建高质量的图片压缩工具

作者&#xff1a;Regan Yue 本文选自“Serverless 函数计算征集令”活动 什么是 Serverless Serverless 是一种基于云计算的开发方法&#xff0c;它让开发人员可以专注于编写代码来解决业务问题&#xff0c;而不是处理服务器问题。它是独一无二的&#xff0c;因为它支持 Auto …

Rust中级教程:指针生态(引用、原始指针、智能指针)and内存操作(Stack、Heap)

指针的一些概览知识点 1.内存地址&#xff1a;指代内存中单个字节的一个整数。 指针&#xff08;原始指针&#xff09;&#xff1a;就是指向某种类型的一个内存地址。 引用&#xff1a;就是指针&#xff0c;是rust提供的一种抽象&#xff0c;如果是动态大小&#xff0c;就是一…

标记肽MGP-7-氨基-4-甲基香豆素、1926163-53-2、Met-Gly-Pro-AMC

蛋氨酸氨基肽酶1D和2的荧光底物。编号: 152397 中文名称: 标记肽MGP-7-氨基-4-甲基香豆素 英文名: H-Met-Gly-Pro-AMC CAS号: 1926163-53-2 单字母: H2N-MGP-AMC 三字母: H2N-Met-Gly-Pro-AMC 氨基酸个数: 3 分子式: C22H28N4O5S1 平均分子量: 460.55 精确分子量: 460.18 等电点…

【SVN】SVN服务端地址变动,idea切换SVN地址

公司切换了SVN服务端的制度&#xff0c;需要本地对应切换SVN地址&#xff0c;以下为具体步骤 错误方式 直接 项目上右键 --> Subversion --> Relocate &#xff0c;修改 To URL 的值&#xff0c;会报错 https://XXXXX is not the root of the repository 的错误 正确的…

Communication-Efficient Learning of Deep Networks from Decentralized Data

international conference on artificial intelligence and statistics Summary 当前机器学习模型训练中存在着数据隐私保护问题&#xff0c;所以作者提出了FL概念。通过分布式隐私保护进行训练模型。对不平衡、non-IID的数据也更合适。 主要提出了FedSGD和FedAvg算法。FedAv…

Andriod开发R文件爆红相关解决方法及排查方案

1.首先尝试下基本的处理方法&#xff1a; 在IDE中工具栏处选择build 尝试clean project&#xff0c;然后再进行rebuild project或者是make project 若使用的是IDEA或Android studio&#xff0c;在上述方法尝试后可以尝试工具栏中File->Setting->Invalidate Caches/Rest…

微信小程序(基础语法)

文章目录基本组件视图容器viewscroll-viewswiper和swiper-item基础内容组件textrich-text其他常用组件buttonimagenavigator基本模板数据绑定插值表达式事件绑定常用事件在事件处理函数中为data的数据赋值事件传参bindinput的使用小程序中的v-model条件渲染wx:ifhidden列表渲染…