首页 > 其他分享 >02_响应式数据的基本实现

02_响应式数据的基本实现

时间:2024-10-17 18:18:10浏览次数:3  
标签:02 基本 obj name 函数 effect 响应 key target

思考

经过上一章节的论述,我们留下了一个疑问,如何将 obj 变为响应式的数据?并通过和 effect 函数搭配实现了一个为代码的例子,从中得到两点线索:

  • 执行 effect 函数时,会获取字段 obj.name 的值,此操作即为
  • 当修改 obj.name 的值时,就会重新设置字段 obj.name 的值,此操作即为

有了这两点之后,我们要做的事情就很明显了,就是进行的时候进行拦截,插入我们需要执行的代码逻辑,比如读的时候就可以把这个 effect 函数收集起来,放到一个数组里面,当进行写的时候,就从这个数组里面取出 effect 函数,在重新执行。

现在问题就又升级了,我如何才能拦截到这个读、写的操作呢?在 ES6 之前,使用的是 Object.defineProperty 来进行实现,而在 ES6 中可以使用代理实现,这两种方法的优缺点,这里不做探讨。这里我们采用 Vue3 的选择,即 proxy。

实现

proxy 初体验

当采用了 proxy 之后,我们就可以对数据的读、写进行拦截,代码如下:

// 存储函数
const list = []

// 原始数据
const obj = {
	name: '张三'
}

const objProxy = new Proxy(obj, {
	// 拦截读取操作
	get(target, key) {
		console.log('get', target, key)
		return target[key]
	},
	// 拦截写入操作
	set(target, key, newVal) {
		console.log('set', target, key, newVal)
		target[key] = newVal
		// set 方法返回 true 表示成功,false 表示失败,失败时,当前操作无效
		return true
	}
})

// 测试
objProxy.name
objProxy.name = '李四'

在预期中,会先触发一次 get 在触发一次 set,结果如图:
在这里插入图片描述

结果也正如我们所期望的那样,既然实现了拦截,那么此时就可以植入我们需要的代码逻辑,在 get 时收集执行的函数,set 时在触发,此时我们会发现一个问题,我 get 触发的时候,怎么才能得到当前正在执行的函数呢,如果不能得到这个函数,我如何进行存储呢?

proxy + 副作用函数

如果需要得到当前正在执行的函数,我们可以定义一个全局变量,执行 get 的时候,从这个全局变量上获取正在执行的函数,而如果想要给这个全局变量赋值,我们就需要有一个函数(记作 fn1)可以帮助我们完成这一件事,即将需要和响应式数据关联的函数(记作 fn2),将 fn2 作为参数传递给 fn1,fn1 内部执行一次 fn2,并将 fn2 赋值给全局变量,fn2 被执行时会触发读取一个响应式对象属性的代码(比如 obj.name),obj.name 机会触发 get,在 get 中收集 fn2,代码如下:

const list = []

const obj = {
	name: '张三'
}

// 全局变量
let activeFn = null

const objProxy = new Proxy(obj, {
	get(target, key) {
		activeFn && list.push(activeFn)
		return target[key]
	},
	set(target, key, newVal) {
		target[key] = newVal
		list.forEach(fn => fn())
		return true
	}
})

// 收集执行的函数
function effect(fn) {
	activeFn = fn
	fn()
}

// 要执行的函数
function foo() {
	console.log('foo函数执行:', objProxy.name)
}

effect(foo)

// 修改值
objProxy.name = '李四'

最后结果也正如我们所期望的这样,结果如图:

在这里插入图片描述

建立正确的副作用函数与目标字段之间的联系

为什么要建立正确的联系呢?我们看一下下面这段测试案例,如下:

const list = []

const obj = {
	name: '张三',
	age: 18
}

let activeFn = null

const objProxy = new Proxy(obj, {
	get(target, key) {
		activeFn && list.push(activeFn)
		return target[key]
	},
	set(target, key, newVal) {
		target[key] = newVal
		list.forEach(fn => fn())
		return true
	}
})

function effect(fn) {
	activeFn = fn
	fn()
}

function foo() {
    // 获取的是 obj.name
	console.log('foo函数执行:', objProxy.name)
}

effect(foo)

// 修改 age
objProxy.age = 20

结果如图:

在这里插入图片描述

在这个例子中,foo 函数里面读取的是 name 字段,为什么修改 age 也会导致再次执行 foo 呢,就是因为没有正确的关联,所以我们需要将 key 和这个 list 进行一个关联,objProxy 的每一个 key 都应该单独对应的 list,既然字段存在这种关系,那么想必 obj 这个对象本身也是存在的,而 foo 函数可能不止于一个。

那针对这些情况,我们应该如何设计数据结构呢?在分析中,我们有三个角色:

  • 被代理的对象 obj
  • 字段名 name
  • 使用 effect 注册的副作用函数 foo

如果 target 表示所代理的原始对象,key 表示被操作的字段名,effectFn 表示被注册的副作用函数,那么可以建议如下关系:

target
   |———— key
  		  |———— effctFn

这就是一种树形的结构,我们还可以多举几个例子来看一下:

**case1:**两个副作用函数同时读取一个对象的属性值

  • 代码如下:

    effect(function effectFn1(){
        console.log(obj.name)
    })
    effect(function effectFn2(){
        console.log(obj.name)
    })
    
  • 关系如下:

    target
      |———— name
              |———— effctFn1
      		|———— effctFn2
    

**case2:**如果同一个副作用函数中读取了同一个对象两个不同属性

  • 代码如下:

    effect(function effectFn1(){
        console.log(obj.name)
        console.log(obj.age)
    })
    
  • 关系如下:

    target
       |———— name
      		|———— effctFn1
       |———— age
      		|———— effctFn1
    

**case3:**在不同的副作用函数中读取了两个不同对象的不同属性

  • 代码如下:

    effect(function effectFn1(){
        console.log(obj1.name)
    })
    effect(function effectFn2(){
        console.log(obj2.age)
    })
    
  • 关系如下:

    obj1
       |———— name
      		  |———— effctFn1
    obj2
       |———— age
      		  |———— effctFn2
    

有了这个数据结构之后,我们就可以来构建它,如下:

const list = []

const obj = {
	name: '张三',
	age: 18
}

let activeFn = null
let targetMap = new WeakMap()

function track(target, key) {
	if (!activeFn) return
	// 从targetMap中获取target对应的依赖集合,是一个 map 结构
	let depsMap = targetMap.get(target)
	// 如果当前对象没有存储过依赖,则创建
	if (!depsMap) {
		depsMap = new Map()
		// 创建后,将依赖集合存储到 targetMap 中
		targetMap.set(target, depsMap)
	}
	// 获取当前key的依赖,没有则创建集合,是一个 set 结构
	let deps = depsMap.get(key)
	if (!deps) {
		deps = new Set()
		// 创建后,将依赖集合存储到 depsMap 中
		depsMap.set(key, deps)
	}
	// 将当前函数存储到依赖集合中
	deps.add(activeFn)
}

function trigger(target, key) {
	// 从targetMap中根据target获取对应的依赖集合,是一个 map 结构
	let depsMap = targetMap.get(target)
	if (!depsMap) return
	// 根据当前key的,获取依赖集合,是一个 set 结构
	let deps = depsMap.get(key)
	if (!deps) return
	// 遍历依赖集合,执行依赖函数
	deps.forEach(fn => fn())
}

const objProxy = new Proxy(obj, {
	get(target, key) {
		// 收集依赖
		track(target, key)
		return target[key]
	},
	set(target, key, newVal) {
		target[key] = newVal
		trigger(target, key)
		return true
	}
})

function effect(fn) {
	activeFn = fn
	fn()
}

function foo() {
	console.log('foo函数执行:', objProxy.name)
}

function foo2() {
	console.log('foo2函数执行:', objProxy.age)
}

effect(foo)
effect(foo2)

console.log('*********修改了数据*********')
objProxy.name = '李四'
objProxy.age = 20

结果如图:

在这里插入图片描述

标签:02,基本,obj,name,函数,effect,响应,key,target
From: https://blog.csdn.net/qq_53109172/article/details/142914973

相关文章

  • 『模拟赛』多校A层冲刺NOIP2024模拟赛08
    Rank还行A.传送(teleport)签。单元最短路,先想Dijkstra。发现这道题还有个不同寻常的移动方式,可以以\(min\left(|x_i-x_j|,|y_i-y_j|\right)\)的代价从\(i\)移动到\(j\)。暴力连边是\(\mathcal{O(n^2)}\)的,时间空间都过不去。被叫去整内务在楼梯上想到,一个点不应......
  • 题解:GZOI2024 D2T2 乒乓球
    考场上切了,但是比较神奇的题,应该是蓝/紫。Discription乒乓球\(\text{}\)时间限制:\(\bold{3}\)秒众所周知,一场乒乓球比赛共有两个玩家\(A\)和\(B\)参与,其中一场比赛由多局比赛组成,而每局比赛中又由多盘比赛组成。每盘比赛\(A\)或\(B\)只有一名选手获胜。当其中一名......
  • 【ACM独立出版 | EI稳检索 】第三届公共卫生与数据科学国际学术会议(ICPHDS 2024)
    随着大数据和人工智能的迅速发展,数据科学在公共卫生领域的应用越来越广泛。会议议题将涵盖公共卫生数据的收集、处理、分析和解读,以及数据科学在流行病学、健康管理、决策支持、公共卫生政策、数据科学理论、人工智能技术、健康大数据分析等方面的应用。第三届公共卫生与数据科......
  • 《柯娜:精神之桥》游戏未响应弹窗“缺少msvcp100.dll”文件的原因分析及处理教程
    当玩家们兴致勃勃地准备开启《柯娜:精神之桥》的游戏之旅时,却突然遭遇游戏未响应,弹窗提示“缺少msvcp100.dll”文件,这无疑给玩家们的热情浇上了一盆冷水。究竟是什么原因导致了这一情况的发生?又该如何去解决呢?下面将为大家详细分析原因并给出处理教程。未响应弹窗“缺少msvcp1......
  • 网络安全2024就业前景如何?找工作容易吗?
    众所周知,网络安全与我们息息相关,无论是企业还是个人都应该重视网络安全。 而网络安全作为一个新兴行业,人才需求量远大于供给,因此在薪资福利上具有很大的优势,但对于初学者而言,很多人依然担心前景问题,那么网络安全就业前景如何?本文将为大家介绍一下。从目前市场情况来讲,网络安......
  • Visual Studio 2022 的安装以及创建文件使用教程(详细版)
    一.下载官网地址:VisualStudio2022IDE-适用于软件开发人员的编程工具1.进入官网下载Community2022版,这个是社区版是免费开放的。2.点击“打开文件”进入下一步3.点击继续,进行预下载二.开始配置环境1.点击自己想要下载的编程语言进行环境配置,要注意的是为了以防万......
  • 【LaTeX】13表格梳理:基本表格、跨行(multirow)和跨列(multicolumn)表格
    【LaTeX】表格梳理:基本表格、跨行(multirow)和跨列(multicolumn)表格写在最前面一、基本表格1.表头标题2.常用选项[htbp]3.其他二、表格美化1.跨列(multicolumn)2.跨行(multirow)三、全部latex代码四、小结......
  • Adobe Premiere(简称PR2024)视频编辑软件下载
    一、发展历史1.1早期版本AdobePremiere(简称PR)是Adobe公司推出的一款视频编辑软件,其历史可以追溯到1991年。当时,Adobe发布了Premiere1.0版本,仅支持MacOS系统。1993年9月,Adobe公司推出了首个针对Windows系统的Premiere版本,这使得更多用户能够接触和使用这款强大的视频编辑工......
  • NOIP2024集训Day53 图论
    NOIP2024集训Day53图论A.[BZOJ4144ANOOZ2014]Petrol首先注意到起点和终点都是加油站。假设中途经过某个非加油站的点\(u\),\(u\)连到\(v\),离\(u\)最近的加油站是\(x\),那么从\(u\)到\(x\)加油后回到\(u\),再到\(v\)一定不比直接从\(u\)到\(v\)差。因为\(u......
  • 2024/10/17 模拟赛总结
    \(100+50+0+35=185\),呃呃呃,终于吃上LRX了#A.语言考虑名词性词组的性质,由于它可以由任意名词,形容词和名词性词组拼接起来,那么连续的名词,形容词或交替出现都是可行的但是如果最后一个是形容词不可行,不然它就无法修饰其他词语了于是可以枚举那一个单独的动词,判断前面和后面知......