Vue3响应式系统(一)

一、副作用函数。

        副作用函数指的是会产生副作用的函数。例如:effect函数会直接或间接影响其他函数的执行,这时我们便说effect函数产生了副作用。

function effect(){
    document.body.innerText = 'hello vue3'
}

        再例如:

//全局变量

let val = 2

function effect() {
    val = 2 //修改全局变量,产生副作用
}

二、响应式数据。

        上代码:

const obj = { text: 'hello world' }

function effect() {
    // effect 函数的执行会读取 obj.text
    document.body.innerText = obj.text
}

        当前修改obj.text的值的时候,除了本身的发生变化之外,不会有任何其他反应。

        若修改obj.text的值的时候,effect函数副作用函数自动重新执行,如果能实现这个目标,那么obj对象就是响应式数据。很显然,目前还不能实现,接下来我们将数据变成响应式数据。

三、响应式数据的基本实现。

        接上思考:如何将数据变为响应式数据呢?

                通过上面我们可以发现有两点:

                        1.当effect副作用函数执行时,触发obj.text的读取操作。

                        2.当修改obj.text的值的时候,出发obj.text的设置操作。

        问题的关键:我们如何才能拦截一个对象属性的读取和设置操作。在ES2015之前只能通过Object.defineProperty函数实现,这也是Vue.js 2所采用的方式。在ES2015+中,我们可以使用Proxy代理对象来实现,这也是Vue.js 3所采用的方式。

        采用Proxy来实现:

/**
 * 实现一个响应式 
 * @param { Object } bucket
 * @param { Object } data
 * @param { Function } effect
 * @param { Object } obj 
 */
// 存储副作用函数的桶
const bucket = new Set()

// 副作用函数
function effect() {
  console.log(obj.text)
}

//原始数据
const data = { text: 'hello world' }
//对数据的代理
const obj = new Proxy(data, {
  //拦截读取操作
  get(target, key) {
    //将副作用函数加入到桶里
    bucket.add(effect)
    //返回属性值
    return target[key]
  },
  //拦截设置操作
  set(target, key, newVal) {
    //设置属性值
    target[key] = newVal
    //把副作用函数从桶里取出来并执行
    bucket.forEach(fun => fun())
    //返回 true 代表设置操作成功
    return true
  }
})
effect()

setTimeout(() => {
  console.log('一秒后触发设置')
  obj.text = 'hello vue3'
},1000)

 

        目前还存在许多缺陷,我们需要去掉通过名字来获取副作用函数的硬编码机制 。

四、实现一个完善的响应式系统。

 1.解决副作用函数收集到桶里的硬编码机制——我们需要注册一个副作用函数的机制

/**
 * 注册副作用函数机制
 * @param {any} activeEffect
 * @param {Function} effect
 * @param {Object} obj1
 * @param {Object} data 用的是上面的
 */
//用全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
  // 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
const obj1 = new Proxy(data, {
  //拦截读取操作
  get(target, key) {
    //--将副作用函数加入到桶里
    //--bucket.add(effect)

    //++将activeEffect中存储的副作用函数收集到“桶”中
    if (activeEffect) {
      bucket.add(activeEffect)
    }

    //返回属性值
    return target[key]
  },
  //拦截设置操作
  set(target, key, newVal) {
    //设置属性值
    target[key] = newVal
    //把副作用函数从桶里取出来并执行
    bucket.forEach(fun => fun())
    //返回 true 代表设置操作成功
    return true
  }
})
effect(
  () => {
    console.log('run', obj1.text)
  }
)
setTimeout(() => {
  console.log('一秒后触发设置')
  obj1.notExist = 'hello vue3'
},1000)

         存在问题: 

                没有在副作用函数与被操作的目标字段之间建立明确的联系。无论读取的是哪一个属性,都会把副作用函数收集到“桶”里。

2.解决上述问题。

        首先分析一下注册副作用函数触发都存在哪些角色:

                ①obj1对象

                ②text字段

                ③使用副作用函数注册的函数

        我们来为这三个角色建立一个树形关系。target表示obj1——代理对象所代理的原始对象;用key来表示text字段——被操作的字段名;effectFn表示被注册的副作用函数。这个联系建立起来后,就可以解决前文提到的问题了。

        我们需要重新设计“桶”的数据结构,不能简单的去使用Set类型的数据作为“桶”,我们需要将Set桶改为WeakMap桶。

        为啥要用到WeakMap,而不是Map?

        因为WeakMap对key是弱引用,只有被引用的有价值的信息可以访问,没有被引用的信息就会被垃圾回收器回收。如果是Map,即使信息没有引用,垃圾回收器也不会去回收它,那么就会有很大机率导致内存溢出。

        代码如下:

const bucketMap = new WeakMap() 
const obj2 = new Proxy(data, {
  get(target, key) {
    // 没有activeEffect直接返回
    if(!activeEffect) return target[key]
    // 取出WeakMap桶里的值 target ===> key
    let depsMap = bucketMap.get(target)
    // 如果不存在depsMap,那就新建Map与target建立联系
    if(!depsMap) {
      bucketMap.set(target, (depsMap = new Map()))
    }
    // key ===> effectFn
    let deps = depsMap.get(key)
    if(!deps) {
      depsMap.set(key, deps = new Set())
    }
    // 注册副作用函数
    deps.add(activeEffect)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    // 取target
    const depsMap = bucketMap.get(target)
    if(!depsMap) return
    // 根据key取副作用函数
    const effects = depsMap.get(key)
    // 执行副作用函数
    effect && effect.forEach(fn => fn())
    return true
  }
})

        我们可以将activeEffect注册副作用函数机制单独封装到一个函数track中,表达追踪的含义。将触发副作用函数单独封装到trigger函数中。代码更改如下:

/**
 * 建立联系
 * @param { Object } bucketMap
 * @param { Object } obj2
 * @param { Function } track 追踪
 * @param { Function } trigger 触发
 */
// WeakMap桶
const bucketMap = new WeakMap()
function track(target, key) {
  // 没有activeEffect直接返回
  if (!activeEffect) return target[key]
  // 取出WeakMap桶里的值 target ===> key
  let depsMap = bucketMap.get(target)
  // 如果不存在depsMap,那就新建Map与target建立联系
  if (!depsMap) {
    bucketMap.set(target, (depsMap = new Map()))
  }
  // key ===> effectFn
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, deps = new Set())
  }
  // 注册副作用函数
  deps.add(activeEffect)
}
function trigger(target, key) {
  // 取target
  const depsMap = bucketMap.get(target)
  if (!depsMap) return
  // 根据key取副作用函数
  const effects = depsMap.get(key)
  // 执行副作用函数
  effect && effect.forEach(fn => fn())
}
const obj2 = new Proxy(data, {
  get(target, key) {
    // 注册副作用函数
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    // 触发副作用函数
    trigger(target, key)
    return true
  }
})

五、分支切换与cleanup

        我们用以下代码说明分支切换。如下:

const data = { ok: true, text: 'hello world'}
const obj = new Proxy(/*....*/)
effect(function effectFn{
    document.body.innerText = obj.ok ? obj.text : 'not'
})

         在effectFn函数内部的三元表达式,根据ok字段值的不同会执行不同的代码分支。ok的值发生变化时,代码执行的分支会根治变化,这就是所谓的分支切换。

          分支切换可能会产生一流的副作用函数。根据上面的代码案例来说,effectFn与响应式数据建立的关系如下:

副作用函数与响应式数据之间的联系

        当修改ok字段值改为false的时候,text的不会被读取,所以指挥触发ok字段的读取,而不会触发text读取,所以理想状态下effectFn不应该被字段text所对应的依赖集合收集。

        显然我们目前还不能做到这一点。 

理想状态

         遗留的副作用函数会导致不必要的更新。解决问题的思路就是:每次副作用函数执行时,我们可以先把它从所有与之关联的依赖几何中删除。当副作用函数执行完毕后,会重新建立联系,新的联系里不会包含遗留的副作用函数。

        重新设计effectFn函数 

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn
    fn()
  }
  // deps用来存储所有与这副作用函数相关联的依赖集合
  effectFn.deps = []
  effectFn()
}

        修改 track 追踪函数 

function track(target, key) {
  if (!activeEffect) return target[key]
  let depsMap = bucketMap.get(target)
  if (!depsMap) {
    bucketMap.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, deps = new Set())
  }
  deps.add(activeEffect)
  // ======= 主要就是增加关联数组中 ===========
  activeEffect.deps.push(deps)
}
对依赖集合收集

        有了这个联系后,我们就可以在每次副作用函数执行时,根据deps获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除。

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }
  // deps用来存储所有与这副作用函数相关联的依赖集合
  effectFn.deps = []
  effectFn()
}
function cleanup(effectFn) {
  //遍历effectFn的deps数组
  for(let i = 0; i < effectFn.deps.length; i++) {
    let deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  // 最后需要重置effectFn.deps数组
  effectFn.deps.length = 0
}

        现在我们来执行一下这完整代码,看会有啥效果:

const data1 = {
  ok: true,
  text: 'hello world'
}
const obj2 = new Proxy(data1, {
  get(target, key) {
    // 注册副作用函数
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    // 触发副作用函数
    trigger(target, key)
    return true
  }
})

function trigger(target, key) {
  // 取target
  const depsMap = bucketMap.get(target)
  if (!depsMap) return
  // 根据key取副作用函数
  const effects = depsMap.get(key)
  // 执行副作用函数
  effects && effects.forEach(fn => fn())
}

/**
 * 重新设计effectFn
 * @param { Function } effect
 * @param { Function } cleanup
 * @param { Function } track
*/
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }
  // deps用来存储所有与这副作用函数相关联的依赖集合
  effectFn.deps = []
  effectFn()
}
function cleanup(effectFn) {
  //遍历effectFn的deps数组
  for(let i = 0; i < effectFn.deps.length; i++) {
    let deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  // 最后需要重置effectFn.deps数组
  effectFn.deps.length = 0
}

function track(target, key) {
  // 没有activeEffect直接返回
  if (!activeEffect) return target[key]
  // 取出WeakMap桶里的值 target ===> key
  let depsMap = bucketMap.get(target)
  // 如果不存在depsMap,那就新建Map与target建立联系
  if (!depsMap) {
    bucketMap.set(target, (depsMap = new Map()))
  }
  // key ===> effectFn
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, deps = new Set())
  }
  // 注册副作用函数
  deps.add(activeEffect)
  // ======= 主要就是增加关联数组中 ===========
  activeEffect.deps.push(deps)
}


// 测试
effect(
  () => {
    let n = obj2.ok ? obj2.text : 'not' 
    console.log('run', n)
  }
)
setTimeout(() => {
  console.log('一秒后触发设置')
  obj2.ok = false
},1000)

        可以看到目前会无限不断去执行, 问题出现在哪里呀?问题便出现在trigger函数下面这句中。

effects && effects.forEach(fn => fn())

       Why?有啥问题?来看下面代码:

const set = new Set([1])

set.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log('遍历中!!!')
})

· 

        由于不断执行,我截不下全图。不断执行的原因:语言规范中说过,在调用forEach遍历Set集合时,一个值被访问过了,但被删除后又被重新添加到集合,如果此时forEach遍历没有结束,那么该值会重新被访问。所以,上面代码会不断去执行 。

        同理,trigger函数里面的effects也是一样,当副作用函数执行的时候,cleanup会进行清除,但是副作用函数的执行会导致其被重新收集到集合中,而此时遍历仍然在进行,所以我们实现的响应式才会不断的去执行。

      如何更改无限循环呢

        我们可以构造另一个Set集合并遍历它。我们去修改一下trigger触发函数:

function trigger(target, key) {
  // 取target
  const depsMap = bucketMap.get(target)
  if (!depsMap) return
  // 根据key取副作用函数
  const effects = depsMap.get(key)
  // 执行副作用函数
  const effectToRun = new Set(effects) //新增
  effectToRun && effectToRun.forEach(fn => fn()) //新增
  // effects && effects.forEach(fn => fn())  //剔除 
}

 

        如上图所示,无限循环问题得以解决。

Vue响应式系统(二)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MATLAB】Linux版本 高分辨率屏 调整显示缩放

0 引言 安装了linux版本的MATLAB R2023b之后&#xff0c;发现工具栏字体很小不方便使用&#xff0c;所以上网找到了MATLAB论坛上某位大佬的教程&#xff1a;参考链接&#xff0c;放在这里供各位参考 。 1 环境 这里注明我的matlab安装环境仅供参考&#xff0c;未在其他环境下…

Java基础 - 黑马

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 知…

msvcp140.dll丢失都有什么办法可以解决呢?分享几种解决办法

msvcp140.dll是Windows操作系统中的一个重要动态链接库文件&#xff0c;它与许多软件的正常运行密切相关。当系统或软件无法找到或访问到该dll文件时&#xff0c;就会出现msvcp140.dll丢失的问题。这可能导致某些软件无法启动或崩溃&#xff0c;给用户带来不便。为了解决这个问…

Proxy的使用方法和13种拦截操作

前言 proxy是ES6新推出的方法,功能很强大。属于元编程,也就是修改js本身的一些东西。可以对数组,对象,函数等引用类型的对象进行一些复杂的操作。 其中,大部分人应该最熟悉的莫过于vue3中使用proxy替换了defineProperty,而且还实现了本身defineProperty不能实现的一些东西。 …

GO——gin中间件和路由

中间件 参考&#xff1a;https://learnku.com/articles/66234 结构 中间件是函数中间件函数被放在调用链上调用链的末尾是路由path对应的函数 执行过程 net/http包调用到gin的serverHTTP 参考&#xff1a;go/pkg/mod/github.com/gin-gonic/ginv1.7.7/gin.go:506 通过path找到…

FlinkSQL【分组聚合-多维分析-性能调优】应用实例分析

FlinkSQL处理如下实时数据需求&#xff1a; 实时聚合不同 类型/账号/发布时间 的各个指标数据&#xff0c;比如&#xff1a;初始化/初始化后删除/初始化后取消/推送/成功/失败 的指标数据。要求实时产出指标数据&#xff0c;数据源是mysql cdc binlog数据。 代码实例 --SET t…

【.net core】yisha框架,bootstrap-table组件增加固定列功能

需要引入 bootstrap-table-fixed-columns.css和bootstrap-table-fixed-columns.js文件 文件代码&#xff1a; bootstrap-table-fixed-columns.css样式文件代码 .fixed-table-header-columns, .fixed-table-body-columns {position: absolute;background-color: #fff;displa…

【Vue3】3-2 : 组件的概念及组件的基本使用方式

本书目录&#xff1a;点击进入 一、组件的概念 1.1、【案例】评分组件与按钮组件的抽离过程 二、组件的使用 - 抽离结构 2.1、【案例】简易首页 &#xff1e; 效果 &#xff1e; 代码 - 原始 &#xff1e; ​​​​​​​代码 - 组件抽离结构 &#xff1e; ​​​​…

【汇编】实验11 编写子程序

综合一下学过的指令就行了&#xff0c;比较简单。 assume cs:code data segmentdb "Beginners All-purpose Symbolic Instruction Code.",0 data ends code segment begin:mov ax,datamov ds,axmov si,0call lettercmov ax,4c00hint 21h letterc:mov cl,[si]mov ch,…

【线路图】世微AP5160宽电压降压型恒流芯片 LED电源 带调光SOT23-6

这是一款14-18V 3A 电流的PCB设计方案. 运用的是世微AP5160 电源驱动IC,这是一款效率高&#xff0c;稳定可靠的 LED 灯恒流驱动控制芯片&#xff0c;内置高精度比较器&#xff0c;固定 关断时间控制电路&#xff0c;恒流驱动电路等&#xff0c;特别适合大功率 LED 恒流驱动。 …

Wpf 使用 Prism 实战开发Day12

待办事项接口增删&#xff08;CURD&#xff09;改查实现 一.添加待办事项控制器&#xff08;ToDoController&#xff09; 控制器类需要继承 ControllerBase 基类需要添加 [ApiController] 特性以及 [Route] 特性Route&#xff08;路由&#xff09; 特性参数规则&#xff0c;一般…

pycharm debug显示的变量过多

问题&#xff1a; https://blog.csdn.net/Hodors/article/details/117535731 解决方法&#xff1a; 把"Show console variables by default"前面的勾取消掉就行 参考&#xff1a; https://stackoverflow.com/questions/48969556/hide-console-variables-in-pychar…

【Leetcode 程序员面试金典 02.08】 —— 环路检测 |双指针

面试题02.08. 环路检测 给定一个链表&#xff0c;如果它是有环链表&#xff0c;实现一个算法返回环路的开头节点。若环不存在&#xff0c;请返回null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪next指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的…

abap 将xstring转换成PDF展示

收到外围系统的xstring之后&#xff0c;如何在sap中将其打开呢 1.创建一个屏幕 2.绘制一个customer control 3.创建流逻辑 4.流逻辑如下&#xff1a; DATA: go_html_container TYPE REF TO cl_gui_custom_container, go_html_control TYPE REF TO cl_gui_html_viewer, lv_u…

基于SSM的社区老年人关怀服务系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

OpenCV-Python(42):摄像机标定

目标 学习摄像机畸变以及摄像机的内部参数和外部参数根据摄像机相关参数对畸变图像进行修复 基础说明 今天的低价单孔摄像机(照相机)会给图像带来很多畸变。畸变主要有两种:径向畸变和切向畸变。如下图所示用红色直线将棋盘的两个边标注出来&#xff0c;但是你会发现棋盘的边…

C++力扣题目40--组合总和II

力扣题目链接(opens new window) 给定一个数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 说明&#xff1a; 所有数字&#xff08;包括目标数&#xff09;都是…

Linux Mii management/mdio子系统分析之五 PHY状态机分析及其与net_device的关联

&#xff08;转载&#xff09;原文链接&#xff1a;https://blog.csdn.net/u014044624/article/details/123303714 前面几章基本上完成了mdio模块驱动模型的分析&#xff0c;本篇文章主要讲述phy device的状态机以及phy device与net_device的关联。Phy device主要是对phy的抽象…

[LitCTF 2023]easy_shark

解压缩&#xff0c;发现需要输入密码&#xff0c;使用010打开&#xff0c;发现frflags和deflags都被修改了&#xff0c;这就会造成压缩包伪加密 把他们都改为0&#xff0c;再打开 将流量包使用wirshark打开 过滤http&#xff0c;并追踪 得到以下信息 看到了一个类似于flag格…