【Vue.js设计与实现】第4章 响应系统的作用与实现

news2024/11/15 3:52:58

前言:

        本文是我看的Vue.js设计与实现这本书第二篇 响应系统 的第4章 响应系统的作用与实现的一些总结与收获。

        第4章从宏观视角讲述了Vue.js 3.0中响应系统的实现机制。从副作用函数开始,逐步实现一个完善的响应系统,还讲述了计算属性和watch的实现原理,同时讨论了在实现响应系统的过程中所遇到的问题,以及响应的解决方案。

第4章 响应系统的作用与实现

        响应系统也是Vue.js的重要组成部分。这章是先从什么是响应式数据和副作用函数开始讨论,然后尝试实现一个相对完整的响应系统。在过程中会遇到各种问题,像如何避免无限递归?为什么需要嵌套的副作用函数?两个副作用函数之间会产生哪些影响以及很多细节。

4.1 响应式数据与副作用函数

        副作用函数是指会产生副作用的函数,如effect(),就是说:effect函数的执行会直接或间接影响其他函数的执行,就说effect函数产生了副作用。

        响应式数据则是说,假设在一个副作用函数中读取到了某个对象的属性,当值变化后,副作用函数自动重新执行,如果能实现这个目标,那么这个对象obj就是响应式数据。

4.2 响应式数据的实现

        那如何让数据变成响应式数据? =》 拦截一个对象的读取和设置操作 =》1.ES2015通过Obhect.defineProperty函数实现(Vue.js 2) 2.ES2015+使用代理对象Proxy来实现(Vue.js 3)

         创建一个存储副作用函数的Set类型的桶,然后设置get和set拦截函数,用于拦截读取和设置操作。当读取属性时将副作用函数effect添加到桶里,即bucket.add(effect),然后返回属性值;当设置属性值时先更新原始数据,再将副作用函数从桶里取出并重新执行。

4.3 设计一个完善的响应系统

        上面的实现只能算微型响应系统,因为非常不完善。上节可知一个响应系统的工作流程:当读取操作发生时,将副作用函数收集到“桶”中;当设置操作发生时,从“桶”中去除副作用函数并执行。

        上面的桶结构没有在副作用函数与被操作的目标字段之间建立明确的联系,无脑从桶中放进取出,所以我们重新设计“桶”的数据结构,不能简单使用Set类型的数据作为“桶”,而是:

01 const obj = new Proxy(data, {
02   // 拦截读取操作
03   get(target, key) {
04     // 没有 activeEffect,直接 return
05     if (!activeEffect) return target[key]
06     // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
07     let depsMap = bucket.get(target)
08     // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
09     if (!depsMap) {
10       bucket.set(target, (depsMap = new Map()))
11     }
12     // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
13     // 里面存储着所有与当前 key 相关联的副作用函数:effects
14     let deps = depsMap.get(key)
15     // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
16     if (!deps) {
17       depsMap.set(key, (deps = new Set()))
18     }
19     // 最后将当前激活的副作用函数添加到“桶”里
20     deps.add(activeEffect)
21
22     // 返回属性值
23     return target[key]
24   },
25   // 拦截设置操作
26   set(target, key, newVal) {
27     // 设置属性值
28     target[key] = newVal
29     // 根据 target 从桶中取得 depsMap,它是 key --> effects
30     const depsMap = bucket.get(target)
31     if (!depsMap) return
32     // 根据 key 取得所有副作用函数 effects
33     const effects = depsMap.get(key)
34     // 执行副作用函数
35     effects && effects.forEach(fn => fn())
36   }
37 })

 4.4 分支切换与cleanup

冗余副作用函数问题

        响应系统的完善,分支切换会导致冗余副作用的问题,这个问题会导致副作用函数进行不必要的更新。比如副作用函数中内部存在一个三元表达式,两个分支分别读取了同一个对象的不同属性,而执行的时候只能用上其中一个,我们需要的是指在执行时用上的那个属性进行副作用函数依赖绑定,也就是修改了那个属性才会触发副作用函数执行而另一个属性修改了不执行,但是我们的代码不管是哪个属性修改了,副作用函数都会执行。
        解决方法是在每次副作用函数重新执行之前,清除上一次建立的响应联系,而当副作用函数重新执行后,会再次建立新的响应练习,新的响应联系中不存在冗余副作用问题。

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
  }

至此,我们的响应系统已经可以避免副作用函数产生遗留了。

无限循环问题

        但此时又有一个问题:目前的实现会导致无限循环执行

01 const set = new Set([1])
02
03 set.forEach(item => {
04   set.delete(1)
05   set.add(1)
06   console.log('遍历中')
07 })

        如果我们在浏览器中执行这段代码,就会发现它会无限执行下去。        

        语言规范中对此有明确的说明:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问。因此,上面的代码会无限执行。问题出在 trigger 函数中

01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05   effects && effects.forEach(fn => fn()) // 问题出在这句代码
06 }

        在 trigger 函数内部,我们遍历 effects 集合,它是一个 Set 集合,里面存储着副作用函数。当副作用函数执行时,会调用 cleanup 进行清除,实际上就是从effects 集合中将当前执行的副作用函数剔除,但是副作用函数的执行会导致其重新被收集到集合中,而此时对于 effects 集合的遍历仍在进行。

解决办法:

        建立一个新的Set数据结构进行遍历。

01 const set = new Set([1])
02
03 const newSet = new Set(set)
04 newSet.forEach(item => {
05   set.delete(1)
06   set.add(1)
07   console.log('遍历中')
08 })

这样就不会无限执行了。回到 trigger 函数,我们需要同样的手段来避免无限执行:

01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05
06   const effectsToRun = new Set(effects)  // 新增
07   effectsToRun.forEach(effectFn => effectFn())  // 新增
08   // effects && effects.forEach(effectFn => effectFn()) // 删除
09 }

4.5 嵌套的effect与effect栈

        因为effect是可嵌套的,而activeEffects所存储的副作用函数只能有一个,而此时当副作用函数发生嵌套时,内层副作用函数的执行就会覆盖activeEffect的值。这时即使响应式数据在外层副作用函数读取,收集到的也会是内层副作用函数。这就是问题所在。

effect栈

        为了解决这个问题,就需要effect栈了:

        我们需要一个副作用函数栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况

01 // 用一个全局变量存储当前激活的 effect 函数
02 let activeEffect
03 // effect 栈
04 const effectStack = []  // 新增
05
06 function effect(fn) {
07   const effectFn = () => {
08     cleanup(effectFn)
09     // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
10     activeEffect = effectFn
11     // 在调用副作用函数之前将当前副作用函数压入栈中
12     effectStack.push(effectFn)  // 新增
13     fn()
14     // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
15     effectStack.pop()  // 新增
16     activeEffect = effectStack[effectStack.length - 1]  // 新增
17   }
18   // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
19   effectFn.deps = []
20   // 执行副作用函数
21   effectFn()
22 }

4.6 避免无限递归循环

01 const data = { foo: 1 }
02 const obj = new Proxy(data, { /*...*/ })
03
04 effect(() => obj.foo++)

        上面effcet的自增操作会引发栈溢出。

01 effect(() => {
02   // 语句
03   obj.foo = obj.foo + 1
04 })

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

        因为在这个语句中,既会读取 obj.foo 的值,又会设置 obj.foo 的值,而这就是导致问题的根本原因。我们可以尝试推理一下代码的执行流程:首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

无限递归循环解决办法:

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

01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05
06   const effectsToRun = new Set()
07   effects && effects.forEach(effectFn => {
08     // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
09     if (effectFn !== activeEffect) {  // 新增
10       effectsToRun.add(effectFn)
11     }
12   })
13   effectsToRun.forEach(effectFn => effectFn())
14   // effects && effects.forEach(effectFn => effectFn())
15 }

4.7 调度执行

        可调度,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。为了实现调度能力,为effect函数添加了第二个选项参数,可以通过scheduler选项指定调用器,这样用户可以通过调度其自行完成任务的调度。

        为 effect 函数设计一个选项参数 options,允许用户指定调度器

01 effect(
02   () => {
03     console.log(obj.foo)
04   },
05   // options
06   {
07     // 调度器 scheduler 是一个函数
08     scheduler(fn) {
09       // ...
10     }
11   }
12 )
01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05
06   const effectsToRun = new Set()
07   effects && effects.forEach(effectFn => {
08     if (effectFn !== activeEffect) {
09       effectsToRun.add(effectFn)
10     }
11   })
12   effectsToRun.forEach(effectFn => {
13     // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
14     if (effectFn.options.scheduler) {  // 新增
15       effectFn.options.scheduler(effectFn)  // 新增
16     } else {
17       // 否则直接执行副作用函数(之前的默认行为)
18       effectFn()  // 新增
19     }
20   })
21 }

4.8 计算属性computed与lazy

        讲解了计算属性,computed。计算属性实际上是一个懒执行的副作用函数,我们通过lazy选项使得副作用函数可以懒执行。

        

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

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

相关文章

java计算机毕业设计基于安卓Android的在线心理咨询与健康App

项目介绍 本文介绍了心理咨询与健康App软件开发建设的意义和国内外发展现状,然后详细描述了所开发手机APP的可行性分析,并分析了手机APP所要实现的功能。因为心里咨询设施较多,而且人口密集,不能更好的管理健康问题,造成需要时人员不必要的身心伤害,所以采用比较方便的、容易便…

Linux基本指令(下)

Linux基本指令&#xff08;下&#xff09;前言cat指令more命令less命令head命令tail命令wc指令date指令cal指令find指令grep指令top命令alias命令zip/unzip命令前言 上一篇Linux基本指令主要讲解了关于文件操作方面的指令&#xff0c;接下来这一片Linux基本指令主要讲解一下关…

聊聊推荐系统的评测(下)

这是鼎叔的第三十九篇原创文章。 行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。 欢迎关注本人专栏和微信公众号《敏捷测试转型》&#xff0c;大量原创思考文章陆续推出。 上篇请查阅&#xff1a;聊聊推荐系统的评测&#xff08;上&#xff09; 下篇&#xff0c;我们…

基于SSM的旅游景点购票管理系统

1、项目介绍 基于SSM的旅游景点购票管理系统拥有两种角色&#xff0c;管理员和用户 管理员&#xff1a;用户管理、景点管理、购票管理、酒店管理、客房管理、客房预订管理、轮播图管理等 用户&#xff1a;登录注册、景区购票、评论、预订客房、收藏、发布攻略等 2、项目技术…

App推广渠道追踪技术更新及应用

如今App推广渠道追踪对App厂商来说非常重要&#xff0c;因为App厂商需要通过渠道追踪来寻找成本最低的和价值最高的获客渠道。 但是现在线上渠道的选择五花八门&#xff0c;比如各种新闻门户网站、字节系平台、腾讯系平台等&#xff0c;那么到底该花多少钱去获取用户&#xff…

本地浏览器打开远程服务器上的Jupyter Notebook

文章目录一、配置过程二、其他需求后台运行Jupyter端口映射实验环境及需求&#xff1a;远程服务器配置了 Jupyter Notebook&#xff0c;本地电脑没有相关的环境&#xff0c;想要在服务器端启动 Jupyter Notebook&#xff0c;然后直接从本地浏览器打开进行操作。 一、配置过程 …

【百度AI_文字识别】示例身份证图片识别(代码官方文档完整,只需获得修改参数、下载类)

文章目录提取身份证信息第一步&#xff1a;登录第二步&#xff1a;获取资源第三步&#xff1a;获取access_token参数&#xff08;AuthService.java&#xff09;第四步&#xff1a;请求代码Idcard.java第五步&#xff1a;修改Idcard.java文件注意&#xff1a;返回错误提取身份证…

【跨境电商卖家】Instagram营销初学者指南(一):重要性、优势

关键词&#xff1a;跨境电商卖家、instagram营销 1.为什么 Instagram 营销对企业很重要&#xff1f; Instagram 是接触大量受众的完美渠道——每月有超过10 亿活跃用户。平均而言&#xff0c;用户每天在 Instagram 上花费 53 分钟&#xff0c;这使得该平台成为仅次于Facebook的…

微信网页支付小白指南-域内浏览器支付 + 外部浏览器支付

关于微信网页支付&#xff0c;分为微信域内浏览器支付 外部浏览器支付&#xff0c;两者还是稍微有点点区别的&#xff0c;内部浏览器即在微信内打开网页&#xff0c;进行支付&#xff0c;支付调用是需要开通JSAPI支付方式&#xff1b;而外部浏览器「比如浏览器等」则需要开通 …

Leetcode刷题day2|数组二|977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II

文章目录一、有序数组的平方错误的尝试思路注意AC代码暴力版本双指针方法二、长度最小的子数组错误的尝试思路滑动窗口介绍注意AC代码三、螺旋矩阵错误的尝试思路注意AC代码继承前边循环变量的写法不继承前边循环变量的做法四、数组做题思路总结基本知识解题思路一、有序数组的…

将爱心代码设为电脑屏保,俘获少女芳心,还能假装黑客,在酷炫的界面中保护隐私

本文介绍 Hacker Screen Saver 一款开源 Windows 屏保的使用。Hacker Screen Saver 是一款 .NET 设计的屏幕保护程序&#xff0c;可以显示 HTML 页面&#xff0c;你可以将黑客模拟器的网页&#xff0c;或者爱心代码网页设置为你的 Windows 电脑屏保。详细介绍了软件的使用和对应…

apritag 定位记录 C++ opencv 3.4.5

参考&#xff1a;2021-06-23 基于AprilTag的位姿估计&#xff0c;原理&#xff0c;完整代码&#xff08;相机坐标系、世界坐标系&#xff09; - 简书 Apriltag使用之二&#xff1a;方位估计(定位)_arczee的博客-CSDN博客_apriltag位姿估计 1.AprilTag概述 AprilTag是一种视觉…

Matlab:绘制日期时间

Matlab&#xff1a;绘制日期时间绘制日期时间数据指定坐标区范围指定刻度值指定刻度格式存储日期时间的坐标区属性导出和转换数据提示值绘制来自文件的日期时间数据此示例说明如何使用存储为 datetime 和 duration 数组的日期时间创建线图。datetime 数据类型表示时间点&#x…

Linux I/O 原理和 Zero-copy 技术全面分析

两万字长文从虚拟内存、I/O 缓冲区&#xff0c;用户态&内核态以及 I/O 模式等等知识点全面而又详尽地剖析 Linux 系统的 I/O 底层原理&#xff0c;分析了 Linux 传统的 I/O 模式的弊端&#xff0c;进而引入 Linux Zero-copy 零拷贝技术的介绍和原理解析&#xff0c;将零拷贝…

项目终于收尾了,第一次体验到专业项目管理软件的魅力

转眼到了年底&#xff0c;我跟进的项目也到了收尾阶段。之前陆陆续续给大家分享了入职新公司后&#xff0c;使用新引进的项目管理软件做项目的一些体会和心得&#xff0c;其中一些比较高效便捷的技巧和功能模块也引起了大家的兴趣。 最近刚好临近项目尾声&#xff0c;也给大家…

Maven的详解

在java中Maven就是一个包管理工具,在没有包管理工具时,我们要做一个java项目,需要第三方依赖包,将别人打包好的Jar包下载到本地,然后手动指定给项目.操作比较麻烦,比如版本控制,有的甚至还有其他包的依赖,属实是繁琐,技术是不断地迭代的,所以就出现了Maven,用了Maven之后,需要什…

安装nodejs的详细流程保姆级(踩了无数次坑)

node 简述: node的使用已经是前端选手基本的选择,其强大的功能甚至到了要和后端抢活干的地步,同时想要搭建个人的博客用node工具也是非常方便的,作为一名后端选上,刚开始准备下载node的时候是因为想要去搭建个人的博客,但是下载之后,使用npm install命令的时候一直报错,无奈找…

G1D14fraudgitpipenvdf操作APT论文RCE37-40服务器搭建

一、fraud 突然发现电脑上还没有python编译器&#xff0c;xswl&#xff0c;快装一下 &#xff08;一&#xff09;git操作 &#xff08;二&#xff09;git中分支的作用 &#xff08;三&#xff09;虚拟环境 1、pip install后的包一般放在哪里 lib/site-packages下 真的是欸&a…

LiveData

LiveData是一个抽象类&#xff0c;那么我们从简单的MutableLiiveData开始剖析&#xff0c;先看源码 源码太简洁了&#xff0c;就是继承LiveData&#xff0c;然后重写了父类的方法&#xff0c;并且没有多余的操作&#xff0c;都是直接使用父类方法里的逻辑&#xff0c;那我们就根…

安全防护的原则

电力行业 工控安全解决思路保障框架从电力行业对工控安全需求看&#xff0c;电力企业在主要是以合规性建设为主&#xff0c;在 2004 年原电监会 5 号令颁布开始&#xff0c;大部 分的电厂控制系统安全 建设已经按照 5 号令的要求进行了整改&#xff0c;形成“安全分区、网络专…